From ba1e3af84df450dfc17dd757dc4a63257db1e2b9 Mon Sep 17 00:00:00 2001 From: Dronny Date: Thu, 25 Jul 2024 14:05:45 +0300 Subject: [PATCH 01/55] Now sdk has no dependencies on cli. But structure remains the same. --- requirements.txt | 5 +- snet/sdk/__init__.py | 20 +- snet/sdk/account.py | 2 +- snet/sdk/commands/__init__.py | 0 snet/sdk/commands/commands.py | 842 ++++++++++++++++++ snet/sdk/commands/mpe_service.py | 559 ++++++++++++ snet/sdk/commands/sdk_command.py | 39 + snet/sdk/concurrency_manager.py | 2 +- snet/sdk/config.py | 283 ++++++ snet/sdk/contract.py | 27 + snet/sdk/identity.py | 341 +++++++ snet/sdk/metadata/__init__.py | 0 snet/sdk/metadata/organization.py | 355 ++++++++ snet/sdk/metadata/service.py | 543 +++++++++++ .../ipfs_metadata_provider.py | 4 +- snet/sdk/mpe/payment_channel.py | 3 +- .../freecall_payment_strategy.py | 4 +- snet/sdk/resources/package.json | 5 + .../sdk/resources/proto/control_service.proto | 55 ++ .../resources/proto/control_service_pb2.py | 33 + .../proto/control_service_pb2_grpc.py | 137 +++ snet/sdk/resources/proto/merckledag.proto | 17 + snet/sdk/resources/proto/merckledag_pb2.py | 27 + .../resources/proto/merckledag_pb2_grpc.py | 4 + snet/sdk/resources/proto/state_service.proto | 81 ++ snet/sdk/resources/proto/state_service_pb2.py | 36 + .../resources/proto/state_service_pb2_grpc.py | 152 ++++ snet/sdk/resources/proto/token_service.proto | 63 ++ snet/sdk/resources/proto/token_service_pb2.py | 30 + .../resources/proto/token_service_pb2_grpc.py | 98 ++ snet/sdk/resources/proto/training.proto | 112 +++ snet/sdk/resources/proto/training_pb2.py | 47 + snet/sdk/resources/proto/training_pb2_grpc.py | 203 +++++ snet/sdk/resources/proto/unixfs.proto | 25 + snet/sdk/resources/proto/unixfs_pb2.py | 29 + snet/sdk/resources/proto/unixfs_pb2_grpc.py | 4 + snet/sdk/resources/root_certificate.py | 1 + snet/sdk/service_client.py | 4 +- snet/sdk/training/training.py | 4 +- snet/sdk/utils/__init__.py | 0 snet/sdk/utils/config.py | 62 ++ snet/sdk/utils/ipfs_utils.py | 122 +++ snet/sdk/utils/utils.py | 310 +++++++ 43 files changed, 4663 insertions(+), 27 deletions(-) create mode 100644 snet/sdk/commands/__init__.py create mode 100644 snet/sdk/commands/commands.py create mode 100644 snet/sdk/commands/mpe_service.py create mode 100644 snet/sdk/commands/sdk_command.py create mode 100644 snet/sdk/config.py create mode 100644 snet/sdk/contract.py create mode 100644 snet/sdk/identity.py create mode 100644 snet/sdk/metadata/__init__.py create mode 100644 snet/sdk/metadata/organization.py create mode 100644 snet/sdk/metadata/service.py create mode 100644 snet/sdk/resources/package.json create mode 100644 snet/sdk/resources/proto/control_service.proto create mode 100644 snet/sdk/resources/proto/control_service_pb2.py create mode 100644 snet/sdk/resources/proto/control_service_pb2_grpc.py create mode 100644 snet/sdk/resources/proto/merckledag.proto create mode 100644 snet/sdk/resources/proto/merckledag_pb2.py create mode 100644 snet/sdk/resources/proto/merckledag_pb2_grpc.py create mode 100644 snet/sdk/resources/proto/state_service.proto create mode 100644 snet/sdk/resources/proto/state_service_pb2.py create mode 100644 snet/sdk/resources/proto/state_service_pb2_grpc.py create mode 100644 snet/sdk/resources/proto/token_service.proto create mode 100644 snet/sdk/resources/proto/token_service_pb2.py create mode 100644 snet/sdk/resources/proto/token_service_pb2_grpc.py create mode 100644 snet/sdk/resources/proto/training.proto create mode 100644 snet/sdk/resources/proto/training_pb2.py create mode 100644 snet/sdk/resources/proto/training_pb2_grpc.py create mode 100644 snet/sdk/resources/proto/unixfs.proto create mode 100644 snet/sdk/resources/proto/unixfs_pb2.py create mode 100644 snet/sdk/resources/proto/unixfs_pb2_grpc.py create mode 100644 snet/sdk/resources/root_certificate.py create mode 100644 snet/sdk/utils/__init__.py create mode 100644 snet/sdk/utils/config.py create mode 100644 snet/sdk/utils/ipfs_utils.py create mode 100644 snet/sdk/utils/utils.py diff --git a/requirements.txt b/requirements.txt index ac89b28..a66518a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,6 +17,7 @@ argcomplete==3.1.2 grpcio-health-checking==1.59.0 jsonschema==4.0.0 eth-account==0.9.0 -snet.cli==2.1.3 snet.contracts==0.1.1 -urllib3>=2.2.2 \ No newline at end of file +urllib3>=2.2.2 +trezor==0.13.8 +ledgerblue==0.1.48 diff --git a/snet/sdk/__init__.py b/snet/sdk/__init__.py index 8f5063c..96f1f95 100644 --- a/snet/sdk/__init__.py +++ b/snet/sdk/__init__.py @@ -4,7 +4,6 @@ import sys from typing import Any, NewType import warnings -import copy import google.protobuf.internal.api_implementation @@ -15,10 +14,10 @@ from snet.sdk.metadata_provider.ipfs_metadata_provider import IPFSMetadataProvider from snet.sdk.payment_strategies.default_payment_strategy import DefaultPaymentStrategy -from snet.cli.commands.sdk_command import SDKCommand -from snet.cli.commands.commands import BlockchainCommand -from snet.cli.config import Config -from snet.cli.utils.utils import bytes32_to_str, type_converter +from snet.sdk.commands.sdk_command import SDKCommand +from snet.sdk.commands.commands import BlockchainCommand +from snet.sdk.config import Config +from snet.sdk.utils.utils import bytes32_to_str, type_converter google.protobuf.internal.api_implementation.Type = lambda: 'python' @@ -27,12 +26,7 @@ _sym_db = _symbol_database.Default() _sym_db.RegisterMessage = lambda x: None - -from urllib.parse import urljoin - - import web3 -from rfc3986 import urlparse import ipfshttpclient from snet.sdk.service_client import ServiceClient @@ -41,9 +35,9 @@ from snet.contracts import get_contract_object -from snet.cli.metadata.service import mpe_service_metadata_from_json -from snet.cli.utils.ipfs_utils import bytesuri_to_hash, get_from_ipfs_and_checkhash -from snet.cli.utils.utils import find_file_by_keyword +from snet.sdk.metadata.service import mpe_service_metadata_from_json +from snet.sdk.utils.ipfs_utils import bytesuri_to_hash, get_from_ipfs_and_checkhash +from snet.sdk.utils.utils import find_file_by_keyword ModuleName = NewType('ModuleName', str) ServiceStub = NewType('ServiceStub', Any) diff --git a/snet/sdk/account.py b/snet/sdk/account.py index d77cc9b..df71493 100644 --- a/snet/sdk/account.py +++ b/snet/sdk/account.py @@ -1,6 +1,6 @@ import json -from snet.cli.utils.utils import get_address_from_private, normalize_private_key +from snet.sdk.utils.utils import get_address_from_private, normalize_private_key from snet.contracts import get_contract_object DEFAULT_GAS = 300000 diff --git a/snet/sdk/commands/__init__.py b/snet/sdk/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/snet/sdk/commands/commands.py b/snet/sdk/commands/commands.py new file mode 100644 index 0000000..a5882f6 --- /dev/null +++ b/snet/sdk/commands/commands.py @@ -0,0 +1,842 @@ +import base64 +import getpass +import json +import secrets +import sys +from textwrap import indent +from urllib.parse import urljoin + +import ipfshttpclient +import yaml +from rfc3986 import urlparse +import web3 +from snet.contracts import get_contract_def + +from snet.sdk.contract import Contract +from snet.sdk.identity import KeyIdentityProvider, KeyStoreIdentityProvider, LedgerIdentityProvider, \ + MnemonicIdentityProvider, RpcIdentityProvider, TrezorIdentityProvider, get_kws_for_identity_type +from snet.sdk.metadata.organization import OrganizationMetadata, PaymentStorageClient, Payment, Group +from snet.sdk.utils.config import get_contract_address, get_field_from_args_or_session, \ + read_default_contract_address +from snet.sdk.utils.ipfs_utils import bytesuri_to_hash, get_from_ipfs_and_checkhash, \ + hash_to_bytesuri, publish_file_in_ipfs +from snet.sdk.utils.utils import DefaultAttributeObject, get_web3, is_valid_url, serializable, type_converter, \ + get_cli_version, bytes32_to_str + + +class Command(object): + def __init__(self, config, args, out_f=sys.stdout, err_f=sys.stderr): + self.config = config + self.args = args + self.out_f = out_f + self.err_f = err_f + + def _error(self, message): + self._printerr("ERROR: {}".format(message)) + sys.exit(1) + + def _ensure(self, condition, message): + if not condition: + self._error(message) + + @staticmethod + def _print(message, fd): + message = str(message) + "\n" + try: + fd.write(message) + except UnicodeEncodeError: + if hasattr(fd, "buffer"): + fd.buffer.write(message.encode("utf-8")) + else: + raise + + def _printout(self, message): + if self.out_f is not None: + self._print(message, self.out_f) + + def _printerr(self, message): + if self.err_f is not None: + self._print(message, self.err_f) + + def _pprint(self, item): + self._printout(indent(yaml.dump(json.loads(json.dumps(item, default=serializable)), default_flow_style=False, + indent=4), " ")) + + def _pprint_receipt_and_events(self, receipt, events): + if self.args.verbose: + self._pprint({"receipt": receipt, "events": events}) + elif self.args.quiet: + self._pprint({"transactionHash": receipt["transactionHash"]}) + else: + self._pprint({"receipt_summary": {"blockHash": receipt["blockHash"], + "blockNumber": receipt["blockNumber"], + "cumulativeGasUsed": receipt["cumulativeGasUsed"], + "gasUsed": receipt["gasUsed"], + "transactionHash": receipt["transactionHash"]}, + "event_summaries": [{"args": e["args"], "event": e["event"]} for e in events]}) + + def _get_ipfs_client(self): + ipfs_endpoint = self.config.get_ipfs_endpoint() + return ipfshttpclient.connect(ipfs_endpoint) + + +class VersionCommand(Command): + def show(self): + self._pprint({"version": get_cli_version()}) + + +""" +# Temporally deprecated + +class CachedGasPriceStrategy: + def __init__(self, gas_price_param): + self.gas_price_param = gas_price_param + self.cached_gas_price = None + + def __call__(self, w3, transaction_params): + if self.cached_gas_price is None: + self.cached_gas_price = int( + self.calc_gas_price(w3, transaction_params)) + return self.cached_gas_price + + def calc_gas_price(self, w3, transaction_params): + gas_price_param = self.gas_price_param + if gas_price_param.isdigit(): + return int(self.gas_price_param) + if gas_price_param == "fast": + return fast_gas_price_strategy(w3, transaction_params) + if gas_price_param == "medium": + return medium_gas_price_strategy(w3, transaction_params) + if gas_price_param == "slow": + return slow_gas_price_strategy(w3, transaction_params) + raise Exception("Unknown gas price strategy: %s" % gas_price_param) + + def is_going_to_calculate(self): + return self.cached_gas_price is None and not self.gas_price_param.isdigit() +""" + + +class BlockchainCommand(Command): + def __init__(self, config, args, out_f=sys.stdout, err_f=sys.stderr, w3=None, ident=None): + super(BlockchainCommand, self).__init__(config, args, out_f, err_f) + self.w3 = w3 or get_web3(self.get_eth_endpoint()) + self.ident = ident or self.get_identity() + + def get_eth_endpoint(self): + # the only one source of eth_rpc_endpoint is the configuration file + return self.config.get_session_field("default_eth_rpc_endpoint") + + def get_wallet_index(self): + return int(get_field_from_args_or_session(self.config, self.args, "wallet_index")) + + def get_gas_price_param(self): + return get_field_from_args_or_session(self.config, self.args, "gas_price") + + def get_gas_price_verbose(self): + # gas price is not given explicitly in Wei + self._printerr("# Calculating gas price... one moment..") + gas_price = self.w3.eth.gas_price + if gas_price <= 15000000000: + gas_price += gas_price * 1 / 3 + elif gas_price > 15000000000 and gas_price <= 50000000000: + gas_price += gas_price * 1 / 5 + elif gas_price > 50000000000 and gas_price <= 150000000000: + gas_price += 7000000000 + elif gas_price > 150000000000: + gas_price += gas_price * 1 / 10 + self._printerr("# gas_price = %f GWei" % (gas_price * 1E-9)) + return int(gas_price) + + def get_mpe_address(self): + return get_contract_address(self, "MultiPartyEscrow") + + def get_registry_address(self): + return get_contract_address(self, "Registry") + + def get_identity(self): + identity_type = self.config.get_session_field("identity_type") + + if identity_type == "rpc": + return RpcIdentityProvider(self.w3, self.get_wallet_index()) + if identity_type == "mnemonic": + return MnemonicIdentityProvider(self.w3, self.config.get_session_field("mnemonic"), self.get_wallet_index()) + if identity_type == "trezor": + return TrezorIdentityProvider(self.w3, self.get_wallet_index()) + if identity_type == "ledger": + return LedgerIdentityProvider(self.w3, self.get_wallet_index()) + if identity_type == "key": + return KeyIdentityProvider(self.w3, self.config.get_session_field("private_key")) + if identity_type == "keystore": + return KeyStoreIdentityProvider(self.w3, self.config.get_session_field("keystore_path")) + + def get_contract_argser(self, contract_address, contract_function, contract_def, **kwargs): + def f(*positional_inputs, **named_inputs): + args_dict = self.args.__dict__.copy() + args_dict.update(dict( + at=contract_address, + contract_function=contract_function, + contract_def=contract_def, + contract_positional_inputs=positional_inputs, + **kwargs + )) + for k, v in named_inputs.items(): + args_dict["contract_named_input_{}".format(k)] = v + return DefaultAttributeObject(**args_dict) + + return f + + def get_contract_command(self, contract_name, contract_address, contract_fn, contract_params, is_silent=True): + contract_def = get_contract_def(contract_name) + if is_silent: + out_f = None + err_f = None + else: + out_f = self.out_f + err_f = self.err_f + return ContractCommand(config=self.config, + args=self.get_contract_argser( + contract_address=contract_address, + contract_function=contract_fn, + contract_def=contract_def, + contract_name=contract_name)(*contract_params), + out_f=out_f, + err_f=err_f, + w3=self.w3, + ident=self.ident) + + def call_contract_command(self, contract_name, contract_fn, contract_params, is_silent=True): + contract_address = get_contract_address(self, contract_name) + return self.get_contract_command(contract_name, contract_address, + contract_fn, contract_params, is_silent).call() + + def transact_contract_command(self, contract_name, contract_fn, contract_params, is_silent=False): + contract_address = get_contract_address(self, contract_name) + return self.get_contract_command(contract_name, contract_address, contract_fn, contract_params, + is_silent).transact() + + +class IdentityCommand(Command): + def create(self): + identity = {} + + identity_name = self.args.identity_name + self._ensure(identity_name not in self.config.get_all_identities_names(), + "identity_name {} already exists".format(identity_name)) + + identity_type = self.args.identity_type + identity["identity_type"] = identity_type + + for kw, is_secret in get_kws_for_identity_type(identity_type): + value = getattr(self.args, kw) + if value is None and is_secret: + kw_prompt = "{}: ".format(" ".join(kw.capitalize().split("_"))) + value = getpass.getpass(kw_prompt) or None + self._ensure( + value is not None, "--{} is required for identity_type {}".format(kw, identity_type)) + identity[kw] = value + + if self.args.network: + identity["network"] = self.args.network + identity["default_wallet_index"] = self.args.wallet_index + self.config.add_identity(identity_name, identity, self.out_f) + + def list(self): + for identity_section in filter(lambda x: x.startswith("identity."), self.config.sections()): + identity = self.config[identity_section] + key_is_secret_lookup = {} + + identity_type = self.config.get(identity_section, 'identity_type') + for kw, is_secret in get_kws_for_identity_type(identity_type): + key_is_secret_lookup[kw] = is_secret + + self._pprint({ + identity_section[len("identity."):]: { + k: (v if not key_is_secret_lookup.get(k, False) else "xxxxxx") for k, v in identity.items() + } + }) + + def delete(self): + self.config.delete_identity(self.args.identity_name) + + def set(self): + self.config.set_session_identity(self.args.identity_name, self.out_f) + + +class NetworkCommand(Command): + def list(self): + for network_section in filter(lambda x: x.startswith("network."), self.config.sections()): + network = self.config[network_section] + self._pprint({network_section[len("network."):]: { + k: v for k, v in network.items()}}) + + def create(self): + network_id = None + w3 = get_web3(self.args.eth_rpc_endpoint) + if not self.args.skip_check: + # check endpoint by getting its network_id + network_id = w3.net.version + + self._printout("add network with name='%s' with networkId='%s'" % ( + self.args.network_name, str(network_id))) + + default_gas_price = w3.eth.gas_price + self.config.add_network( + self.args.network_name, self.args.eth_rpc_endpoint, default_gas_price) + + def set(self): + self.config.set_session_network(self.args.network_name, self.out_f) + + +class SessionSetCommand(Command): + def set(self): + self.config.set_session_field( + self.args.key, self.args.value, self.out_f) + + def unset(self): + self.config.unset_session_field(self.args.key, self.out_f) + + +class SessionShowCommand(BlockchainCommand): + def show(self): + rez = self.config.session_to_dict() + key = "network.%s" % rez['session']['network'] + self.populate_contract_address(rez, key) + + # we don't want to who private_key and mnemonic + for d in rez.values(): + d.pop("private_key", None) + d.pop("mnemonic", None) + self._pprint(rez) + + def populate_contract_address(self, rez, key): + try: + rez[key]['default_registry_at'] = read_default_contract_address( + w3=self.w3, contract_name="Registry") + rez[key]['default_multipartyescrow_at'] = read_default_contract_address( + w3=self.w3, contract_name="MultiPartyEscrow") + rez[key]['default_singularitynettoken_at'] = read_default_contract_address( + w3=self.w3, contract_name="SingularityNetToken") + except Exception as e: + pass + return + + +class ContractCommand(BlockchainCommand): + def call(self): + contract_address = get_contract_address(self, self.args.contract_name, + "--at is required to specify target contract address") + + abi = self.args.contract_def["abi"] + + contract = Contract(self.w3, contract_address, abi) + + positional_inputs = getattr( + self.args, "contract_positional_inputs", []) + named_inputs = { + name[len("contract_named_input_"):]: value for name, value + in self.args.__dict__.items() if name.startswith("contract_named_input_") + } + + result = contract.call(self.args.contract_function, + *positional_inputs, **named_inputs) + self._printout(result) + return result + + def transact(self): + contract_address = get_contract_address(self, self.args.contract_name, + "--at is required to specify target contract address") + + abi = self.args.contract_def["abi"] + + contract = Contract(self.w3, contract_address, abi) + + positional_inputs = getattr( + self.args, "contract_positional_inputs", []) + named_inputs = { + name[len("contract_named_input_"):]: value for name, value + in self.args.__dict__.items() if name.startswith("contract_named_input_") + } + + gas_price = self.get_gas_price_verbose() + + txn = contract.build_transaction(self.args.contract_function, + self.ident.get_address(), + gas_price, + *positional_inputs, + **named_inputs) + + if not self.args.yes or self.args.verbose: + self._pprint({"transaction": txn}) + + proceed = self.args.yes or input("Proceed? (y/n): ") == "y" + + if proceed: + receipt = self.ident.transact(txn, self.err_f) + events = contract.process_receipt(receipt) + self._pprint_receipt_and_events(receipt, events) + return receipt, events + else: + self._error("Cancelled") + + +class OrganizationCommand(BlockchainCommand): + + def add_group(self): + metadata_file = self.args.metadata_file + + try: + with open(metadata_file, 'r') as f: + org_metadata = OrganizationMetadata.from_json(json.load(f)) + except Exception as e: + print( + "Organization metadata json file not found ,Please check --metadata-file path ") + raise e + + for endpoint in self.args.endpoints: + if not is_valid_url(endpoint): + raise Exception(f"Invalid {endpoint} endpoint passed") + + payment_storage_client = PaymentStorageClient(self.args.payment_channel_connection_timeout, + self.args.payment_channel_request_timeout, self.args.endpoints) + payment = Payment(self.args.payment_address, self.args.payment_expiration_threshold, + self.args.payment_channel_storage_type, payment_storage_client) + group_id = base64.b64encode(secrets.token_bytes(32)) + + group = Group(self.args.group_name, group_id.decode("ascii"), payment) + org_metadata.add_group(group) + org_metadata.save_pretty(metadata_file) + + def remove_group(self): + group_id = self.args.group_id + metadata_file = self.args.metadata_file + + try: + with open(metadata_file, 'r') as f: + org_metadata = OrganizationMetadata.from_json(json.load(f)) + except Exception as e: + print( + "Organization metadata json file not found ,Please check --metadata-file path ") + raise e + + existing_groups = org_metadata.groups + updated_groups = [ + group for group in existing_groups if not group_id == group.group_id] + org_metadata.groups = updated_groups + org_metadata.save_pretty(metadata_file) + + def set_changed_values_for_group(self, group): + # if value of a parameter is None that means it was not updated + + if self.args.endpoints: + group.update_endpoints(self.args.endpoints) + if self.args.payment_address: + group.update_payment_address(self.args.payment_address) + if self.args.payment_expiration_threshold: + group.update_payment_expiration_threshold( + self.args.payment_expiration_threshold) + if self.args.payment_channel_storage_type: + group.update_payment_channel_storage_type( + self.args.payment_channel_storage_type) + if self.args.payment_channel_connection_timeout: + group.update_connection_timeout( + self.args.payment_channel_connection_timeout) + if self.args.payment_channel_request_timeout: + group.update_request_timeout( + self.args.payment_channel_request_timeout) + + def update_group(self): + group_id = self.args.group_id + metadata_file = self.args.metadata_file + try: + with open(metadata_file, 'r') as f: + org_metadata = OrganizationMetadata.from_json(json.load(f)) + except Exception as e: + print( + "Organization metadata json file not found ,Please check --metadata-file path ") + raise e + existing_groups = org_metadata.groups + for group in existing_groups: + if group_id == group.group_id: + self.set_changed_values_for_group(group) + + org_metadata.save_pretty(metadata_file) + + def initialize_metadata(self): + org_id = self.args.org_id + metadata_file_name = self.args.metadata_file + + # Check if Organization already exists + found = self._get_organization_by_id(org_id)[0] + if found: + raise Exception( + "\nOrganization with id={} already exists!\n".format(org_id)) + org_metadata = OrganizationMetadata(self.args.org_name, org_id, self.args.org_type) + org_metadata.save_pretty(metadata_file_name) + + def print_metadata(self): + org_id = self.args.org_id + org_metadata = self._get_organization_metadata_from_registry(org_id) + self._printout(org_metadata.get_json_pretty()) + + def _get_organization_registration(self, org_id): + params = [type_converter("bytes32")(org_id)] + rez = self.call_contract_command( + "Registry", "getOrganizationById", params) + if not rez[0]: + raise Exception("Cannot find Organization with id=%s" % ( + self.args.org_id)) + return {"orgMetadataURI": rez[2]} + + def _get_organization_metadata_from_registry(self, org_id): + rez = self._get_organization_registration(org_id) + metadata_hash = bytesuri_to_hash(rez["orgMetadataURI"]) + metadata = get_from_ipfs_and_checkhash( + self._get_ipfs_client(), metadata_hash) + metadata = metadata.decode("utf-8") + return OrganizationMetadata.from_json(json.loads(metadata)) + + def _get_organization_by_id(self, org_id): + org_id_bytes32 = type_converter("bytes32")(org_id) + if len(org_id_bytes32) > 32: + raise Exception("Your org_id is too long, it should be 32 bytes or less. len(org_id_bytes32)=%i" % ( + len(org_id_bytes32))) + return self.call_contract_command("Registry", "getOrganizationById", [org_id_bytes32]) + + # TODO: It would be better to have standard nargs="+" in argparse for members. + # But we keep comma separated members for backward compatibility + def get_members_from_args(self): + if not self.args.members: + return [] + members = [m.replace("[", "").replace("]", "") + for m in self.args.members.split(',')] + for m in members: + if not web3.Web3.is_checksum_address(m): + raise Exception( + "Member account %s is not a valid Ethereum checksum address" % m) + return members + + def list(self): + org_list = self.call_contract_command( + "Registry", "listOrganizations", []) + + self._printout("# OrgId") + for idx, org_id in enumerate(org_list): + self._printout(bytes32_to_str(org_id)) + + def list_org_name(self): + org_list = self.call_contract_command( + "Registry", "listOrganizations", []) + + self._printout("# OrgName OrgId") + for idx, org_id in enumerate(org_list): + rez = self.call_contract_command( + "Registry", "getOrganizationById", [org_id]) + if not rez[0]: + raise Exception( + "Organization was removed during this call. Please retry.") + org_name = rez[2] + self._printout("%s %s" % (org_name, bytes32_to_str(org_id))) + + def error_organization_not_found(self, org_id, found): + if not found: + raise Exception( + "Organization with id={} doesn't exist!\n".format(org_id)) + + def info(self): + org_id = self.args.org_id + (found, org_id, org_name, owner, members, serviceNames) = self._get_organization_by_id(org_id) + self.error_organization_not_found(self.args.org_id, found) + + org_m = self._get_organization_metadata_from_registry(web3.Web3.to_text(org_id)) + org_name = org_m.org_name + org_type = org_m.org_type + description = org_m.description.get("description", "") + + self._printout("\nOrganization Name:\n - %s" % org_name) + self._printout("\nId:\n - %s" % bytes32_to_str(org_id)) + self._printout("\nType:\n - %s" % org_type) + self._printout("\nDescription:\n - %s" % description) + + if members: + self._printout("\nMembers:") + for idx, member in enumerate(members): + self._printout(" - {}".format(member)) + if serviceNames: + self._printout("\nServices:") + for idx, service in enumerate(serviceNames): + self._printout(" - {}".format(bytes32_to_str(service))) + + def create(self): + + metadata_file = self.args.metadata_file + + try: + with open(metadata_file, 'r') as f: + org_metadata = OrganizationMetadata.from_json(json.load(f)) + except Exception as e: + print( + "Organization metadata json file not found ,Please check --metadata-file path ") + raise e + org_id = self.args.org_id + # validate the metadata before creating + org_metadata.validate() + + # R Check if Organization already exists + found = self._get_organization_by_id(org_id)[0] + if found: + raise Exception( + "\nOrganization with id={} already exists!\n".format(org_id)) + + members = self.get_members_from_args() + + ipfs_metadata_uri = publish_file_in_ipfs( + self._get_ipfs_client(), metadata_file, False) + params = [type_converter("bytes32")( + org_id), hash_to_bytesuri(ipfs_metadata_uri), members] + self._printout("Creating transaction to create organization name={} id={}\n".format( + org_metadata.org_name, org_id)) + self.transact_contract_command( + "Registry", "createOrganization", params) + self._printout("id:\n%s" % org_id) + + def delete(self): + org_id = self.args.org_id + # Check if Organization exists + (found, _, org_name, _, _, _) = self._get_organization_by_id(org_id) + self.error_organization_not_found(org_id, found) + + self._printout("Creating transaction to delete organization with name={} id={}".format( + org_name, org_id)) + try: + self.transact_contract_command("Registry", "deleteOrganization", [ + type_converter("bytes32")(org_id)]) + except Exception as e: + self._printerr( + "\nTransaction error!\nHINT: Check if you are the owner of organization with id={}\n".format(org_id)) + raise + + def update_metadata(self): + metadata_file = self.args.metadata_file + + try: + with open(metadata_file, 'r') as f: + org_metadata = OrganizationMetadata.from_json(json.load(f)) + except Exception as e: + print( + "Organization metadata json file not found ,Please check --metadata-file path ") + raise e + # validate the metadata before updating + + org_id = self.args.org_id + existing_registry_org_metadata = self._get_organization_metadata_from_registry( + org_id) + org_metadata.validate(existing_registry_org_metadata) + + # Check if Organization already exists + found = self._get_organization_by_id(org_id)[0] + if not found: + raise Exception( + "\nOrganization with id={} does not exists!\n".format(org_id)) + + ipfs_metadata_uri = publish_file_in_ipfs( + self._get_ipfs_client(), metadata_file, False) + params = [type_converter("bytes32")( + org_id), hash_to_bytesuri(ipfs_metadata_uri)] + self._printout( + "Creating transaction to create organization name={} id={}\n".format(org_metadata.org_name, org_id)) + self.transact_contract_command( + "Registry", "changeOrganizationMetadataURI", params) + self._printout("id:\n%s" % org_id) + + def list_services(self): + org_id = self.args.org_id + (found, org_service_list) = self.call_contract_command("Registry", "listServicesForOrganization", + [type_converter("bytes32")(org_id)]) + self.error_organization_not_found(org_id, found) + if org_service_list: + self._printout("\nList of {}'s Services:".format(org_id)) + for idx, org_service in enumerate(org_service_list): + self._printout("- {}".format(bytes32_to_str(org_service))) + else: + self._printout( + "Organization with id={} exists but has no registered services.".format(org_id)) + + def change_owner(self): + org_id = self.args.org_id + # Check if Organization exists + (found, _, _, owner, _, _) = self._get_organization_by_id(org_id) + self.error_organization_not_found(org_id, found) + + new_owner = self.args.owner + if not web3.Web3.is_checksum_address(new_owner): + raise Exception( + "New owner account %s is not a valid Ethereum checksum address" % new_owner) + + if new_owner.lower() == owner.lower(): + raise Exception( + "\n{} is the owner of Organization with id={}!\n".format(new_owner, org_id)) + + self._printout( + "Creating transaction to change organization {}'s owner...\n".format(org_id)) + try: + self.transact_contract_command("Registry", "changeOrganizationOwner", + [type_converter("bytes32")(org_id), self.args.owner]) + except Exception as e: + self._printerr( + "\nTransaction error!\nHINT: Check if you are the owner of {}\n".format(org_id)) + raise + + def add_members(self): + org_id = self.args.org_id + # Check if Organization exists and member is not part of it + (found, _, _, _, members, _) = self._get_organization_by_id(org_id) + self.error_organization_not_found(org_id, found) + + members = [member.lower() for member in members] + add_members = [] + for add_member in self.get_members_from_args(): + if add_member.lower() in members: + self._printout( + "{} is already a member of organization {}".format(add_member, org_id)) + else: + add_members.append(add_member) + + if not add_members: + self._printout("No member was added to {}!\n".format(org_id)) + return + + params = [type_converter("bytes32")(org_id), add_members] + self._printout( + "Creating transaction to add {} members into organization {}...\n".format(len(add_members), org_id)) + try: + self.transact_contract_command( + "Registry", "addOrganizationMembers", params) + except Exception as e: + self._printerr( + "\nTransaction error!\nHINT: Check if you are the owner of {}\n".format(org_id)) + raise + + def rem_members(self): + org_id = self.args.org_id + # Check if Organization exists and member is part of it + (found, _, _, _, members, _) = self._get_organization_by_id(org_id) + self.error_organization_not_found(org_id, found) + + members = [member.lower() for member in members] + rem_members = [] + for rem_member in self.get_members_from_args(): + if rem_member.lower() not in members: + self._printout( + "{} is not a member of organization {}".format(rem_member, org_id)) + else: + rem_members.append(rem_member) + + if not rem_members: + self._printout("No member was removed from {}!\n".format(org_id)) + return + + params = [type_converter("bytes32")(org_id), rem_members] + self._printout( + "Creating transaction to remove {} members from organization with id={}...\n".format(len(rem_members), + org_id)) + try: + self.transact_contract_command( + "Registry", "removeOrganizationMembers", params) + except Exception as e: + self._printerr( + "\nTransaction error!\nHINT: Check if you are the owner of {}\n".format(org_id)) + raise + + def list_my(self): + """ Find organization that has the current identity as the owner or as the member """ + org_list = self.call_contract_command( + "Registry", "listOrganizations", []) + + rez_owner = [] + rez_member = [] + for idx, org_id in enumerate(org_list): + (found, org_id, org_name, owner, members, serviceNames) = self.call_contract_command( + "Registry", "getOrganizationById", [org_id]) + if not found: + raise Exception( + "Organization was removed during this call. Please retry.") + if self.ident.address == owner: + rez_owner.append((org_name, bytes32_to_str(org_id))) + + if self.ident.address in members: + rez_member.append((org_name, bytes32_to_str(org_id))) + + if rez_owner: + self._printout("# Organizations you are the owner of") + self._printout("# OrgName OrgId") + for n, i in rez_owner: + self._printout("%s %s" % (n, i)) + + if rez_member: + self._printout("# Organizations you are the member of") + self._printout("# OrgName OrgId") + for n, i in rez_member: + self._printout("%s %s" % (n, i)) + + def metadata_add_asset_to_ipfs(self): + metadata_file = self.args.metadata_file + org_metadata = OrganizationMetadata.from_file(metadata_file) + asset_file_ipfs_hash_base58 = publish_file_in_ipfs(self._get_ipfs_client(), + self.args.asset_file_path) + + org_metadata.add_asset(asset_file_ipfs_hash_base58, self.args.asset_type) + org_metadata.save_pretty(self.args.metadata_file) + + def metadata_remove_assets_of_a_given_type(self): + metadata_file = self.args.metadata_file + org_metadata = OrganizationMetadata.from_file(metadata_file) + org_metadata.remove_assets(self.args.asset_type) + org_metadata.save_pretty(self.args.metadata_file) + + def metadata_remove_all_assets(self): + metadata_file = self.args.metadata_file + org_metadata = OrganizationMetadata.from_file(metadata_file) + org_metadata.remove_all_assets() + org_metadata.save_pretty(self.args.metadata_file) + + def metadata_add_description(self): + description = self.args.description + url = self.args.url + short_description = self.args.short_description + metadata_file = self.args.metadata_file + org_metadata = OrganizationMetadata.from_file(metadata_file) + if description: + org_metadata.add_description(description) + if short_description: + org_metadata.add_short_description(short_description) + if url: + org_metadata.add_url(url) + if description is None and url is None and short_description is None: + raise Exception("No attributes are given") + org_metadata.save_pretty(metadata_file) + + def metadata_add_contact(self): + args = self.args.__dict__ + metadata_file = args["metadata_file"] + contact_type = args.get("contact_type", None) + phone = args.get("phone", None) + email = args.get("email", None) + if phone is None and email is None: + self._printout("email and phone both can not be empty") + else: + org_metadata = OrganizationMetadata.from_file(metadata_file) + org_metadata.add_contact(contact_type, phone, email) + org_metadata.save_pretty(metadata_file) + + def metadata_remove_contact_by_type(self): + metadata_file = self.args.metadata_file + contact_type = self.args.contact_type + org_metadata = OrganizationMetadata.from_file(metadata_file) + org_metadata.remove_contact_by_type(contact_type) + org_metadata.save_pretty(metadata_file) + + def metadata_remove_all_contacts(self): + metadata_file = self.args.metadata_file + org_metadata = OrganizationMetadata.from_file(metadata_file) + org_metadata.remove_all_contacts() + org_metadata.save_pretty(metadata_file) diff --git a/snet/sdk/commands/mpe_service.py b/snet/sdk/commands/mpe_service.py new file mode 100644 index 0000000..2a21b60 --- /dev/null +++ b/snet/sdk/commands/mpe_service.py @@ -0,0 +1,559 @@ +import json +from collections import defaultdict +from pathlib import Path +from re import search +from sys import exit + +from grpc_health.v1 import health_pb2 as heartb_pb2 +from grpc_health.v1 import health_pb2_grpc as heartb_pb2_grpc +from jsonschema import validate, ValidationError + +from snet.sdk.commands.commands import BlockchainCommand +from snet.sdk.metadata.organization import OrganizationMetadata +from snet.sdk.metadata.service import MPEServiceMetadata, load_mpe_service_metadata, mpe_service_metadata_from_json +from snet.sdk.utils import ipfs_utils +from snet.sdk.utils.utils import is_valid_url, open_grpc_channel, type_converter + + +class MPEServiceCommand(BlockchainCommand): + + def publish_proto_in_ipfs(self): + """ Publish proto files in ipfs and print hash """ + ipfs_hash_base58 = ipfs_utils.publish_proto_in_ipfs( + self._get_ipfs_client(), self.args.protodir) + self._printout(ipfs_hash_base58) + + def service_metadata_init(self): + """Utility for creating a service metadata file. + + CLI questionnaire for service metadata creation. Creates a `service_metadata.json` + (if file name is not set) with values entered by the user in the questionnaire utility. + + Mandatory args: + display_name: Display name of the service. + org_id: Organization ID the service would be assosciated with. + protodir_path: Directory containing protobuf files. + groups: Payment groups supported by the organization (default: `default_group`). If multiple + payment groups, ask user for entry. + endpoints: Storage end points for the clients to connect. + daemon_addresses: Ethereum public addresses of daemon in given payment group of service. + + Optional args: + url: Service user guide resource. + long_description: Long description of service. + short_description: Service overview. + contributors: Contributor name and email-id. + file_name: Service metdadata filename. + """ + print("This utility will walk you through creating the service metadata file.", + "It only covers the most common items and tries to guess sensible defaults.", + "", + "See `snet service metadata-init-utility -h` on how to use this utility.", + "", + "Press ^C at any time to quit.", sep='\n') + try: + metadata = MPEServiceMetadata() + while True: + display_name = input("display name: ").strip() + if display_name == "": + print("display name is required.") + else: + break + # Find number of payment groups available for organization + # If only 1, set `default_group` as payment group + while True: + org_id = input(f"organization id `{display_name}` service would be linked to: ").strip() + while org_id == "": + org_id = input(f"organization id required: ").strip() + try: + org_metadata = self._get_organization_metadata_from_registry(org_id) + no_of_groups = len(org_metadata.groups) + break + except Exception: + print(f"`{org_id}` is invalid.") + while True: + try: + protodir_path = input("protodir path: ") + model_ipfs_hash_base58 = ipfs_utils.publish_proto_in_ipfs(self._get_ipfs_client(), protodir_path) + break + except Exception: + print(f'Invalid path: "{protodir_path}"') + if no_of_groups == 1: + metadata.group_init('default_group') + else: + while input("Add group? [y/n] ") == 'y': + metadata.group_init(input('group name: ')) + metadata.add_description() + metadata.add_contributor(input('Enter contributor name: '), input('Enter contributor email: ')) + while input('Add another contributor? [y/n] ').lower() == 'y': + metadata.add_contributor(input('Enter contributor name '), input('Enter contributor email: ')) + mpe_address = self.get_mpe_address() + + metadata.set_simple_field('model_ipfs_hash', model_ipfs_hash_base58) + metadata.set_simple_field('mpe_address', mpe_address) + metadata.set_simple_field('display_name', display_name) + print('', '', json.dumps(metadata.m, indent=2), sep='\n') + print("Are you sure you want to create? [y/n] ", end='') + if input() == 'y': + file_name = input(f"Choose file name: (service_metadata) ") or 'service_metadata' + file_name += '.json' + metadata.save_pretty(file_name) + print(f"{file_name} created.") + else: + exit("ABORTED.") + except KeyboardInterrupt: + exit("\n`snet service metadata-init-utility` CANCELLED.") + + def publish_proto_metadata_init(self): + model_ipfs_hash_base58 = ipfs_utils.publish_proto_in_ipfs( + self._get_ipfs_client(), self.args.protodir) + + metadata = MPEServiceMetadata() + mpe_address = self.get_mpe_address() + metadata.set_simple_field("model_ipfs_hash", model_ipfs_hash_base58) + metadata.set_simple_field("mpe_address", mpe_address) + metadata.set_simple_field("display_name", self.args.display_name) + metadata.set_simple_field("encoding", self.args.encoding) + metadata.set_simple_field("service_type", self.args.service_type) + + if self.args.group_name: + metadata.add_group(self.args.group_name) + if self.args.endpoints: + for endpoint in self.args.endpoints: + if not is_valid_url(endpoint): + raise Exception(f"Invalid {endpoint} endpoint passed") + metadata.add_endpoint_to_group( + self.args.group_name, endpoint) + if self.args.fixed_price is not None: + metadata.set_fixed_price_in_cogs( + self.args.group_name, self.args.fixed_price) + elif self.args.group_name or self.args.fixed_price: + raise Exception( + "endpoints / fixed price can be attached to a group please pass group_name") + metadata.save_pretty(self.args.metadata_file) + + def publish_proto_metadata_update(self): + """ Publish protobuf model in ipfs and update existing metadata file """ + metadata = load_mpe_service_metadata(self.args.metadata_file) + ipfs_hash_base58 = ipfs_utils.publish_proto_in_ipfs( + self._get_ipfs_client(), self.args.protodir) + metadata.set_simple_field("model_ipfs_hash", ipfs_hash_base58) + metadata.save_pretty(self.args.metadata_file) + + def metadata_set_fixed_price(self): + metadata = load_mpe_service_metadata(self.args.metadata_file) + metadata.set_fixed_price_in_cogs(self.args.group_name, self.args.price) + metadata.save_pretty(self.args.metadata_file) + + def metadata_set_method_price(self): + metadata = load_mpe_service_metadata(self.args.metadata_file) + metadata.set_method_price_in_cogs( + self.args.group_name, self.args.package_name, self.args.service_name, self.args.method, self.args.price) + metadata.save_pretty(self.args.metadata_file) + + def _metadata_add_group(self, metadata): + metadata.add_group(self.args.group_name) + + def metadata_add_group(self): + metadata = load_mpe_service_metadata(self.args.metadata_file) + self._metadata_add_group(metadata) + metadata.save_pretty(self.args.metadata_file) + + def metadata_remove_group(self): + metadata = load_mpe_service_metadata(self.args.metadata_file) + metadata.remove_group(self.args.group_name) + metadata.save_pretty(self.args.metadata_file) + + def metadata_set_free_calls(self): + metadata = load_mpe_service_metadata(self.args.metadata_file) + metadata.set_free_calls_for_group(self.args.group_name, int(self.args.free_calls)) + metadata.save_pretty(self.args.metadata_file) + + def metadata_set_freecall_signer_address(self): + metadata = load_mpe_service_metadata(self.args.metadata_file) + metadata.set_freecall_signer_address(self.args.group_name, self.args.signer_address) + metadata.save_pretty(self.args.metadata_file) + + def metadata_add_daemon_addresses(self): + """ Metadata: add daemon addresses to the group """ + metadata = load_mpe_service_metadata(self.args.metadata_file) + group_name = metadata.get_group_name_nonetrick(self.args.group_name) + for daemon_address in self.args.daemon_addresses: + metadata.add_daemon_address_to_group(group_name, daemon_address) + metadata.save_pretty(self.args.metadata_file) + + def metadata_remove_all_daemon_addresses(self): + """ Metadata: remove all daemon addresses from all groups """ + metadata = load_mpe_service_metadata(self.args.metadata_file) + metadata.remove_all_daemon_addresses_for_group(self.args.group_name) + metadata.save_pretty(self.args.metadata_file) + + def metadata_update_daemon_addresses(self): + """ Metadata: Remove all daemon addresses from the group and add new ones """ + metadata = load_mpe_service_metadata(self.args.metadata_file) + group_name = metadata.get_group_name_nonetrick(self.args.group_name) + metadata.remove_all_daemon_addresses_for_group(group_name) + for daemon_address in self.args.daemon_addresses: + metadata.add_daemon_address_to_group(group_name, daemon_address) + metadata.save_pretty(self.args.metadata_file) + + def metadata_add_endpoints(self): + """ Metadata: add endpoint to the group """ + metadata = load_mpe_service_metadata(self.args.metadata_file) + group_name = metadata.get_group_name_nonetrick(self.args.group_name) + for endpoint in self.args.endpoints: + metadata.add_endpoint_to_group(group_name, endpoint) + metadata.save_pretty(self.args.metadata_file) + + def metadata_remove_all_endpoints(self): + """ Metadata: remove all endpoints from all groups """ + metadata = load_mpe_service_metadata(self.args.metadata_file) + metadata.remove_all_endpoints_for_group(self.args.group_name) + metadata.save_pretty(self.args.metadata_file) + + def metadata_update_endpoints(self): + """ Metadata: Remove all endpoints from the group and add new ones """ + metadata = load_mpe_service_metadata(self.args.metadata_file) + group_name = metadata.get_group_name_nonetrick(self.args.group_name) + metadata.remove_all_endpoints_for_group(group_name) + for endpoint in self.args.endpoints: + metadata.add_endpoint_to_group(group_name, endpoint) + metadata.save_pretty(self.args.metadata_file) + + def metadata_add_asset_to_ipfs(self): + metadata = load_mpe_service_metadata(self.args.metadata_file) + asset_file_ipfs_hash_base58 = ipfs_utils.publish_file_in_ipfs(self._get_ipfs_client(), + self.args.asset_file_path) + metadata.add_asset(asset_file_ipfs_hash_base58, self.args.asset_type) + metadata.save_pretty(self.args.metadata_file) + + def metadata_remove_all_assets(self): + metadata = load_mpe_service_metadata(self.args.metadata_file) + metadata.remove_all_assets() + metadata.save_pretty(self.args.metadata_file) + + def metadata_remove_assets_of_a_given_type(self): + metadata = load_mpe_service_metadata(self.args.metadata_file) + metadata.remove_assets(self.args.asset_type) + metadata.save_pretty(self.args.metadata_file) + + def metadata_add_media(self): + """Metadata: Add new individual media + + Detects media type for files to be added on IPFS, explict declaration for external resources. + """ + metadata = load_mpe_service_metadata(self.args.metadata_file) + # Support endpoints only with SSL Certificate + url_validator = r'https?:\/\/(www\.)?([-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b[-a-zA-Z0-9()@:%_\+.~#?&//=]*)' + # Automatic media type identification + if search(r'^.+\.(jpg|jpeg|png|gif)$', self.args.media_url): + media_type = 'image' + elif search(r'^.+\.(mp4)$', self.args.media_url): + media_type = 'video' + elif search(url_validator, self.args.media_url): + while True: + try: + media_type = input(f"Enter the media type (image, video) present at {self.args.media_url}: ") + except ValueError: + print("Choose only between (image, video).") + else: + if media_type not in ('image', 'video'): + print("Choose only between (image, video).") + else: + break + else: + if search(r'(https:\/\/)?(www\.)+([-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b[-a-zA-Z0-9()@:%_\+.~#?&//=]*)', self.args.media_url): + raise ValueError("Media endpoint supported only for secure sites.") + else: + raise ValueError(f"Entered url '{self.args.media_url}' is invalid.") + file_extension_validator = r'^.+\.(jpg|jpeg|JPG|png|gif|GIF|mp4)$' + # Detect whether to add asset on IPFS or if external resource + if search(file_extension_validator, self.args.media_url): + asset_file_ipfs_hash_base58 = ipfs_utils.publish_file_in_ipfs(self._get_ipfs_client(), self.args.media_url) + metadata.add_media(asset_file_ipfs_hash_base58, media_type, self.args.hero_image) + else: + metadata.add_media(self.args.media_url, media_type, self.args.hero_image) + metadata.save_pretty(self.args.metadata_file) + + def metadata_remove_media(self): + """Metadata: Remove individual media using unique order key.""" + metadata = load_mpe_service_metadata(self.args.metadata_file) + metadata.remove_media(self.args.order) + metadata.save_pretty(self.args.metadata_file) + + def metadata_remove_all_media(self): + """Metadata: Remove all individual media.""" + metadata = load_mpe_service_metadata(self.args.metadata_file) + metadata.remove_all_media() + metadata.save_pretty(self.args.metadata_file) + + def metadata_swap_media_order(self): + """Metadata: Swap order of two individual media given 'from' and 'to'.""" + metadata = load_mpe_service_metadata(self.args.metadata_file) + metadata.swap_media_order(self.args.move_from, self.args.move_to) + metadata.save_pretty(self.args.metadata_file) + + def metadata_change_media_order(self): + """Metadata: REPL to change order of all individual media.""" + metadata = load_mpe_service_metadata(self.args.metadata_file) + metadata.change_media_order() + metadata.save_pretty(self.args.metadata_file) + + def metadata_add_contributor(self): + metadata = load_mpe_service_metadata(self.args.metadata_file) + metadata.add_contributor(self.args.name, self.args.email_id) + metadata.save_pretty(self.args.metadata_file) + + def metadata_remove_contributor(self): + metadata = load_mpe_service_metadata(self.args.metadata_file) + metadata.remove_contributor_by_email(self.args.email_id) + metadata.save_pretty(self.args.metadata_file) + + def metadata_add_description(self): + """ Metadata: add description """ + service_description = {} + if self.args.json: + service_description = json.loads(self.args.json) + if self.args.url: + if "url" in service_description: + raise Exception( + "json service description already contains url field") + service_description["url"] = self.args.url + if self.args.description: + if "description" in service_description: + raise Exception( + "json service description already contains description field") + service_description["description"] = self.args.description + if self.args.short_description: + if "short_description" in service_description: + raise Exception( + "json service description already contains short description field") + if len(self.args.short_description) > 180: + raise Exception( + "size of short description must be less than 181 characters" + ) + service_description["short_description"] = self.args.short_description + metadata = load_mpe_service_metadata(self.args.metadata_file) + # merge with old service_description if necessary + if "service_description" in metadata: + service_description = { + **metadata["service_description"], **service_description} + metadata.set_simple_field("service_description", service_description) + metadata.save_pretty(self.args.metadata_file) + + def metadata_validate(self): + """Validates the service metadata file for structure and input consistency. + + Validates whether service metadata (`service_metadata.json` if not provided as argument) is consistent + with the schema provided in `service_schema` present in `snet_cli/snet/snet_cli/resources.` + + Args: + metadata_file: Option provided through the command line. (default: service_metadata.json) + service_schema: Schema of a consistent service metadata file. + + Raises: + ValidationError: Inconsistent service metadata structure or missing values. + docs -> Handling ValidationErrors (https://python-jsonschema.readthedocs.io/en/stable/errors/) + """ + # Set path to `service_schema` stored in the `resources` directory from cwd of `mpe_service.py` + current_path = Path(__file__).parent + relative_path = '../../snet/snet_cli/resources/service_schema' + path_to_schema = (current_path / relative_path).resolve() + with open(path_to_schema, 'r') as f: + schema = json.load(f) + metadata = load_mpe_service_metadata(self.args.metadata_file) + try: + validate(instance=metadata.m, schema=schema) + except Exception as e: + docs = "http://snet-cli-docs.singularitynet.io/service.html" + error_message = f"\nVisit {docs} for more information." + if e.validator == 'required': + raise ValidationError(e.message + error_message) + elif e.validator == 'minLength': + raise ValidationError(f"`{e.path[-1]}` -> cannot be empty." + error_message) + elif e.validator == 'minItems': + raise ValidationError(f"`{e.path[-1]}` -> minimum 1 item required." + error_message) + elif e.validator == 'type': + raise ValidationError(f"`{e.path[-1]}` -> {e.message}" + error_message) + elif e.validator == 'enum': + raise ValidationError(f"`{e.path[-1]}` -> {e.message}" + error_message) + elif e.validator == 'additionalProperties': + if len(e.path) != 0: + raise ValidationError(f"{e.message} in `{e.path[-2]}`." + error_message) + else: + raise ValidationError(f"{e.message} in main object." + error_message) + else: + exit("OK. Ready to publish.") + + def _publish_metadata_in_ipfs(self, metadata_file): + metadata = load_mpe_service_metadata(metadata_file) + mpe_address = self.get_mpe_address() + if self.args.update_mpe_address: + metadata.set_simple_field("mpe_address", mpe_address) + metadata.save_pretty(self.args.metadata_file) + + if mpe_address.lower() != metadata["mpe_address"].lower(): + raise Exception( + "\n\nmpe_address in metadata does not correspond to the current MultiPartyEscrow contract address\n" + + "You have two possibilities:\n" + + "1. You can use --multipartyescrow-at to set current mpe address\n" + + "2. You can use --update-mpe-address parameter to update mpe_address in metadata before publishing it\n") + return self._get_ipfs_client().add_bytes(metadata.get_json().encode("utf-8")) + + def publish_metadata_in_ipfs(self): + """ Publish metadata in ipfs and print hash """ + self._printout(self._publish_metadata_in_ipfs(self.args.metadata_file)) + + #def _get_converted_tags(self): + # return [type_converter("bytes32")(tag) for tag in self.args.tags] + + def _get_organization_metadata_from_registry(self, org_id): + rez = self._get_organization_registration(org_id) + metadata_hash = ipfs_utils.bytesuri_to_hash(rez["orgMetadataURI"]) + metadata = ipfs_utils.get_from_ipfs_and_checkhash( + self._get_ipfs_client(), metadata_hash) + metadata = metadata.decode("utf-8") + return OrganizationMetadata.from_json(json.loads(metadata)) + + def _get_organization_registration(self, org_id): + params = [type_converter("bytes32")(org_id)] + result = self.call_contract_command( + "Registry", "getOrganizationById", params) + if result[0] == False: + raise Exception("Cannot find Organization with id=%s" % ( + self.args.org_id)) + return {"orgMetadataURI": result[2]} + + def _validate_service_group_with_org_group_and_update_group_id(self, org_id, metadata_file): + org_metadata = self._get_organization_metadata_from_registry(org_id) + new_service_metadata = load_mpe_service_metadata(metadata_file) + org_groups = {} + for group in org_metadata.groups: + org_groups[group.group_name] = group + + for group in new_service_metadata.m["groups"]: + if group["group_name"] in org_groups: + group["group_id"] = org_groups[group["group_name"]].group_id + new_service_metadata.save_pretty(metadata_file) + else: + raise Exception( + "Group name %s does not exist in organization" % group["group_name"]) + + def publish_service_with_metadata(self): + self._validate_service_group_with_org_group_and_update_group_id( + self.args.org_id, self.args.metadata_file) + metadata_uri = ipfs_utils.hash_to_bytesuri( + self._publish_metadata_in_ipfs(self.args.metadata_file)) + #tags = self._get_converted_tags() + params = [type_converter("bytes32")(self.args.org_id), type_converter( + "bytes32")(self.args.service_id), metadata_uri] + self.transact_contract_command( + "Registry", "createServiceRegistration", params) + + def publish_metadata_in_ipfs_and_update_registration(self): + # first we check that we do not change payment_address or group_id in existed payment groups + self._validate_service_group_with_org_group_and_update_group_id( + self.args.org_id, self.args.metadata_file) + metadata_uri = ipfs_utils.hash_to_bytesuri( + self._publish_metadata_in_ipfs(self.args.metadata_file)) + params = [type_converter("bytes32")(self.args.org_id), type_converter( + "bytes32")(self.args.service_id), metadata_uri] + self.transact_contract_command( + "Registry", "updateServiceRegistration", params) + + #def _get_params_for_tags_update(self): + # tags = self._get_converted_tags() + # params = [type_converter("bytes32")(self.args.org_id), type_converter( + # "bytes32")(self.args.service_id), tags] + # return params + + def metadata_add_tags(self): + metadata = load_mpe_service_metadata(self.args.metadata_file) + [metadata.add_tag(tag) for tag in self.args.tags] + metadata.save_pretty(self.args.metadata_file) + + def metadata_remove_tags(self): + metadata = load_mpe_service_metadata(self.args.metadata_file) + [metadata.remove_tag(tag) for tag in self.args.tags] + metadata.save_pretty(self.args.metadata_file) + + def update_registration_add_tags(self): + self._printout("This command has been deprecated. Please use `snet service metadata-add-tags` instead") + + def update_registration_remove_tags(self): + self._printout("This command has been deprecated. Please use `snet service metadata-remove-tags` instead") + + def _get_service_registration(self): + params = [type_converter("bytes32")(self.args.org_id), type_converter( + "bytes32")(self.args.service_id)] + rez = self.call_contract_command( + "Registry", "getServiceRegistrationById", params) + if rez[0] == False: + raise Exception("Cannot find Service with id=%s in Organization with id=%s" % ( + self.args.service_id, self.args.org_id)) + return {"metadataURI": rez[2]} + + def _get_service_metadata_from_registry(self): + rez = self._get_service_registration() + metadata_hash = ipfs_utils.bytesuri_to_hash(rez["metadataURI"]) + metadata = ipfs_utils.get_from_ipfs_and_checkhash( + self._get_ipfs_client(), metadata_hash) + metadata = metadata.decode("utf-8") + metadata = mpe_service_metadata_from_json(metadata) + return metadata + + def print_service_metadata_from_registry(self): + metadata = self._get_service_metadata_from_registry() + self._printout(metadata.get_json_pretty()) + + def _service_status(self, url, secure=True): + try: + channel = open_grpc_channel(endpoint=url) + stub = heartb_pb2_grpc.HealthStub(channel) + response = stub.Check( + heartb_pb2.HealthCheckRequest(service=""), timeout=10) + if response != None and response.status == 1: + return True + return False + except Exception as e: + return False + + def print_service_status(self): + metadata = self._get_service_metadata_from_registry() + if self.args.group_name != None: + groups = {self.args.group_name: metadata.get_all_endpoints_for_group( + self.args.group_name)} + else: + groups = metadata.get_all_group_endpoints() + service_status = defaultdict(list) + for name, group_endpoints in groups.items(): + for endpoint in group_endpoints: + status = "Available" if self._service_status( + url=endpoint) else "Not Available" + service_status[name].append( + {"endpoint": endpoint, "status": status}) + if service_status == {}: + self._printout( + "Error: No endpoints found to check service status.") + return + self._pprint(service_status) + + def print_service_tags_from_registry(self): + metadata = self._get_service_metadata_from_registry() + self._printout(" ".join(metadata.get_tags())) + + def extract_service_api_from_metadata(self): + metadata = load_mpe_service_metadata(self.args.metadata_file) + ipfs_utils.safe_extract_proto_from_ipfs(self._get_ipfs_client( + ), metadata["model_ipfs_hash"], self.args.protodir) + + def extract_service_api_from_registry(self): + metadata = self._get_service_metadata_from_registry() + ipfs_utils.safe_extract_proto_from_ipfs(self._get_ipfs_client( + ), metadata["model_ipfs_hash"], self.args.protodir) + + def delete_service_registration(self): + params = [type_converter("bytes32")(self.args.org_id), type_converter( + "bytes32")(self.args.service_id)] + self.transact_contract_command( + "Registry", "deleteServiceRegistration", params) diff --git a/snet/sdk/commands/sdk_command.py b/snet/sdk/commands/sdk_command.py new file mode 100644 index 0000000..903f474 --- /dev/null +++ b/snet/sdk/commands/sdk_command.py @@ -0,0 +1,39 @@ +import os +from pathlib import Path, PurePath + +from snet.sdk.utils.ipfs_utils import safe_extract_proto_from_ipfs +from snet.sdk.utils.utils import compile_proto +from snet.sdk.commands.mpe_service import MPEServiceCommand + + +class SDKCommand(MPEServiceCommand): + def generate_client_library(self): + + if os.path.isabs(self.args.protodir): + client_libraries_base_dir_path = PurePath(self.args.protodir) + else: + cur_dir_path = PurePath(os.getcwd()) + client_libraries_base_dir_path = cur_dir_path.joinpath(self.args.protodir) + + os.makedirs(client_libraries_base_dir_path, exist_ok=True) + + # Create service client libraries path + library_language = self.args.language + library_org_id = self.args.org_id + library_service_id = self.args.service_id + + library_dir_path = client_libraries_base_dir_path.joinpath(library_org_id, library_service_id, library_language) + + metadata = self._get_service_metadata_from_registry() + model_ipfs_hash = metadata["model_ipfs_hash"] + + # Receive proto files + safe_extract_proto_from_ipfs(self._get_ipfs_client(), model_ipfs_hash, library_dir_path) + + # Compile proto files + compile_proto(Path(library_dir_path), library_dir_path, target_language=self.args.language) + + self._printout( + 'client libraries for service with id "{}" in org with id "{}" generated at {}'.format(library_service_id, + library_org_id, + library_dir_path)) diff --git a/snet/sdk/concurrency_manager.py b/snet/sdk/concurrency_manager.py index 8348396..6c2a29a 100644 --- a/snet/sdk/concurrency_manager.py +++ b/snet/sdk/concurrency_manager.py @@ -3,7 +3,7 @@ import grpc import web3 -from snet.cli.utils.utils import RESOURCES_PATH, add_to_path +from snet.sdk.utils.utils import RESOURCES_PATH, add_to_path class ConcurrencyManager: diff --git a/snet/sdk/config.py b/snet/sdk/config.py new file mode 100644 index 0000000..4636d74 --- /dev/null +++ b/snet/sdk/config.py @@ -0,0 +1,283 @@ +from configparser import ConfigParser, ExtendedInterpolation +from pathlib import Path +import sys + +default_snet_folder = Path("~").expanduser().joinpath(".snet") +DEFAULT_NETWORK = "sepolia" + + +class Config(ConfigParser): + def __init__(self, _snet_folder=default_snet_folder, sdk_config=None): + super(Config, self).__init__(interpolation=ExtendedInterpolation(), delimiters=("=",)) + self._config_file = _snet_folder.joinpath("config") + self.sdk_config = sdk_config + self.is_sdk = True if sdk_config else False + if self._config_file.exists(): + with open(self._config_file) as f: + self.read_file(f) + else: + self.create_default_config() + + def get_session_network_name(self): + session_network = self["session"]["network"] + self._check_section("network.%s" % session_network) + return session_network + + def safe_get_session_identity_network_names(self): + if "identity" not in self["session"]: + first_identity_message_and_exit(is_sdk=self.is_sdk) + + session_identity = self["session"]["identity"] + self._check_section("identity.%s" % session_identity) + + session_network = self.get_session_network_name() + + network = self._get_identity_section(session_identity).get("network") + if network and network != session_network: + raise Exception("Your session identity '%s' is bind to network '%s', which is different from your" + " session network '%s', please switch identity or network" % ( + session_identity, network, session_network)) + return session_identity, session_network + + def set_session_network(self, network, out_f): + self._set_session_network(network, out_f) + if "identity" in self["session"]: + session_identity = self["session"]["identity"] + identity_network = self._get_identity_section(session_identity).get("network") + if identity_network and identity_network != network: + print("Your new session network '%s' is incompatible with your current session identity '%s' " + "(which is bind to network '%s'), please switch your identity" % ( + network, session_identity, identity_network), file=out_f) + + def _set_session_network(self, network, out_f): + if network not in self.get_all_networks_names(): + raise Exception("Network %s is not in config" % network) + print("Switch to network: %s" % network, file=out_f) + self["session"]["network"] = network + self._persist() + + def set_session_identity(self, identity, out_f): + if identity not in self.get_all_identities_names(): + raise Exception('Identity "%s" is not in config' % identity) + network = self._get_identity_section(identity).get("network") + if network: + print('Identity "%s" is bind to network "%s"' % (identity, network), file=out_f) + self._set_session_network(network, out_f) + else: + print( + 'Identity "%s" is not bind to any network. You should switch network manually if you need.' % identity, + file=out_f) + print("Switch to identity: %s" % (identity), file=out_f) + self["session"]["identity"] = identity + self._persist() + + # session is the union of session.identity + session.network + default_ipfs_endpoint + # if value is presented in both session.identity and session.network we get it from session.identity (can happen only for default_eth_rpc_endpoint) + def get_session_field(self, key, exception_if_not_found=True): + session_identity, session_network = self.safe_get_session_identity_network_names() + + rez_identity = self._get_identity_section(session_identity).get(key) + rez_network = self._get_network_section(session_network).get(key) + + rez_ipfs = None + if key == "default_ipfs_endpoint": + rez_ipfs = self.get_ipfs_endpoint() + + rez = rez_identity or rez_network or rez_ipfs + if not rez and exception_if_not_found: + raise Exception("Cannot find %s in the session.identity and in the session.network" % key) + return rez + + def set_session_field(self, key, value, out_f): + if key == "default_ipfs_endpoint": + self.set_ipfs_endpoint(value) + print("set default_ipfs_endpoint=%s" % value, file=out_f) + elif key in get_session_network_keys(): + session_network = self.get_session_network_name() + self.set_network_field(session_network, key, value) + print("set {}={} for network={}".format(key, value, session_network), file=out_f) + elif key in get_session_identity_keys(): + session_identity, _ = self.safe_get_session_identity_network_names() + self.set_identity_field(session_identity, key, value) + print("set {}={} for identity={}".format(key, value, session_identity), file=out_f) + else: + all_keys = get_session_network_keys() + get_session_identity_keys() + ["default_ipfs_endpoint"] + raise Exception("key {} not in {}".format(key, all_keys)) + + def unset_session_field(self, key, out_f): + if key in get_session_network_keys_removable(): + print("unset %s from network %s" % (key, self["session"]["network"]), file=out_f) + del self._get_network_section(self["session"]["network"])[key] + self._persist() + + def session_to_dict(self): + session_identity, session_network = self.safe_get_session_identity_network_names() + + show = {"session", "network.%s" % session_network, "identity.%s" % session_identity, "ipfs"} + response = {f: dict(self[f]) for f in show} + return response + + def add_network(self, network, rpc_endpoint, default_gas_price): + network_section = "network.%s" % network + if network_section in self: + raise Exception("Network section %s already exists in config" % network) + + self[network_section] = {} + self[network_section]["default_eth_rpc_endpoint"] = str(rpc_endpoint) + # TODO: find solution with default gas price + self[network_section]["default_gas_price"] = str(default_gas_price) + self._persist() + + def set_network_field(self, network, key, value): + self._get_network_section(network)[key] = str(value) + self._persist() + + def add_identity(self, identity_name, identity, out_f=sys.stdout): + identity_section = "identity.%s" % identity_name + if identity_section in self: + raise Exception("Identity section %s already exists in config" % identity_section) + if "network" in identity and identity["network"] not in self.get_all_networks_names(): + raise Exception("Network %s is not in config" % identity["network"]) + self[identity_section] = identity + self._persist() + # switch to it, if it was the first identity + if len(self.get_all_identities_names()) == 1: + print("You've just added your first identity %s. We will automatically switch to it!" % identity_name) + self.set_session_identity(identity_name, out_f) + + def set_identity_field(self, identity, key, value): + self._get_identity_section(identity)[key] = str(value) + self._persist() + + def _get_network_section(self, network): + """ return section for network or identity """ + return self["network.%s" % network] + + def _get_identity_section(self, identity): + """ return section for the specific identity """ + return self["identity.%s" % identity] + + def get_ipfs_endpoint(self): + return self["ipfs"]["default_ipfs_endpoint"] + + def set_ipfs_endpoint(self, ipfs_endpoint): + self["ipfs"]["default_ipfs_endpoint"] = ipfs_endpoint + self._persist() + + def get_all_identities_names(self): + return [x[len("identity."):] for x in self.sections() if x.startswith("identity.")] + + def get_all_networks_names(self): + return [x[len("network."):] for x in self.sections() if x.startswith("network.")] + + def delete_identity(self, identity_name): + if identity_name not in self.get_all_identities_names(): + raise Exception("identity_name {} does not exist".format(identity_name)) + + session_identity, _ = self.safe_get_session_identity_network_names() + if identity_name == session_identity: + raise Exception("identity_name {} is in use".format(identity_name)) + self.remove_section("identity.{}".format(identity_name)) + self._persist() + + def create_default_config(self): + """ Create default configuration if config file does not exist """ + # make config directory with the minimal possible permission + self._config_file.parent.mkdir(mode=0o700, exist_ok=True) + self["network.mainnet"] = { + "default_eth_rpc_endpoint": "https://mainnet.infura.io/v3/09027f4a13e841d48dbfefc67e7685d5" + } + self["network.goerli"] = { + "default_eth_rpc_endpoint": "https://goerli.infura.io/v3/09027f4a13e841d48dbfefc67e7685d5", + } + self["network.sepolia"] = { + "default_eth_rpc_endpoint": "https://sepolia.infura.io/v3/09027f4a13e841d48dbfefc67e7685d5", + } + self["ipfs"] = {"default_ipfs_endpoint": "/dns/ipfs.singularitynet.io/tcp/80/"} + network = self.get_param_from_sdk_config("network") + if network: + if network not in self.get_all_networks_names(): + raise Exception("Network '%s' is not in config" % network) + self["session"] = {"network": network} + else: + self["session"] = {"network": DEFAULT_NETWORK} + identity_name = self.get_param_from_sdk_config("identity_name") + identity_type = self.get_param_from_sdk_config("identity_type") + if identity_name and identity_type: + identity = self.setup_identity() + self.add_identity(identity_name, identity) + self._persist() + print("We've created configuration file with default values in: %s\n" % str(self._config_file)) + + def _check_section(self, s): + if s not in self: + raise Exception("Config error, section %s is absent" % s) + + def _persist(self): + with open(self._config_file, "w") as f: + self.write(f) + self._config_file.chmod(0o600) + + def get_param_from_sdk_config(self, param: str, alternative=None): + if self.sdk_config: + return self.sdk_config.get(param, alternative) + return None + + def setup_identity(self): + identity_type = self.get_param_from_sdk_config("identity_type") + private_key = self.get_param_from_sdk_config("private_key") + default_wallet_index = self.get_param_from_sdk_config("wallet_index", 0) + if not identity_type: + raise Exception("identity_type not passed") + if identity_type == "key": + identity = { + "identity_type": "key", + "private_key": private_key, + "default_wallet_index": default_wallet_index + } + # TODO: logic for other identity_type + else: + print("\nThe identity_type parameter value you passed is not supported " + "by the sdk at this time.\n") + print("The available identity types are:\n" + " - 'key' (uses a required hex-encoded private key for signing)\n\n") + exit(1) + return identity + + +def first_identity_message_and_exit(is_sdk=False): + if is_sdk: + print("\nPlease create your first identity by passing the 'identity_name' " + "and 'identity_type' parameters in SDK config.\n") + print("The available identity types are:\n" + " - 'key' (uses a required hex-encoded private key for signing)\n\n") + else: + print("\nPlease create your first identity by running 'snet identity create'.\n\n") + print("The available identity types are:\n" + " - 'rpc' (yields to a required ethereum json-rpc endpoint for signing using a given wallet\n" + " index)\n" + " - 'mnemonic' (uses a required bip39 mnemonic for HDWallet/account derivation and signing\n" + " using a given wallet index)\n" + " - 'key' (uses a required hex-encoded private key for signing)\n" + " - 'ledger' (yields to a required ledger nano s device for signing using a given wallet\n" + " index)\n" + " - 'trezor' (yields to a required trezor device for signing using a given wallet index)\n" + "\n") + exit(1) + + +def get_session_identity_keys(): + return ["default_wallet_index"] + + +def get_session_network_keys(): + return ["default_gas_price", "current_registry_at", "current_multipartyescrow_at", "current_singularitynettoken_at", + "default_eth_rpc_endpoint"] + + +def get_session_network_keys_removable(): + return ["default_gas_price", "current_registry_at", "current_multipartyescrow_at", "current_singularitynettoken_at"] + + +def get_session_keys(): + return get_session_network_keys() + get_session_identity_keys() + ["default_ipfs_endpoint"] diff --git a/snet/sdk/contract.py b/snet/sdk/contract.py new file mode 100644 index 0000000..6c79d1f --- /dev/null +++ b/snet/sdk/contract.py @@ -0,0 +1,27 @@ +class Contract: + def __init__(self, w3, address, abi): + self.w3 = w3 + self.contract = self.w3.eth.contract(address=self.w3.to_checksum_address(address), abi=abi) + self.abi = abi + + def call(self, function_name, *positional_inputs, **named_inputs): + return getattr(self.contract.functions, function_name)(*positional_inputs, **named_inputs).call() + + def build_transaction(self, function_name, from_address, gas_price, *positional_inputs, **named_inputs): + nonce = self.w3.eth.get_transaction_count(from_address) + chain_id = self.w3.net.version + return getattr(self.contract.functions, function_name)(*positional_inputs, **named_inputs).build_transaction({ + "from": from_address, + "nonce": nonce, + "gasPrice": gas_price, + "chainId": int(chain_id) + }) + + def process_receipt(self, receipt): + events = [] + + contract_events = map(lambda e: e["name"], filter(lambda e: e["type"] == "event", self.abi)) + for contract_event in contract_events: + events.extend(getattr(self.contract.events, contract_event)().process_receipt(receipt)) + + return events diff --git a/snet/sdk/identity.py b/snet/sdk/identity.py new file mode 100644 index 0000000..18b4927 --- /dev/null +++ b/snet/sdk/identity.py @@ -0,0 +1,341 @@ +import abc +import json +import struct +import time +import getpass + +import rlp +from eth_account import Account +from eth_account.messages import defunct_hash_message +from eth_account._utils.legacy_transactions import encode_transaction, \ + UnsignedTransaction, serializable_unsigned_transaction_from_dict +from ledgerblue.comm import getDongle +from ledgerblue.commException import CommException +from trezorlib.client import TrezorClient +from trezorlib import messages as proto +from trezorlib.transport.hid import HidTransport + + +from snet.sdk.utils.utils import get_address_from_private, normalize_private_key + +BIP32_HARDEN = 0x80000000 + + +class IdentityProvider(abc.ABC): + @abc.abstractmethod + def get_address(self): + raise NotImplementedError() + + @abc.abstractmethod + def transact(self, transaction, out_f): + raise NotImplementedError() + + @abc.abstractmethod + def sign_message_after_solidity_keccak(self, message): + raise NotImplementedError() + + +class KeyIdentityProvider(IdentityProvider): + def __init__(self, w3, private_key): + self.w3 = w3 + self.private_key = normalize_private_key(private_key) + self.address = get_address_from_private(self.private_key) + + def get_address(self): + return self.address + + def transact(self, transaction, out_f): + raw_transaction = sign_transaction_with_private_key( + self.w3, self.private_key, transaction) + return send_and_wait_for_transaction(raw_transaction, self.w3, out_f) + + def sign_message_after_solidity_keccak(self, message): + return sign_message_with_private_key(self.w3, self.private_key, message) + + +class KeyStoreIdentityProvider(IdentityProvider): + def __init__(self, w3, path_to_keystore): + self.w3 = w3 + try: + with open(path_to_keystore) as keyfile: + encrypted_key = keyfile.read() + self.address = self.w3.to_checksum_address( + json.loads(encrypted_key)["address"]) + self.path_to_keystore = path_to_keystore + self.private_key = None + except CommException: + raise RuntimeError( + "Error decrypting your keystore. Are you sure it is the correct path?") + + def get_address(self): + return self.address + + def transact(self, transaction, out_f): + + if self.private_key is None: + self.private_key = unlock_keystore_with_password( + self.w3, self.path_to_keystore) + + raw_transaction = sign_transaction_with_private_key( + self.w3, self.private_key, transaction) + return send_and_wait_for_transaction(raw_transaction, self.w3, out_f) + + def sign_message_after_solidity_keccak(self, message): + + if self.private_key is None: + self.private_key = unlock_keystore_with_password( + self.w3, self.path_to_keystore) + + return sign_message_with_private_key(self.w3, self.private_key, message) + + +class RpcIdentityProvider(IdentityProvider): + def __init__(self, w3, index): + self.w3 = w3 + self.address = self.w3.personal.listAccounts[index] + + def get_address(self): + return self.address + + def transact(self, transaction, out_f): + print("Submitting transaction...\n", file=out_f) + txn_hash = self.w3.eth.sendTransaction(transaction) + return send_and_wait_for_transaction_receipt(txn_hash, self.w3) + + def sign_message_after_solidity_keccak(self, message): + return self.w3.eth.sign(self.get_address(), message) + + +class MnemonicIdentityProvider(IdentityProvider): + def __init__(self, w3, mnemonic, index): + self.w3 = w3 + Account.enable_unaudited_hdwallet_features() + account = Account.from_mnemonic(mnemonic, account_path=f"m/44'/60'/0'/0/{index}") + self.private_key = account.key.hex() + self.address = account.address + + def get_address(self): + return self.address + + def transact(self, transaction, out_f): + raw_transaction = sign_transaction_with_private_key( + self.w3, self.private_key, transaction) + return send_and_wait_for_transaction(raw_transaction, self.w3, out_f) + + def sign_message_after_solidity_keccak(self, message): + return sign_message_with_private_key(self.w3, self.private_key, message) + + +class TrezorIdentityProvider(IdentityProvider): + def __init__(self, w3, index): + self.w3 = w3 + self.client = TrezorClient(HidTransport.enumerate()[0]) + self.index = index + self.address = self.w3.to_checksum_address( + "0x" + bytes(self.client.ethereum_get_address([44 + BIP32_HARDEN, + 60 + BIP32_HARDEN, + BIP32_HARDEN, 0, + index])).hex()) + + def get_address(self): + return self.address + + def transact(self, transaction, out_f): + print("Sending transaction to trezor for signature...\n", file=out_f) + signature = self.client.ethereum_sign_tx(n=[44 + BIP32_HARDEN, 60 + BIP32_HARDEN, + BIP32_HARDEN, 0, self.index], + nonce=transaction["nonce"], + gas_price=transaction["gasPrice"], + gas_limit=transaction["gas"], + to=bytearray.fromhex( + transaction["to"][2:]), + value=transaction["value"], + data=bytearray.fromhex(transaction["data"][2:])) + + transaction.pop("from") + unsigned_transaction = serializable_unsigned_transaction_from_dict( + transaction) + raw_transaction = encode_transaction(unsigned_transaction, + vrs=(signature[0], + int(signature[1].hex(), 16), + int(signature[2].hex(), 16))) + return send_and_wait_for_transaction(raw_transaction, self.w3, out_f) + + def sign_message_after_solidity_keccak(self, message): + n = self.client._convert_prime([44 + BIP32_HARDEN, + 60 + BIP32_HARDEN, + BIP32_HARDEN, + 0, + self.index]) + return self.client.call(proto.EthereumSignMessage(address_n=n, message=message)).signature + + +class LedgerIdentityProvider(IdentityProvider): + GET_ADDRESS_OP = b"\xe0\x02\x00\x00" + SIGN_TX_OP = b"\xe0\x04\x00\x00" + SIGN_TX_OP_CONT = b"\xe0\x04\x80\x00" + SIGN_MESSAGE_OP = b"\xe0\x08\x00\x00" + + def __init__(self, w3, index): + self.w3 = w3 + try: + self.dongle = getDongle(False) + except CommException: + raise RuntimeError( + "Received commException from Ledger. Are you sure your device is plugged in?") + self.dongle_path = parse_bip32_path("44'/60'/0'/0/{}".format(index)) + apdu = LedgerIdentityProvider.GET_ADDRESS_OP + apdu += bytearray([len(self.dongle_path) + 1, + int(len(self.dongle_path) / 4)]) + self.dongle_path + try: + result = self.dongle.exchange(apdu) + except CommException: + raise RuntimeError("Received commException from Ledger. Are you sure your device is unlocked and the " + "Ethereum app is running?") + + offset = 1 + result[0] + self.address = self.w3.to_checksum_address(bytes(result[offset + 1: offset + 1 + result[offset]]) + .decode("utf-8")) + + def get_address(self): + return self.address + + def transact(self, transaction, out_f): + tx = UnsignedTransaction( + nonce=transaction["nonce"], + gasPrice=transaction["gasPrice"], + gas=transaction["gas"], + to=bytes(bytearray.fromhex(transaction["to"][2:])), + value=transaction["value"], + data=bytes(bytearray.fromhex(transaction["data"][2:])) + ) + + encoded_tx = rlp.encode(tx, UnsignedTransaction) + + overflow = len(self.dongle_path) + 1 + len(encoded_tx) - 255 + + if overflow > 0: + encoded_tx, remaining_tx = encoded_tx[:- + overflow], encoded_tx[-overflow:] + + apdu = LedgerIdentityProvider.SIGN_TX_OP + apdu += bytearray([len(self.dongle_path) + 1 + + len(encoded_tx), int(len(self.dongle_path) / 4)]) + apdu += self.dongle_path + encoded_tx + try: + print("Sending transaction to Ledger for signature...\n", file=out_f) + result = self.dongle.exchange(apdu) + while overflow > 0: + encoded_tx = remaining_tx + overflow = len(encoded_tx) - 255 + + if overflow > 0: + encoded_tx, remaining_tx = encoded_tx[:- + overflow], encoded_tx[-overflow:] + + apdu = LedgerIdentityProvider.SIGN_TX_OP_CONT + apdu += bytearray([len(encoded_tx)]) + apdu += encoded_tx + result = self.dongle.exchange(apdu) + except CommException as e: + if e.sw == 27013: + raise RuntimeError("Transaction denied from Ledger by user") + raise RuntimeError(e.message, e.sw) + + transaction.pop("from") + unsigned_transaction = serializable_unsigned_transaction_from_dict( + transaction) + raw_transaction = encode_transaction(unsigned_transaction, + vrs=(result[0], + int.from_bytes( + result[1:33], byteorder="big"), + int.from_bytes(result[33:65], byteorder="big"))) + return send_and_wait_for_transaction(raw_transaction, self.w3, out_f) + + def sign_message_after_solidity_keccak(self, message): + apdu = LedgerIdentityProvider.SIGN_MESSAGE_OP + apdu += bytearray([len(self.dongle_path) + 1 + + len(message) + 4, int(len(self.dongle_path) / 4)]) + apdu += self.dongle_path + struct.pack(">I", len(message)) + message + try: + result = self.dongle.exchange(apdu) + except CommException: + raise RuntimeError("Received commException from Ledger. Are you sure your device is unlocked and the " + "Ethereum app is running?") + + return result[1:] + result[0:1] + + +def send_and_wait_for_transaction_receipt(txn_hash, w3): + # Wait for transaction to be mined + receipt = dict() + while not receipt: + time.sleep(1) + try: + receipt = w3.eth.get_transaction_receipt(txn_hash) + if receipt and "blockHash" in receipt and receipt["blockHash"] is None: + receipt = dict() + except: + receipt = dict() + return receipt + + +def send_and_wait_for_transaction(raw_transaction, w3, out_f): + print("Submitting transaction...\n", file=out_f) + txn_hash = w3.eth.send_raw_transaction(raw_transaction) + return send_and_wait_for_transaction_receipt(txn_hash, w3) + + +def parse_bip32_path(path): + if len(path) == 0: + return b"" + result = b"" + elements = path.split('/') + for pathElement in elements: + element = pathElement.split('\'') + if len(element) == 1: + result = result + struct.pack(">I", int(element[0])) + else: + result = result + struct.pack(">I", BIP32_HARDEN | int(element[0])) + return result + + +def get_kws_for_identity_type(identity_type): + SECRET = True + PLAINTEXT = False + + if identity_type == "rpc": + return [("network", PLAINTEXT)] + elif identity_type == "mnemonic": + return [("mnemonic", SECRET)] + elif identity_type == "key": + return [("private_key", SECRET)] + elif identity_type == "trezor": + return [] + elif identity_type == "ledger": + return [] + elif identity_type == "keystore": + return [("keystore_path", PLAINTEXT)] + else: + raise RuntimeError( + "unrecognized identity_type {}".format(identity_type)) + + +def get_identity_types(): + return ["rpc", "mnemonic", "key", "trezor", "ledger", "keystore"] + + +def sign_transaction_with_private_key(w3, private_key, transaction): + return w3.eth.account.sign_transaction(transaction, private_key).rawTransaction + + +def sign_message_with_private_key(w3, private_key, message): + h = defunct_hash_message(message) + return w3.eth.account.signHash(h, private_key).signature + + +def unlock_keystore_with_password(w3, path_to_keystore): + password = getpass.getpass("Password : ") or "" + with open(path_to_keystore) as keyfile: + encrypted_key = keyfile.read() + return w3.eth.account.decrypt(encrypted_key, password) diff --git a/snet/sdk/metadata/__init__.py b/snet/sdk/metadata/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/snet/sdk/metadata/organization.py b/snet/sdk/metadata/organization.py new file mode 100644 index 0000000..84f9e50 --- /dev/null +++ b/snet/sdk/metadata/organization.py @@ -0,0 +1,355 @@ +import base64 +from enum import Enum +from json import JSONEncoder +import json + +from snet.sdk.utils.utils import is_valid_url + + +class DefaultEncoder(JSONEncoder): + def default(self, o): + return o.__dict__ + + +class AssetType(Enum): + HERO_IMAGE = "hero_image" + + +class PaymentStorageClient(object): + + def __init__(self, connection_timeout=None, request_timeout="", endpoints=None): + if endpoints is None: + endpoints = [] + self.connection_timeout = connection_timeout + self.request_timeout = request_timeout + self.endpoints = endpoints + + def add_payment_storage_client_details(self, connection_time_out, request_timeout, endpoints): + self.connection_timeout = connection_time_out + self.request_timeout = request_timeout + self.endpoints = endpoints + + @classmethod + def from_json(cls, json_data: dict): + endpoints = json_data["endpoints"] + if endpoints: + for endpoint in endpoints: + if not is_valid_url(endpoint): + raise Exception("Invalid endpoint passed in json file") + return cls(**json_data) + + def validate(self): + if len(self.endpoints) < 1: + raise Exception( + "At least one endpoint is required for payment channel ") + + +class Payment(object): + + def __init__(self, payment_address="", payment_expiration_threshold="", payment_channel_storage_type="", + payment_channel_storage_client=PaymentStorageClient()): + self.payment_address = payment_address + self.payment_expiration_threshold = payment_expiration_threshold + self.payment_channel_storage_type = payment_channel_storage_type + self.payment_channel_storage_client = payment_channel_storage_client + + @classmethod + def from_json(cls, json_data: dict): + payment_channel_storage_client = PaymentStorageClient.from_json( + json_data['payment_channel_storage_client']) + return cls(json_data['payment_address'], json_data['payment_expiration_threshold'], + json_data['payment_channel_storage_type'], payment_channel_storage_client) + + def validate(self): + if self.payment_address is None: + raise Exception("Payment address cannot be null") + if self.payment_channel_storage_type is None: + raise Exception("Payment channel storage type cannot be null") + if self.payment_expiration_threshold is None: + raise Exception("Payment expiration threshold cannot be null") + + if self.payment_channel_storage_client is None: + raise Exception("Payment channel storage client cannot be null") + else: + self.payment_channel_storage_client.validate() + + def update_connection_timeout(self, connection_timeout): + self.payment_channel_storage_client.connection_timeout = connection_timeout + + def update_request_timeout(self, request_timeout): + self.payment_channel_storage_client.request_timeout = request_timeout + + def update_endpoints(self, endpoints): + self.payment_channel_storage_client.endpoints = endpoints + + +class Group(object): + + def __init__(self, group_name="", group_id="", payment=Payment()): + self.group_name = group_name + self.group_id = group_id + self.payment = payment + + @classmethod + def from_json(cls, json_data: dict): + payment = Payment() + if 'payment' in json_data: + payment = Payment.from_json(json_data['payment']) + return cls(json_data['group_name'], json_data['group_id'], payment) + + def add_group_details(self, group_name, group_id, payment): + self.group_name = group_name + self.group_id = group_id + self.payment = payment + + def validate(self): + if self.group_name is None: + raise Exception("group name cannot be null") + if self.group_id is None: + raise Exception("group_id is cannot be null") + + if self.payment is None: + raise Exception( + "payment details cannot be null for group_name %s", self.group_name) + else: + self.payment.validate() + + def update_payment_expiration_threshold(self, payment_expiration_threshold): + self.payment.payment_expiration_threshold = payment_expiration_threshold + + def update_payment_channel_storage_type(self, payment_channel_storage_type): + self.update_payment_channel_storage_type = payment_channel_storage_type + + def update_payment_address(self, payment_address): + self.payment.payment_address = payment_address + + def update_connection_timeout(self, connection_timeout): + self.payment.update_connection_timeout(connection_timeout) + + def update_request_timeout(self, request_timeout): + self.payment.update_request_timeout(request_timeout) + + def update_endpoints(self, endpoints): + self.payment.update_endpoints(endpoints) + + def get_group_id(self, group_name=None): + return base64.b64decode(self.get_group_id_base64(group_name)) + + def get_payment_address(self): + return self.payment.payment_address + + +class OrganizationMetadata(object): + """ + { + "org_name": "organization_name", + "org_id": "org_id1", + org_type: "organization"/"individual", + "contacts": [ + { + "contact_type": "support", + "email_id":"abcd@abcdef.com", + "phone":"1234567890", + }, + { + "contact_type": "dummy", + "email_id":"dummy@abcdef.com", + "phone":"1234567890", + }, + ], + "description": "We do this and that ... Describe your organization here ", + "assets": { + "hero_image": "QmNW2jjz11enwbRrF1mJ2LdaQPeZVEtmKU8Uq7kpEkmXCc/hero_gene-annotation.png" + }, + "groups": [ + { + "group_name": "default_group2", + "group_id": "99ybRIg2wAx55mqVsA6sB4S7WxPQHNKqa4BPu/bhj+U=", + "payment": { + "payment_address": "0x671276c61943A35D5F230d076bDFd91B0c47bF09", + "payment_expiration_threshold": 40320, + "payment_channel_storage_type": "etcd", + "payment_channel_storage_client": { + "connection_timeout": "5s", + "request_timeout": "3s", + "endpoints": [ + "http://127.0.0.1:2379" + ] + } + } + }, + { + "group_name": "default_group2", + "group_id": "99ybRIg2wAx55mqVsA6sB4S7WxPQHNKqa4BPu/bhj+U=", + "payment": { + "payment_address": "0x671276c61943A35D5F230d076bDFd91B0c47bF09", + "payment_expiration_threshold": 40320, + "payment_channel_storage_type": "etcd", + "payment_channel_storage_client": { + "connection_timeout": "5s", + "request_timeout": "3s", + "endpoints": [ + "http://127.0.0.1:2379" + ] + } + } + } + ] + } + """ + + def __init__(self, org_name="", org_id="", org_type="",contacts=[], description={}, + assets={}, groups=[]): + self.org_name = org_name + self.org_id = org_id + self.org_type = org_type + self.description = description + self.assets = assets + self.contacts = contacts + self.groups = groups + + def add_group(self, group): + self.groups.append(group) + + def get_json_pretty(self): + return json.dumps(self, indent=4, cls=DefaultEncoder) + + def save_pretty(self, file_name): + with open(file_name, 'w') as f: + f.write(self.get_json_pretty()) + + @classmethod + def from_json(cls, json_data: dict): + groups = [] + if 'groups' in json_data: + groups = list(map(Group.from_json, json_data["groups"])) + if "contacts" not in json_data: + json_data["contacts"] = [] + if "description" not in json_data: + json_data["description"] = {} + if "assets" not in json_data: + json_data["assets"] = {} + if "org_type" not in json_data: + json_data["org_type"] = "" + return cls( + org_name=json_data['org_name'], + org_id=json_data['org_id'], + org_type=json_data['org_type'], + contacts=json_data['contacts'], + description=json_data['description'], + groups=groups, + assets=json_data['assets'] + ) + + @classmethod + def from_file(cls, filepath): + try: + with open(filepath, 'r') as f: + return OrganizationMetadata.from_json(json.load(f)) + except Exception as e: + print( + "Organization metadata json file not found ,Please check --metadata-file path ") + raise e + + def is_removing_existing_group_from_org(self, current_group_name, existing_registry_metadata_group_names): + if len(existing_registry_metadata_group_names-current_group_name) == 0: + pass + else: + removed_groups = existing_registry_metadata_group_names - current_group_name + raise Exception("Cannot remove existing group from organization as it might be attached" + " to services, groups you are removing are %s" % removed_groups) + + def validate(self, existing_registry_metadata=None): + + if self.org_id is None: + raise Exception("Org_id cannot be null") + if self.org_name is None: + raise Exception("Org_name cannot be null") + if self.org_type is None: + raise Exception("Org_type cannot be null") + if self.contacts is None: + raise Exception("contact_details can not be null") + if self.description is None: + raise Exception("description can not be null") + if self.groups: + unique_group_names = set() + for group in self.groups: + unique_group_names.add(group.group_name) + + if len(unique_group_names) < len(self.groups): + raise Exception("Cannot create group with duplicate names") + if len(self.groups) < 1: + raise Exception( + "At least One group is required to create an organization") + else: + for group in self.groups: + group.validate() + + existing_registry_metadata_group_names = set() + if existing_registry_metadata: + for group in existing_registry_metadata.groups: + existing_registry_metadata_group_names.add(group.group_name) + + self.is_removing_existing_group_from_org( + unique_group_names, existing_registry_metadata_group_names) + + def get_payment_address_for_group(self, group_name): + for group in self.groups: + if group.group_name == group_name: + return group.get_payment_address() + + def get_group_id_by_group_name(self, group_name): + for group in self.groups: + if group.group_name == group_name: + return group.group_id + + def get_group_by_group_id(self, group_id): + for group in self.groups: + if group.group_id == group_id: + return group + + def add_asset(self, asset_ipfs_hash, asset_type): + if asset_type == AssetType.HERO_IMAGE.value: + self.assets[asset_type] = asset_ipfs_hash + else: + raise Exception("Invalid asset type %s" % asset_type) + + def remove_all_assets(self): + self.assets = {} + + def remove_assets(self, asset_type): + if asset_type == AssetType.HERO_IMAGE.value: + self.assets[asset_type] = "" + else: + raise Exception("Invalid asset type %s" % asset_type) + + def add_description(self, description): + self.description["description"] = description + + def add_short_description(self, short_description): + self.description["short_description"] = short_description + + def add_url(self, url): + self.description["url"] = url + + def remove_description(self): + self.description = {} + + def add_contact(self, contact_type, phone, email): + if phone is None: + phone = "" + if email is None: + email = "" + + contact = { + "contact_type": contact_type, + "email_id": email, + "phone": phone + } + self.contacts.append(contact) + + def remove_contact_by_type(self, contact_type): + self.contacts = [contact for contact in self.contacts if contact["contact_type"] != contact_type] + + def remove_all_contacts(self): + self.contacts = [] diff --git a/snet/sdk/metadata/service.py b/snet/sdk/metadata/service.py new file mode 100644 index 0000000..5c5f267 --- /dev/null +++ b/snet/sdk/metadata/service.py @@ -0,0 +1,543 @@ +""" +Functions for manipulating service metadata + +Metadata format: +---------------------------------------------------- +version - used to track format changes (current version is 1) +display_name - Display name of the service +encoding - Service encoding (proto or json) +service_type - Service type (grpc, jsonrpc or process) +service_description - Service description (arbitrary field) +payment_expiration_threshold - Service will reject payments with expiration less + than current_block + payment_expiration_threshold. + This field should be used by the client with caution. + Client should not accept arbitrary payment_expiration_threshold +model_ipfs_hash - IPFS HASH to the .tar archive of protobuf service specification +mpe_address - Address of MultiPartyEscrow contract. + Client should use it exclusively for cross-checking of mpe_address, + (because service can attack via mpe_address) + Daemon can use it directly if authenticity of metadata is confirmed +pricing {} - Pricing model + Possible pricing models: + 1. Fixed price + price_model - "fixed_price" + price_in_cogs - unique fixed price in cogs for all method (1 AGIX = 10^8 cogs) + (other pricing models can be easily supported) +groups [] - group is the number of endpoints which shares same payment channel; + grouping strategy is defined by service provider; + for example service provider can use region name as group name + group_name - unique name of the group (human readable) + group_id - unique id of the group (random 32 byte string in base64 encoding) + payment_address - Ethereum address to recieve payments +endpoints[] - address in the off-chain network to provide a service + group_name + endpoint - unique endpoint identifier (ip:port) + +assets {} - asset type and its ipfs value/values +""" + +import re +import json +import base64 + +from collections import defaultdict +from enum import Enum + +from snet.sdk.utils.utils import is_valid_endpoint + + +# Supported Asset types +class AssetType(Enum): + HERO_IMAGE = "hero_image" + IMAGES = "images" + DOCUMENTATION = "documentation" + TERMS_OF_USE = "terms_of_use" + + @staticmethod + def is_single_value(asset_type): + if asset_type == AssetType.HERO_IMAGE.value or asset_type == AssetType.DOCUMENTATION.value or asset_type == AssetType.TERMS_OF_USE.value: + return True + + +# TODO: we should use some standard solution here +class MPEServiceMetadata: + + def __init__(self): + """ init with modelIPFSHash """ + self.m = {"version": 1, + "display_name": "", + "encoding": "grpc", # grpc by default + "service_type": "grpc", # grpc by default + # one week by default (15 sec block, 24*60*60*7/15) + "model_ipfs_hash": "", + "mpe_address": "", + "groups": [], + "assets": {}, + "media": [], + "tags": [] + } + + def set_simple_field(self, f, v): + if f != "display_name" and f != "encoding" and f != "model_ipfs_hash" and f != "mpe_address" and \ + f != "service_type" and f != "payment_expiration_threshold" and f != "service_description": + raise Exception("unknown field in MPEServiceMetadata") + self.m[f] = v + + def set_fixed_price_in_cogs(self, group_name, price): + if type(price) != int: + raise Exception("Price should have int type") + + if not self.is_group_name_exists(group_name): + raise Exception("the group %s is not present" % str(group_name)) + + for group in self.m["groups"]: + if group["group_name"] == group_name: + is_fixed_price_enabled = False + # default=True it will change when we will go live with method level pricing + if "pricing" in group: + for pricing in group['pricing']: + if pricing["price_model"] == "fixed_price": + is_fixed_price_enabled = True + pricing["price_in_cogs"] = price + if not is_fixed_price_enabled: + group["pricing"].append({"price_model": "fixed_price", + "price_in_cogs": price, "default": True}) + else: + group["pricing"] = [{"price_model": "fixed_price", + "price_in_cogs": price, "default": True}] + + def set_method_price_in_cogs(self, group_name, package_name, service_name, method, price): + if type(price) != int: + raise Exception("Price should have int type") + + if not self.is_group_name_exists(group_name): + raise Exception("the group %s is not present" % str(group_name)) + + groups = self.m["groups"] + for group in groups: + if group["group_name"] == group_name: + + service_name = service_name + package_name = package_name + method_pricing = {"method_name": method, + "price_in_cogs": price} + pricings = [] + + if 'pricings' in group: + pricings = group["pricings"] + + fixed_price_method_model_exist = False + for pricing in pricings: + if pricing['price_model'] == 'fixed_price_per_method': + fixed_price_method_model_exist = True + + if 'details' in pricing: + fixed_price_method_pricing_for_service_exist = False + for detail in pricing['details']: + + if detail['service_name'] == service_name: + # adding new method pricing for existing service + fixed_price_method_pricing_for_service_exist = True + detail['method_pricing'].append( + method_pricing) + + if not fixed_price_method_pricing_for_service_exist: + # pricing for new method for new service + pricing['details'].append({"service_name": service_name, + "method_pricing": [method_pricing]}) + else: + pricing['details'] = [{"service_name": service_name, + "method_pricing": [method_pricing]}] + + if not fixed_price_method_model_exist: + fixed_price_per_method = {"package_name": package_name, + "price_model": "fixed_price_per_method", + "details": [ + {"service_name": service_name, "method_pricing": [method_pricing]}]} + group['pricings'] = [fixed_price_per_method] + + def add_group(self, group_name): + """ Return new group_id in base64 """ + if self.is_group_name_exists(group_name): + raise Exception("the group \"%s\" is already present" % + str(group_name)) + + self.m["groups"] += [{"group_name": group_name}] + + def remove_group(self, group_name): + for group in self.m["groups"]: + if group["group_name"] == group_name: + self.m["groups"].remove(group) + + def get_tags(self): + tags = [] + if "tags" in self.m: + tags = self.m["tags"] + return tags + + def add_tag(self, tag_name): + if not "tags" in self.m: + self.m["tags"] = [] + + if tag_name in self.m["tags"]: + print(f"The tag {str(tag_name)} is already present") + return + self.m["tags"] += [tag_name] + + def remove_tag(self, tag_name): + if not "tags" in self.m: + self.m["tags"] = [] + + if tag_name not in self.m["tags"]: + print(f"The tag {str(tag_name)} is not found") + return + self.m["tags"].remove(tag_name) + + def add_asset(self, asset_ipfs_hash, asset_type): + # Check if we need to validation if same asset type is added twice if we need to add it or replace the existing one + + if 'assets' not in self.m: + self.m['assets'] = {} + + # hero image will contain the single value + if AssetType.is_single_value(asset_type): + self.m['assets'][asset_type] = asset_ipfs_hash + + # images can contain multiple value + elif asset_type == AssetType.IMAGES.value: + if asset_type in self.m['assets']: + self.m['assets'][asset_type].append(asset_ipfs_hash) + else: + self.m['assets'][asset_type] = [asset_ipfs_hash] + else: + raise Exception("Invalid asset type %s" % asset_type) + + def remove_all_assets(self): + self.m['assets'] = {} + + def remove_assets(self, asset_type): + if 'assets' in self.m: + if AssetType.is_single_value(asset_type): + self.m['assets'][asset_type] = "" + elif asset_type == AssetType.IMAGES.value: + self.m['assets'][asset_type] = [] + else: + raise Exception("Invalid asset type %s" % asset_type) + + def add_endpoint_to_group(self, group_name, endpoint): + if re.match("^\w+://", endpoint) is None: + # TODO: Default to https when our tutorials show setting up a ssl certificate as well + endpoint = 'http://' + endpoint + if not is_valid_endpoint(endpoint): + raise Exception("Endpoint is not a valid URL") + if not self.is_group_name_exists(group_name): + raise Exception("the group %s is not present" % str(group_name)) + if endpoint in self.get_all_endpoints_for_group(group_name): + raise Exception("the endpoint %s is already present" % + str(endpoint)) + + groups = self.m["groups"] + for group in groups: + if group["group_name"] == group_name: + if 'endpoints' in group: + group['endpoints'].append(endpoint) + else: + group['endpoints'] = [endpoint] + + def remove_all_endpoints_for_group(self, group_name): + if not self.is_group_name_exists(group_name): + raise Exception("Group name does not exist %s", group_name) + + groups = self.m["groups"] + for group in groups: + if group["group_name"] == group_name: + group["endpoints"] = [] + + def is_group_name_exists(self, group_name): + """ check if group with given name is already exists """ + groups = self.m["groups"] + for g in groups: + if g["group_name"] == group_name: + return True + return False + + def get_group_by_group_id(self, group_id): + """ return group with given group_id (return None if doesn't exists) """ + group_id_base64 = base64.b64encode(group_id).decode('ascii') + groups = self.m["groups"] + for g in groups: + if g["group_id"] == group_id_base64: + return g + return None + + def set_free_calls_for_group(self, group_name, free_calls): + groups = self.m["groups"] + for g in groups: + if g["group_name"] == group_name: + g["free_calls"] = free_calls + + def set_freecall_signer_address(self, group_name, signer_address): + groups = self.m["groups"] + for g in groups: + if g["group_name"] == group_name: + g["free_call_signer_address"] = signer_address + + def get_json(self): + return json.dumps(self.m) + + def get_json_pretty(self): + return json.dumps(self.m, indent=4) + + def set_from_json(self, j): + # TODO: we probaly should check the consistensy of loaded json here + # check that it contains required fields + self.m = json.loads(j) + if not "tags" in self.m: + self.m["tags"] = [] + + def load(self, file_name): + with open(file_name) as f: + self.set_from_json(f.read()) + + def save_pretty(self, file_name): + with open(file_name, 'w') as f: + f.write(self.get_json_pretty()) + + def __getitem__(self, key): + return self.m[key] + + def __contains__(self, key): + return key in self.m + + def get_group_name_nonetrick(self, group_name=None): + """ In all getter function in case of single payment group, group_name can be None """ + groups = self.m["groups"] + if len(groups) == 0: + raise Exception("Cannot find any groups in metadata") + if not group_name: + if len(groups) > 1: + raise Exception( + "We have more than one payment group in metadata, so group_name should be specified") + return groups[0]["group_name"] + return group_name + + def get_group(self, group_name=None): + group_name = self.get_group_name_nonetrick(group_name) + for g in self.m["groups"]: + if g["group_name"] == group_name: + return g + raise Exception('Cannot find group "%s" in metadata' % group_name) + + def get_group_id_base64(self, group_name=None): + return self.get_group(group_name)["group_id"] + + def get_group_id(self, group_name=None): + return base64.b64decode(self.get_group_id_base64(group_name)) + + def get_payment_address(self, group_name=None): + return self.get_group(group_name)["payment_address"] + + def add_daemon_address_to_group(self, group_name, daemon_address): + groups = self.m["groups"] + if not self.is_group_name_exists(group_name): + raise Exception('Cannot find group "%s" in metadata' % group_name) + for group in groups: + if group["group_name"] == group_name: + if 'daemon_addresses' in group: + group['daemon_addresses'].append(daemon_address) + else: + group['daemon_addresses'] = [daemon_address] + + def remove_all_daemon_addresses_for_group(self, group_name): + groups = self.m["groups"] + if not self.is_group_name_exists(group_name): + raise Exception('Cannot find group "%s" in metadata' % group_name) + for group in groups: + if group["group_name"] == group_name: + group["daemon_addresses"] = [] + + def get_all_endpoints_for_group(self, group_name): + for group in self.m["groups"]: + if group["group_name"] == group_name: + if "endpoints" in group: + return group["endpoints"] + return [] + + def get_all_group_endpoints(self): + group_endpoints = {} + for group in self.m["groups"]: + if "endpoints" in group: + group_endpoints[group["group_name"]] = group['endpoints'] + return group_endpoints + + def get_all_endpoints_with_group_name(self): + endpts_with_grp = defaultdict(list) + for e in self.m["endpoints"]: + endpts_with_grp[e['group_name']].append(e['endpoint']) + return endpts_with_grp + + def get_endpoints_for_group(self, group_name=None): + group_name = self.get_group_name_nonetrick(group_name) + return [e["endpoint"] for e in self.m["endpoints"] if e["group_name"] == group_name] + + def add_contributor(self, name, email_id): + if "contributors" in self.m: + contributors = self.m["contributors"] + else: + contributors = [] + + contributors.append( + { + "name": name, + "email_id": email_id + } + ) + self.m["contributors"] = contributors + + def remove_contributor_by_email(self, email_id): + self.m["contributors"] = [ + contributor for contributor in self.m["contributors"] if contributor["email_id"] != email_id] + + def group_init(self, group_name): + """Required values for creating a new payment group. + + Args: + group_name: If org contains only 1 payment group -> default_group, ask user for other groups otherwise. + + Raises: + ValueError: User enters non-integer value for `fixed_price.` + Exception: User enters same endpoints. + """ + self.add_group(group_name) + while True: + try: + fixed_price = int(input("Set fixed price: ")) + except ValueError: + print("Enter a valid integer.") + else: + self.set_fixed_price_in_cogs(group_name, fixed_price) + break + while True: + try: + endpoints = input("Add endpoints as comma separated values: ").split(',') + if endpoints[0] == "": + print("Endpoints required.") + else: + for endpoint in endpoints: + self.add_endpoint_to_group(group_name, endpoint.strip()) + break + except Exception as e: + print(e) + while True: + daemon_addresses = input("Add daemon addresses as comma separated values: ").split(',') + if daemon_addresses[0] == "": + print("Daemon address required.") + else: + for daemon_address in daemon_addresses: + self.add_daemon_address_to_group(group_name, daemon_address.strip()) + break + if input('Free calls included? [y/n] ').lower() == 'y': + self.set_free_calls_for_group(group_name, int(input('free calls: (15) ') or 15)) + self.set_freecall_signer_address(group_name, input('free call signer address: ')) + + def add_media(self, url, media_type, hero_img=False): + """Add new individual media to service metadata.""" + if 'media' not in self.m: + self.m['media'] = [] + individual_media = {} + if hero_img: + assert (media_type == 'image'), f"{media_type.upper()} media-type cannot be a hero-image." + assert (not self._is_asset_type_exists()), "Hero-image already exists (only 1 unique hero-image allowed.)" + individual_media['asset_type'] = AssetType.HERO_IMAGE.value # Dependency with AssetType, fix if obsolete + if len(self.m['media']) == 0: + individual_media['order'] = 1 + else: + individual_media['order'] = self.m['media'][-1]['order'] + 1 + individual_media['url'] = url + individual_media['file_type'] = media_type + if media_type == 'image': + individual_media['alt_text'] = 'hover_on_the_image_text' + else: + individual_media['alt_text'] = 'hover_on_the_video_url' + self.m['media'].append(individual_media) + + def remove_media(self, order): + """Remove individual media from service metadata using unique order key.""" + assert (len(self.m['media']) > 0), "No media content to remove." + assert (order > 0), "Order of individual media starts from 1." + del_position = -1 + for i in range(len(self.m['media'])): + if order == self.m['media'][i]['order']: + del self.m['media'][i] + del_position = i + break + if del_position == -1: + raise Exception(f"Media with order: {order} not found.") + for i in range(del_position, len(self.m['media'])): + self.m['media'][i]['order'] -= 1 + + def remove_all_media(self): + """Remove all individual media from metadata.""" + self.m['media'].clear() + + def swap_media_order(self, move_from, move_to): + """Swap orders of two different media given their individual orders (move_from, move_to).""" + assert (len(self.m['media']) + 1 > move_from > 0), f"Order {move_from} out of bounds." + assert (len(self.m['media']) + 1 > move_to > 0), f"Order {move_to} out of bounds." + self.m['media'][move_to - 1], self.m['media'][move_from - 1] = self.m['media'][move_from - 1], self.m['media'][ + move_to - 1] + self.m['media'][move_to - 1]['order'], self.m['media'][move_from - 1]['order'] = self.m['media'][move_from - 1][ + 'order'], \ + self.m['media'][move_to - 1][ + 'order'] + + def change_media_order(self): + """Mini REPL to change order of all individual media""" + order_range = range(1, len(self.m['media']) + 1) + available_orders = list(order_range) + for individual_media in self.m['media']: + print(f"File Type: {individual_media['file_type']}, Current Order: {individual_media['order']}") + while True: + try: + new_order = int(input(f"Enter new order for {individual_media['url']}: ")) + except ValueError: + print("Error: Order entered not a number. Try again.") + else: + if new_order in available_orders: + individual_media['order'] = new_order + available_orders.remove(new_order) + break + elif new_order not in order_range: + print( + f"Media array contains only {len(self.m['media'])} items. Enter order between [{order_range.start}, {order_range.stop - 1}]") + else: + print(f"Order already taken. Available orders: {available_orders}") + self.m['media'].sort(key=lambda x: x['order']) + + def _is_asset_type_exists(self): + """Return boolean on whether asset type already exists""" + media = self.m['media'] + for individual_media in media: + if 'asset_type' in individual_media: + return True + return False + + def add_description(self): + if 'service_description' not in self.m: + self.m['service_description'] = { + "url": input("user guide url: "), + "long_description": input("service long description: "), + "short_description": input("service short description: ") + } + + +def load_mpe_service_metadata(f): + metadata = MPEServiceMetadata() + metadata.load(f) + return metadata + + +def mpe_service_metadata_from_json(j): + metadata = MPEServiceMetadata() + metadata.set_from_json(j) + return metadata diff --git a/snet/sdk/metadata_provider/ipfs_metadata_provider.py b/snet/sdk/metadata_provider/ipfs_metadata_provider.py index 4f1446f..f461fb5 100644 --- a/snet/sdk/metadata_provider/ipfs_metadata_provider.py +++ b/snet/sdk/metadata_provider/ipfs_metadata_provider.py @@ -1,8 +1,8 @@ import json import web3 -from snet.cli.metadata.service import mpe_service_metadata_from_json -from snet.cli.utils.ipfs_utils import bytesuri_to_hash, get_from_ipfs_and_checkhash +from snet.sdk.metadata.service import mpe_service_metadata_from_json +from snet.sdk.utils.ipfs_utils import bytesuri_to_hash, get_from_ipfs_and_checkhash class IPFSMetadataProvider(object): diff --git a/snet/sdk/mpe/payment_channel.py b/snet/sdk/mpe/payment_channel.py index c13f6f3..ead629e 100644 --- a/snet/sdk/mpe/payment_channel.py +++ b/snet/sdk/mpe/payment_channel.py @@ -2,8 +2,7 @@ import importlib from eth_account.messages import defunct_hash_message -from snet.cli.utils.utils import RESOURCES_PATH, add_to_path - +from snet.sdk.utils.utils import RESOURCES_PATH, add_to_path class PaymentChannel: def __init__(self, channel_id, w3, account, payment_channel_state_service_client, mpe_contract): diff --git a/snet/sdk/payment_strategies/freecall_payment_strategy.py b/snet/sdk/payment_strategies/freecall_payment_strategy.py index 4649fea..7373019 100644 --- a/snet/sdk/payment_strategies/freecall_payment_strategy.py +++ b/snet/sdk/payment_strategies/freecall_payment_strategy.py @@ -3,8 +3,8 @@ import grpc import web3 -from snet.cli.resources.root_certificate import certificate -from snet.cli.utils.utils import RESOURCES_PATH, add_to_path +from snet.sdk.resources.root_certificate import certificate +from snet.sdk.utils.utils import RESOURCES_PATH, add_to_path from snet.sdk.payment_strategies.payment_staregy import PaymentStrategy diff --git a/snet/sdk/resources/package.json b/snet/sdk/resources/package.json new file mode 100644 index 0000000..15db610 --- /dev/null +++ b/snet/sdk/resources/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "grpc-tools": "1.9.0" + } +} diff --git a/snet/sdk/resources/proto/control_service.proto b/snet/sdk/resources/proto/control_service.proto new file mode 100644 index 0000000..efd301a --- /dev/null +++ b/snet/sdk/resources/proto/control_service.proto @@ -0,0 +1,55 @@ +syntax = "proto3"; + +package escrow; + +service ProviderControlService { + + //get list of all unclaimed "payments". + //in PaymentsSignatureReply signatures MUST be omited + //if signature field is present then response should be considered as invalid + rpc GetListUnclaimed(GetPaymentsListRequest) returns (PaymentsListReply) {} + + //get list of all payments in progress + rpc GetListInProgress(GetPaymentsListRequest) returns (PaymentsListReply) {} + + //initilize claim for specific channel + rpc StartClaim(StartClaimRequest) returns (PaymentReply) {} +} + + +message GetPaymentsListRequest { + //address of MultiPartyEscrow contract + string mpe_address = 1; + //current block number (signature will be valid only for short time around this block number) + uint64 current_block = 2; + //signature of the following message: + //for GetListUnclaimed ("__list_unclaimed", mpe_address, current_block_number) + //for GetListInProgress ("__list_in_progress", mpe_address, current_block_number) + bytes signature = 3; +} + +message StartClaimRequest { + //address of MultiPartyEscrow contract + string mpe_address = 1; + //channel_id contains id of the channel which state is requested. + bytes channel_id = 2; + //signature of the following message ("__start_claim", mpe_address, channel_id, channel_nonce) + bytes signature = 3; +} + +message PaymentReply { + bytes channel_id = 1; + + bytes channel_nonce = 2; + + bytes signed_amount = 3; + + //this filed must be OMITED in GetListUnclaimed request + bytes signature = 4; +} + +message PaymentsListReply { + repeated PaymentReply payments = 1; +} + + diff --git a/snet/sdk/resources/proto/control_service_pb2.py b/snet/sdk/resources/proto/control_service_pb2.py new file mode 100644 index 0000000..43b685a --- /dev/null +++ b/snet/sdk/resources/proto/control_service_pb2.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: control_service.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x63ontrol_service.proto\x12\x06\x65scrow\"W\n\x16GetPaymentsListRequest\x12\x13\n\x0bmpe_address\x18\x01 \x01(\t\x12\x15\n\rcurrent_block\x18\x02 \x01(\x04\x12\x11\n\tsignature\x18\x03 \x01(\x0c\"O\n\x11StartClaimRequest\x12\x13\n\x0bmpe_address\x18\x01 \x01(\t\x12\x12\n\nchannel_id\x18\x02 \x01(\x0c\x12\x11\n\tsignature\x18\x03 \x01(\x0c\"c\n\x0cPaymentReply\x12\x12\n\nchannel_id\x18\x01 \x01(\x0c\x12\x15\n\rchannel_nonce\x18\x02 \x01(\x0c\x12\x15\n\rsigned_amount\x18\x03 \x01(\x0c\x12\x11\n\tsignature\x18\x04 \x01(\x0c\";\n\x11PaymentsListReply\x12&\n\x08payments\x18\x01 \x03(\x0b\x32\x14.escrow.PaymentReply2\xfc\x01\n\x16ProviderControlService\x12O\n\x10GetListUnclaimed\x12\x1e.escrow.GetPaymentsListRequest\x1a\x19.escrow.PaymentsListReply\"\x00\x12P\n\x11GetListInProgress\x12\x1e.escrow.GetPaymentsListRequest\x1a\x19.escrow.PaymentsListReply\"\x00\x12?\n\nStartClaim\x12\x19.escrow.StartClaimRequest\x1a\x14.escrow.PaymentReply\"\x00\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'control_service_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_GETPAYMENTSLISTREQUEST']._serialized_start=33 + _globals['_GETPAYMENTSLISTREQUEST']._serialized_end=120 + _globals['_STARTCLAIMREQUEST']._serialized_start=122 + _globals['_STARTCLAIMREQUEST']._serialized_end=201 + _globals['_PAYMENTREPLY']._serialized_start=203 + _globals['_PAYMENTREPLY']._serialized_end=302 + _globals['_PAYMENTSLISTREPLY']._serialized_start=304 + _globals['_PAYMENTSLISTREPLY']._serialized_end=363 + _globals['_PROVIDERCONTROLSERVICE']._serialized_start=366 + _globals['_PROVIDERCONTROLSERVICE']._serialized_end=618 +# @@protoc_insertion_point(module_scope) diff --git a/snet/sdk/resources/proto/control_service_pb2_grpc.py b/snet/sdk/resources/proto/control_service_pb2_grpc.py new file mode 100644 index 0000000..818e476 --- /dev/null +++ b/snet/sdk/resources/proto/control_service_pb2_grpc.py @@ -0,0 +1,137 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +import control_service_pb2 as control__service__pb2 + + +class ProviderControlServiceStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.GetListUnclaimed = channel.unary_unary( + '/escrow.ProviderControlService/GetListUnclaimed', + request_serializer=control__service__pb2.GetPaymentsListRequest.SerializeToString, + response_deserializer=control__service__pb2.PaymentsListReply.FromString, + ) + self.GetListInProgress = channel.unary_unary( + '/escrow.ProviderControlService/GetListInProgress', + request_serializer=control__service__pb2.GetPaymentsListRequest.SerializeToString, + response_deserializer=control__service__pb2.PaymentsListReply.FromString, + ) + self.StartClaim = channel.unary_unary( + '/escrow.ProviderControlService/StartClaim', + request_serializer=control__service__pb2.StartClaimRequest.SerializeToString, + response_deserializer=control__service__pb2.PaymentReply.FromString, + ) + + +class ProviderControlServiceServicer(object): + """Missing associated documentation comment in .proto file.""" + + def GetListUnclaimed(self, request, context): + """get list of all unclaimed "payments". + in PaymentsSignatureReply signatures MUST be omited + if signature field is present then response should be considered as invalid + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetListInProgress(self, request, context): + """get list of all payments in progress + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def StartClaim(self, request, context): + """initilize claim for specific channel + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_ProviderControlServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'GetListUnclaimed': grpc.unary_unary_rpc_method_handler( + servicer.GetListUnclaimed, + request_deserializer=control__service__pb2.GetPaymentsListRequest.FromString, + response_serializer=control__service__pb2.PaymentsListReply.SerializeToString, + ), + 'GetListInProgress': grpc.unary_unary_rpc_method_handler( + servicer.GetListInProgress, + request_deserializer=control__service__pb2.GetPaymentsListRequest.FromString, + response_serializer=control__service__pb2.PaymentsListReply.SerializeToString, + ), + 'StartClaim': grpc.unary_unary_rpc_method_handler( + servicer.StartClaim, + request_deserializer=control__service__pb2.StartClaimRequest.FromString, + response_serializer=control__service__pb2.PaymentReply.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'escrow.ProviderControlService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class ProviderControlService(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def GetListUnclaimed(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/escrow.ProviderControlService/GetListUnclaimed', + control__service__pb2.GetPaymentsListRequest.SerializeToString, + control__service__pb2.PaymentsListReply.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetListInProgress(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/escrow.ProviderControlService/GetListInProgress', + control__service__pb2.GetPaymentsListRequest.SerializeToString, + control__service__pb2.PaymentsListReply.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def StartClaim(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/escrow.ProviderControlService/StartClaim', + control__service__pb2.StartClaimRequest.SerializeToString, + control__service__pb2.PaymentReply.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/snet/sdk/resources/proto/merckledag.proto b/snet/sdk/resources/proto/merckledag.proto new file mode 100644 index 0000000..5af078a --- /dev/null +++ b/snet/sdk/resources/proto/merckledag.proto @@ -0,0 +1,17 @@ +syntax = "proto2"; +// An IPFS MerkleDAG Link +message MerkleLink { + required bytes Hash = 1; // multihash of the target object + required string Name = 2; // utf string name + required uint64 Tsize = 3; // cumulative size of target object + + // user extensions start at 50 +} + +// An IPFS MerkleDAG Node +message MerkleNode { + repeated MerkleLink Links = 2; // refs to other objects + required bytes Data = 1; // opaque user data + + // user extensions start at 50 +} \ No newline at end of file diff --git a/snet/sdk/resources/proto/merckledag_pb2.py b/snet/sdk/resources/proto/merckledag_pb2.py new file mode 100644 index 0000000..c47ebb5 --- /dev/null +++ b/snet/sdk/resources/proto/merckledag_pb2.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: merckledag.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10merckledag.proto\"7\n\nMerkleLink\x12\x0c\n\x04Hash\x18\x01 \x02(\x0c\x12\x0c\n\x04Name\x18\x02 \x02(\t\x12\r\n\x05Tsize\x18\x03 \x02(\x04\"6\n\nMerkleNode\x12\x1a\n\x05Links\x18\x02 \x03(\x0b\x32\x0b.MerkleLink\x12\x0c\n\x04\x44\x61ta\x18\x01 \x02(\x0c') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'merckledag_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_MERKLELINK']._serialized_start=20 + _globals['_MERKLELINK']._serialized_end=75 + _globals['_MERKLENODE']._serialized_start=77 + _globals['_MERKLENODE']._serialized_end=131 +# @@protoc_insertion_point(module_scope) diff --git a/snet/sdk/resources/proto/merckledag_pb2_grpc.py b/snet/sdk/resources/proto/merckledag_pb2_grpc.py new file mode 100644 index 0000000..2daafff --- /dev/null +++ b/snet/sdk/resources/proto/merckledag_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/snet/sdk/resources/proto/state_service.proto b/snet/sdk/resources/proto/state_service.proto new file mode 100644 index 0000000..a2ce9aa --- /dev/null +++ b/snet/sdk/resources/proto/state_service.proto @@ -0,0 +1,81 @@ +syntax = "proto3"; + +package escrow; + +option java_package = "io.singularitynet.daemon.escrow"; + +// PaymentChannelStateService contains methods to get the MultiPartyEscrow +// payment channel state. +// channel_id, channel_nonce, value and amount fields below in fact are +// Solidity uint256 values. Which are big-endian integers, see +// https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI#formal-specification-of-the-encoding +// These values may be or may be not padded by zeros, service supports both +// options. +service PaymentChannelStateService { + // GetChannelState method returns a channel state by channel id. + rpc GetChannelState(ChannelStateRequest) returns (ChannelStateReply) {} +} + +// ChanelStateRequest is a request for channel state. +message ChannelStateRequest { + // channel_id contains id of the channel which state is requested. + bytes channel_id = 1; + + // signature is a client signature of the message which contains + // channel_id. It is used for client authorization. + bytes signature = 2; + + //current block number (signature will be valid only for short time around this block number) + uint64 current_block = 3; +} + +// ChannelStateReply message contains a latest channel state. current_nonce and +// current_value fields can be different from ones stored in the blockchain if +// server started withdrawing funds froms channel but transaction is still not +// finished. +message ChannelStateReply { + // current_nonce is a latest nonce of the payment channel. + bytes current_nonce = 1; + + // current_signed_amount is a last amount which were signed by client with current_nonce + //it could be absent if none message was signed with current_nonce + bytes current_signed_amount = 2; + + // current_signature is a last signature sent by client with current_nonce + // it could be absent if none message was signed with current nonce + bytes current_signature = 3; + + // last amount which was signed by client with nonce=current_nonce - 1 + bytes old_nonce_signed_amount = 4; + + // last signature sent by client with nonce = current_nonce - 1 + bytes old_nonce_signature = 5; + } + +//Used to determine free calls available for a given user. +service FreeCallStateService { + rpc GetFreeCallsAvailable(FreeCallStateRequest) returns (FreeCallStateReply) {} +} + +message FreeCallStateRequest { + //Has the user email id + string user_id = 1; + //signer-token = (user@mail, user-public-key, token_issue_date), this is generated my Market place Dapp + //to leverage free calls from SDK/ snet-cli, you will need this signer-token to be downloaded from Dapp + bytes token_for_free_call = 2; + //Token expiration date in Block number + uint64 token_expiry_date_block = 3 ; + //Signature is made up of the below, user signs with the private key corresponding with the public key used to generate the authorized token + //free-call-metadata = ("__prefix_free_trial",user_id,organization_id,service_id,group_id,current_block,authorized_token) + bytes signature = 4; + //current block number (signature will be valid only for short time around this block number) + uint64 current_block = 5; + +} + +message FreeCallStateReply { + //Has the user email id + string user_id = 1; + //Balance number of free calls available + uint64 free_calls_available = 2; +} diff --git a/snet/sdk/resources/proto/state_service_pb2.py b/snet/sdk/resources/proto/state_service_pb2.py new file mode 100644 index 0000000..d298f8a --- /dev/null +++ b/snet/sdk/resources/proto/state_service_pb2.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: state_service.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13state_service.proto\x12\x06\x65scrow\"S\n\x13\x43hannelStateRequest\x12\x12\n\nchannel_id\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\x12\x15\n\rcurrent_block\x18\x03 \x01(\x04\"\xa2\x01\n\x11\x43hannelStateReply\x12\x15\n\rcurrent_nonce\x18\x01 \x01(\x0c\x12\x1d\n\x15\x63urrent_signed_amount\x18\x02 \x01(\x0c\x12\x19\n\x11\x63urrent_signature\x18\x03 \x01(\x0c\x12\x1f\n\x17old_nonce_signed_amount\x18\x04 \x01(\x0c\x12\x1b\n\x13old_nonce_signature\x18\x05 \x01(\x0c\"\x8f\x01\n\x14\x46reeCallStateRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\x1b\n\x13token_for_free_call\x18\x02 \x01(\x0c\x12\x1f\n\x17token_expiry_date_block\x18\x03 \x01(\x04\x12\x11\n\tsignature\x18\x04 \x01(\x0c\x12\x15\n\rcurrent_block\x18\x05 \x01(\x04\"C\n\x12\x46reeCallStateReply\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\x1c\n\x14\x66ree_calls_available\x18\x02 \x01(\x04\x32i\n\x1aPaymentChannelStateService\x12K\n\x0fGetChannelState\x12\x1b.escrow.ChannelStateRequest\x1a\x19.escrow.ChannelStateReply\"\x00\x32k\n\x14\x46reeCallStateService\x12S\n\x15GetFreeCallsAvailable\x12\x1c.escrow.FreeCallStateRequest\x1a\x1a.escrow.FreeCallStateReply\"\x00\x42!\n\x1fio.singularitynet.daemon.escrowb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'state_service_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\037io.singularitynet.daemon.escrow' + _globals['_CHANNELSTATEREQUEST']._serialized_start=31 + _globals['_CHANNELSTATEREQUEST']._serialized_end=114 + _globals['_CHANNELSTATEREPLY']._serialized_start=117 + _globals['_CHANNELSTATEREPLY']._serialized_end=279 + _globals['_FREECALLSTATEREQUEST']._serialized_start=282 + _globals['_FREECALLSTATEREQUEST']._serialized_end=425 + _globals['_FREECALLSTATEREPLY']._serialized_start=427 + _globals['_FREECALLSTATEREPLY']._serialized_end=494 + _globals['_PAYMENTCHANNELSTATESERVICE']._serialized_start=496 + _globals['_PAYMENTCHANNELSTATESERVICE']._serialized_end=601 + _globals['_FREECALLSTATESERVICE']._serialized_start=603 + _globals['_FREECALLSTATESERVICE']._serialized_end=710 +# @@protoc_insertion_point(module_scope) diff --git a/snet/sdk/resources/proto/state_service_pb2_grpc.py b/snet/sdk/resources/proto/state_service_pb2_grpc.py new file mode 100644 index 0000000..977dbd7 --- /dev/null +++ b/snet/sdk/resources/proto/state_service_pb2_grpc.py @@ -0,0 +1,152 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +import state_service_pb2 as state__service__pb2 + + +class PaymentChannelStateServiceStub(object): + """PaymentChannelStateService contains methods to get the MultiPartyEscrow + payment channel state. + channel_id, channel_nonce, value and amount fields below in fact are + Solidity uint256 values. Which are big-endian integers, see + https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI#formal-specification-of-the-encoding + These values may be or may be not padded by zeros, service supports both + options. + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.GetChannelState = channel.unary_unary( + '/escrow.PaymentChannelStateService/GetChannelState', + request_serializer=state__service__pb2.ChannelStateRequest.SerializeToString, + response_deserializer=state__service__pb2.ChannelStateReply.FromString, + ) + + +class PaymentChannelStateServiceServicer(object): + """PaymentChannelStateService contains methods to get the MultiPartyEscrow + payment channel state. + channel_id, channel_nonce, value and amount fields below in fact are + Solidity uint256 values. Which are big-endian integers, see + https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI#formal-specification-of-the-encoding + These values may be or may be not padded by zeros, service supports both + options. + """ + + def GetChannelState(self, request, context): + """GetChannelState method returns a channel state by channel id. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_PaymentChannelStateServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'GetChannelState': grpc.unary_unary_rpc_method_handler( + servicer.GetChannelState, + request_deserializer=state__service__pb2.ChannelStateRequest.FromString, + response_serializer=state__service__pb2.ChannelStateReply.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'escrow.PaymentChannelStateService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class PaymentChannelStateService(object): + """PaymentChannelStateService contains methods to get the MultiPartyEscrow + payment channel state. + channel_id, channel_nonce, value and amount fields below in fact are + Solidity uint256 values. Which are big-endian integers, see + https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI#formal-specification-of-the-encoding + These values may be or may be not padded by zeros, service supports both + options. + """ + + @staticmethod + def GetChannelState(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/escrow.PaymentChannelStateService/GetChannelState', + state__service__pb2.ChannelStateRequest.SerializeToString, + state__service__pb2.ChannelStateReply.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + +class FreeCallStateServiceStub(object): + """Used to determine free calls available for a given user. + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.GetFreeCallsAvailable = channel.unary_unary( + '/escrow.FreeCallStateService/GetFreeCallsAvailable', + request_serializer=state__service__pb2.FreeCallStateRequest.SerializeToString, + response_deserializer=state__service__pb2.FreeCallStateReply.FromString, + ) + + +class FreeCallStateServiceServicer(object): + """Used to determine free calls available for a given user. + """ + + def GetFreeCallsAvailable(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_FreeCallStateServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'GetFreeCallsAvailable': grpc.unary_unary_rpc_method_handler( + servicer.GetFreeCallsAvailable, + request_deserializer=state__service__pb2.FreeCallStateRequest.FromString, + response_serializer=state__service__pb2.FreeCallStateReply.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'escrow.FreeCallStateService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class FreeCallStateService(object): + """Used to determine free calls available for a given user. + """ + + @staticmethod + def GetFreeCallsAvailable(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/escrow.FreeCallStateService/GetFreeCallsAvailable', + state__service__pb2.FreeCallStateRequest.SerializeToString, + state__service__pb2.FreeCallStateReply.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/snet/sdk/resources/proto/token_service.proto b/snet/sdk/resources/proto/token_service.proto new file mode 100644 index 0000000..33a6656 --- /dev/null +++ b/snet/sdk/resources/proto/token_service.proto @@ -0,0 +1,63 @@ +syntax = "proto3"; + +package escrow; + +option java_package = "io.singularitynet.daemon.escrow"; +//It is expected that the user would call the GetChannelState to Determine the Current state of the Channel +//Based on the usage forecast, the user/client will have to sign for an amount L + U , where L is the last amount Signed +//and U is the amount based on expected usage. +//Please be aware that the Signing up an amount upfront ( Pre Paid) does come with a risk and hence the +//user must exercise caution on the amount signed specially with new service providers. +//If there is no need of making concurrent calls then you may consider pay per mode. +//Using a Token, the Client can now make concurrent calls, which was not supported previously with the pay per mode. +//However the pay per mode is a lot secure than the pre-paid mode. +service TokenService { + // GetToken method checks the Signature sent and returns a Token + // 1) The Signature is valid and has to be signed in the below format + //"__MPE_claim_message"+MpeContractAddress+ChannelID+ChannelNonce+SignedAmount + //Signature is to let the Service Provider make a claim + // 2) Signed amount >= Last amount Signed. + // if Signed amount == Last Signed amount , then check if planned_amount < used_amount + // if Signed amount > Last Signed amount , then update the planned amount = Signed Amount + // GetToken method in a way behaves as a renew Token too!. + rpc GetToken(TokenRequest) returns (TokenReply) {} + + + +} + +// TokenRequest is a request for getting a valid token. +message TokenRequest { + // channel_id contains id of the channel which state is requested. + uint64 channel_id = 1; + // current_nonce is a latest nonce of the payment channel. + uint64 current_nonce = 2; + //signed_amount is the amount signed by client with current_nonce + uint64 signed_amount = 3; + // Signature is a client signature of the message which contains 2 parts + //Part 1 : MPE Signature "__MPE_claim_message"+MpeContractAddress+ChannelID+ChannelNonce+SignedAmount + //Part 2 : Current Block Number + bytes signature = 4; + //current block number (signature will be valid only for short time around this block number) + uint64 current_block = 5; + + bytes claim_signature = 6; + +} + +// TokenReply message contains a latest channel state. current_nonce and +message TokenReply { + // current_nonce is a latest nonce of the payment channel. + uint64 channel_id = 1; + + //it could be absent if none message was signed with current_nonce + string token = 2; + + //If the client / user chooses to sign upfront , the planned amount in cogs will be indicative of this. + uint64 planned_amount = 3; + + //If the client / user chooses to sign upfront , the used amount in cogs will be indicative of how much of the + //planned amount has actually been used. + uint64 used_amount = 4; + +} diff --git a/snet/sdk/resources/proto/token_service_pb2.py b/snet/sdk/resources/proto/token_service_pb2.py new file mode 100644 index 0000000..038ca7f --- /dev/null +++ b/snet/sdk/resources/proto/token_service_pb2.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: token_service.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13token_service.proto\x12\x06\x65scrow\"\x93\x01\n\x0cTokenRequest\x12\x12\n\nchannel_id\x18\x01 \x01(\x04\x12\x15\n\rcurrent_nonce\x18\x02 \x01(\x04\x12\x15\n\rsigned_amount\x18\x03 \x01(\x04\x12\x11\n\tsignature\x18\x04 \x01(\x0c\x12\x15\n\rcurrent_block\x18\x05 \x01(\x04\x12\x17\n\x0f\x63laim_signature\x18\x06 \x01(\x0c\"\\\n\nTokenReply\x12\x12\n\nchannel_id\x18\x01 \x01(\x04\x12\r\n\x05token\x18\x02 \x01(\t\x12\x16\n\x0eplanned_amount\x18\x03 \x01(\x04\x12\x13\n\x0bused_amount\x18\x04 \x01(\x04\x32\x46\n\x0cTokenService\x12\x36\n\x08GetToken\x12\x14.escrow.TokenRequest\x1a\x12.escrow.TokenReply\"\x00\x42!\n\x1fio.singularitynet.daemon.escrowb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'token_service_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\037io.singularitynet.daemon.escrow' + _globals['_TOKENREQUEST']._serialized_start=32 + _globals['_TOKENREQUEST']._serialized_end=179 + _globals['_TOKENREPLY']._serialized_start=181 + _globals['_TOKENREPLY']._serialized_end=273 + _globals['_TOKENSERVICE']._serialized_start=275 + _globals['_TOKENSERVICE']._serialized_end=345 +# @@protoc_insertion_point(module_scope) diff --git a/snet/sdk/resources/proto/token_service_pb2_grpc.py b/snet/sdk/resources/proto/token_service_pb2_grpc.py new file mode 100644 index 0000000..dd1bc73 --- /dev/null +++ b/snet/sdk/resources/proto/token_service_pb2_grpc.py @@ -0,0 +1,98 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +import token_service_pb2 as token__service__pb2 + + +class TokenServiceStub(object): + """It is expected that the user would call the GetChannelState to Determine the Current state of the Channel + Based on the usage forecast, the user/client will have to sign for an amount L + U , where L is the last amount Signed + and U is the amount based on expected usage. + Please be aware that the Signing up an amount upfront ( Pre Paid) does come with a risk and hence the + user must exercise caution on the amount signed specially with new service providers. + If there is no need of making concurrent calls then you may consider pay per mode. + Using a Token, the Client can now make concurrent calls, which was not supported previously with the pay per mode. + However the pay per mode is a lot secure than the pre-paid mode. + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.GetToken = channel.unary_unary( + '/escrow.TokenService/GetToken', + request_serializer=token__service__pb2.TokenRequest.SerializeToString, + response_deserializer=token__service__pb2.TokenReply.FromString, + ) + + +class TokenServiceServicer(object): + """It is expected that the user would call the GetChannelState to Determine the Current state of the Channel + Based on the usage forecast, the user/client will have to sign for an amount L + U , where L is the last amount Signed + and U is the amount based on expected usage. + Please be aware that the Signing up an amount upfront ( Pre Paid) does come with a risk and hence the + user must exercise caution on the amount signed specially with new service providers. + If there is no need of making concurrent calls then you may consider pay per mode. + Using a Token, the Client can now make concurrent calls, which was not supported previously with the pay per mode. + However the pay per mode is a lot secure than the pre-paid mode. + """ + + def GetToken(self, request, context): + """GetToken method checks the Signature sent and returns a Token + 1) The Signature is valid and has to be signed in the below format + "__MPE_claim_message"+MpeContractAddress+ChannelID+ChannelNonce+SignedAmount + Signature is to let the Service Provider make a claim + 2) Signed amount >= Last amount Signed. + if Signed amount == Last Signed amount , then check if planned_amount < used_amount + if Signed amount > Last Signed amount , then update the planned amount = Signed Amount + GetToken method in a way behaves as a renew Token too!. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_TokenServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'GetToken': grpc.unary_unary_rpc_method_handler( + servicer.GetToken, + request_deserializer=token__service__pb2.TokenRequest.FromString, + response_serializer=token__service__pb2.TokenReply.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'escrow.TokenService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class TokenService(object): + """It is expected that the user would call the GetChannelState to Determine the Current state of the Channel + Based on the usage forecast, the user/client will have to sign for an amount L + U , where L is the last amount Signed + and U is the amount based on expected usage. + Please be aware that the Signing up an amount upfront ( Pre Paid) does come with a risk and hence the + user must exercise caution on the amount signed specially with new service providers. + If there is no need of making concurrent calls then you may consider pay per mode. + Using a Token, the Client can now make concurrent calls, which was not supported previously with the pay per mode. + However the pay per mode is a lot secure than the pre-paid mode. + """ + + @staticmethod + def GetToken(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/escrow.TokenService/GetToken', + token__service__pb2.TokenRequest.SerializeToString, + token__service__pb2.TokenReply.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/snet/sdk/resources/proto/training.proto b/snet/sdk/resources/proto/training.proto new file mode 100644 index 0000000..86b3074 --- /dev/null +++ b/snet/sdk/resources/proto/training.proto @@ -0,0 +1,112 @@ +syntax = "proto3"; +import "google/protobuf/descriptor.proto"; +package training; +option go_package = "../training"; +//Please note that the AI developers need to provide a server implementation of the gprc server of this proto. +message ModelDetails { + //This Id will be generated when you invoke the create_model method and hence doesnt need to be filled when you + //invoke the create model + string model_id = 1; + //define the training method name + string grpc_method_name = 2; + //define the grpc service name , under which the method is defined + string grpc_service_name = 3; + string description = 4; + + string status = 6; + string updated_date = 7; + //List of all the addresses that will have access to this model + repeated string address_list = 8; + // this is optional + string training_data_link = 9; + string model_name = 10; + + + string organization_id = 11; + string service_id = 12 ; + string group_id = 13; + + //set this to true if you want your model to be used by other AI consumers + bool is_publicly_accessible = 14; + +} + +message AuthorizationDetails { + uint64 current_block = 1; + //Signer can fill in any message here + string message = 2; + //signature of the following message: + //("user specified message", user_address, current_block_number) + bytes signature = 3; + string signer_address = 4; + +} + +enum Status { + CREATED = 0; + IN_PROGRESS = 1; + ERRORED = 2; + COMPLETED = 3; + DELETED = 4; +} + +message CreateModelRequest { + AuthorizationDetails authorization = 1; + ModelDetails model_details = 2; +} + +//the signer address will get to know all the models associated with this address. +message AccessibleModelsRequest { + string grpc_method_name = 1; + string grpc_service_name = 2; + AuthorizationDetails authorization = 3; +} + +message AccessibleModelsResponse { + repeated ModelDetails list_of_models = 1; +} + +message ModelDetailsRequest { + ModelDetails model_details = 1 ; + AuthorizationDetails authorization = 2; +} + +//helps determine which service end point to call for model training +//format is of type "packageName/serviceName/MethodName", Example :"/example_service.Calculator/estimate_add" +//Daemon will invoke the model training end point , when the below method option is specified +message TrainingMethodOption { + string trainingMethodIndicator = 1; +} + +extend google.protobuf.MethodOptions { + TrainingMethodOption my_method_option = 9999197; +} + +message UpdateModelRequest { + ModelDetails update_model_details = 1 ; + AuthorizationDetails authorization = 2; +} + + +message ModelDetailsResponse { + Status status = 1; + ModelDetails model_details = 2; + +} + +service Model { + + // The AI developer needs to Implement this service and Daemon will call these + // There will be no cost borne by the consumer in calling these methods, + // Pricing will apply when you actually call the training methods defined. + // AI consumer will call all these methods + rpc create_model(CreateModelRequest) returns (ModelDetailsResponse) {} + rpc delete_model(UpdateModelRequest) returns (ModelDetailsResponse) {} + rpc get_model_status(ModelDetailsRequest) returns (ModelDetailsResponse) {} + + // Daemon will implement , however the AI developer should skip implementing these and just provide dummy code. + rpc update_model_access(UpdateModelRequest) returns (ModelDetailsResponse) {} + rpc get_all_models(AccessibleModelsRequest) returns (AccessibleModelsResponse) {} + + +} \ No newline at end of file diff --git a/snet/sdk/resources/proto/training_pb2.py b/snet/sdk/resources/proto/training_pb2.py new file mode 100644 index 0000000..735e256 --- /dev/null +++ b/snet/sdk/resources/proto/training_pb2.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: training.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0etraining.proto\x12\x08training\x1a google/protobuf/descriptor.proto\"\xb5\x02\n\x0cModelDetails\x12\x10\n\x08model_id\x18\x01 \x01(\t\x12\x18\n\x10grpc_method_name\x18\x02 \x01(\t\x12\x19\n\x11grpc_service_name\x18\x03 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12\x0e\n\x06status\x18\x06 \x01(\t\x12\x14\n\x0cupdated_date\x18\x07 \x01(\t\x12\x14\n\x0c\x61\x64\x64ress_list\x18\x08 \x03(\t\x12\x1a\n\x12training_data_link\x18\t \x01(\t\x12\x12\n\nmodel_name\x18\n \x01(\t\x12\x17\n\x0forganization_id\x18\x0b \x01(\t\x12\x12\n\nservice_id\x18\x0c \x01(\t\x12\x10\n\x08group_id\x18\r \x01(\t\x12\x1e\n\x16is_publicly_accessible\x18\x0e \x01(\x08\"i\n\x14\x41uthorizationDetails\x12\x15\n\rcurrent_block\x18\x01 \x01(\x04\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x11\n\tsignature\x18\x03 \x01(\x0c\x12\x16\n\x0esigner_address\x18\x04 \x01(\t\"z\n\x12\x43reateModelRequest\x12\x35\n\rauthorization\x18\x01 \x01(\x0b\x32\x1e.training.AuthorizationDetails\x12-\n\rmodel_details\x18\x02 \x01(\x0b\x32\x16.training.ModelDetails\"\x85\x01\n\x17\x41\x63\x63\x65ssibleModelsRequest\x12\x18\n\x10grpc_method_name\x18\x01 \x01(\t\x12\x19\n\x11grpc_service_name\x18\x02 \x01(\t\x12\x35\n\rauthorization\x18\x03 \x01(\x0b\x32\x1e.training.AuthorizationDetails\"J\n\x18\x41\x63\x63\x65ssibleModelsResponse\x12.\n\x0elist_of_models\x18\x01 \x03(\x0b\x32\x16.training.ModelDetails\"{\n\x13ModelDetailsRequest\x12-\n\rmodel_details\x18\x01 \x01(\x0b\x32\x16.training.ModelDetails\x12\x35\n\rauthorization\x18\x02 \x01(\x0b\x32\x1e.training.AuthorizationDetails\"7\n\x14TrainingMethodOption\x12\x1f\n\x17trainingMethodIndicator\x18\x01 \x01(\t\"\x81\x01\n\x12UpdateModelRequest\x12\x34\n\x14update_model_details\x18\x01 \x01(\x0b\x32\x16.training.ModelDetails\x12\x35\n\rauthorization\x18\x02 \x01(\x0b\x32\x1e.training.AuthorizationDetails\"g\n\x14ModelDetailsResponse\x12 \n\x06status\x18\x01 \x01(\x0e\x32\x10.training.Status\x12-\n\rmodel_details\x18\x02 \x01(\x0b\x32\x16.training.ModelDetails*O\n\x06Status\x12\x0b\n\x07\x43REATED\x10\x00\x12\x0f\n\x0bIN_PROGRESS\x10\x01\x12\x0b\n\x07\x45RRORED\x10\x02\x12\r\n\tCOMPLETED\x10\x03\x12\x0b\n\x07\x44\x45LETED\x10\x04\x32\xae\x03\n\x05Model\x12N\n\x0c\x63reate_model\x12\x1c.training.CreateModelRequest\x1a\x1e.training.ModelDetailsResponse\"\x00\x12N\n\x0c\x64\x65lete_model\x12\x1c.training.UpdateModelRequest\x1a\x1e.training.ModelDetailsResponse\"\x00\x12S\n\x10get_model_status\x12\x1d.training.ModelDetailsRequest\x1a\x1e.training.ModelDetailsResponse\"\x00\x12U\n\x13update_model_access\x12\x1c.training.UpdateModelRequest\x1a\x1e.training.ModelDetailsResponse\"\x00\x12Y\n\x0eget_all_models\x12!.training.AccessibleModelsRequest\x1a\".training.AccessibleModelsResponse\"\x00:[\n\x10my_method_option\x12\x1e.google.protobuf.MethodOptions\x18\xdd\xa6\xe2\x04 \x01(\x0b\x32\x1e.training.TrainingMethodOptionB\rZ\x0b../trainingb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'training_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'Z\013../training' + _globals['_STATUS']._serialized_start=1236 + _globals['_STATUS']._serialized_end=1315 + _globals['_MODELDETAILS']._serialized_start=63 + _globals['_MODELDETAILS']._serialized_end=372 + _globals['_AUTHORIZATIONDETAILS']._serialized_start=374 + _globals['_AUTHORIZATIONDETAILS']._serialized_end=479 + _globals['_CREATEMODELREQUEST']._serialized_start=481 + _globals['_CREATEMODELREQUEST']._serialized_end=603 + _globals['_ACCESSIBLEMODELSREQUEST']._serialized_start=606 + _globals['_ACCESSIBLEMODELSREQUEST']._serialized_end=739 + _globals['_ACCESSIBLEMODELSRESPONSE']._serialized_start=741 + _globals['_ACCESSIBLEMODELSRESPONSE']._serialized_end=815 + _globals['_MODELDETAILSREQUEST']._serialized_start=817 + _globals['_MODELDETAILSREQUEST']._serialized_end=940 + _globals['_TRAININGMETHODOPTION']._serialized_start=942 + _globals['_TRAININGMETHODOPTION']._serialized_end=997 + _globals['_UPDATEMODELREQUEST']._serialized_start=1000 + _globals['_UPDATEMODELREQUEST']._serialized_end=1129 + _globals['_MODELDETAILSRESPONSE']._serialized_start=1131 + _globals['_MODELDETAILSRESPONSE']._serialized_end=1234 + _globals['_MODEL']._serialized_start=1318 + _globals['_MODEL']._serialized_end=1748 +# @@protoc_insertion_point(module_scope) diff --git a/snet/sdk/resources/proto/training_pb2_grpc.py b/snet/sdk/resources/proto/training_pb2_grpc.py new file mode 100644 index 0000000..51ccf27 --- /dev/null +++ b/snet/sdk/resources/proto/training_pb2_grpc.py @@ -0,0 +1,203 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +import training_pb2 as training__pb2 + + +class ModelStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.create_model = channel.unary_unary( + '/training.Model/create_model', + request_serializer=training__pb2.CreateModelRequest.SerializeToString, + response_deserializer=training__pb2.ModelDetailsResponse.FromString, + ) + self.delete_model = channel.unary_unary( + '/training.Model/delete_model', + request_serializer=training__pb2.UpdateModelRequest.SerializeToString, + response_deserializer=training__pb2.ModelDetailsResponse.FromString, + ) + self.get_model_status = channel.unary_unary( + '/training.Model/get_model_status', + request_serializer=training__pb2.ModelDetailsRequest.SerializeToString, + response_deserializer=training__pb2.ModelDetailsResponse.FromString, + ) + self.update_model_access = channel.unary_unary( + '/training.Model/update_model_access', + request_serializer=training__pb2.UpdateModelRequest.SerializeToString, + response_deserializer=training__pb2.ModelDetailsResponse.FromString, + ) + self.get_all_models = channel.unary_unary( + '/training.Model/get_all_models', + request_serializer=training__pb2.AccessibleModelsRequest.SerializeToString, + response_deserializer=training__pb2.AccessibleModelsResponse.FromString, + ) + + +class ModelServicer(object): + """Missing associated documentation comment in .proto file.""" + + def create_model(self, request, context): + """The AI developer needs to Implement this service and Daemon will call these + There will be no cost borne by the consumer in calling these methods, + Pricing will apply when you actually call the training methods defined. + AI consumer will call all these methods + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def delete_model(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def get_model_status(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def update_model_access(self, request, context): + """Daemon will implement , however the AI developer should skip implementing these and just provide dummy code. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def get_all_models(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_ModelServicer_to_server(servicer, server): + rpc_method_handlers = { + 'create_model': grpc.unary_unary_rpc_method_handler( + servicer.create_model, + request_deserializer=training__pb2.CreateModelRequest.FromString, + response_serializer=training__pb2.ModelDetailsResponse.SerializeToString, + ), + 'delete_model': grpc.unary_unary_rpc_method_handler( + servicer.delete_model, + request_deserializer=training__pb2.UpdateModelRequest.FromString, + response_serializer=training__pb2.ModelDetailsResponse.SerializeToString, + ), + 'get_model_status': grpc.unary_unary_rpc_method_handler( + servicer.get_model_status, + request_deserializer=training__pb2.ModelDetailsRequest.FromString, + response_serializer=training__pb2.ModelDetailsResponse.SerializeToString, + ), + 'update_model_access': grpc.unary_unary_rpc_method_handler( + servicer.update_model_access, + request_deserializer=training__pb2.UpdateModelRequest.FromString, + response_serializer=training__pb2.ModelDetailsResponse.SerializeToString, + ), + 'get_all_models': grpc.unary_unary_rpc_method_handler( + servicer.get_all_models, + request_deserializer=training__pb2.AccessibleModelsRequest.FromString, + response_serializer=training__pb2.AccessibleModelsResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'training.Model', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class Model(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def create_model(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/training.Model/create_model', + training__pb2.CreateModelRequest.SerializeToString, + training__pb2.ModelDetailsResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def delete_model(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/training.Model/delete_model', + training__pb2.UpdateModelRequest.SerializeToString, + training__pb2.ModelDetailsResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def get_model_status(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/training.Model/get_model_status', + training__pb2.ModelDetailsRequest.SerializeToString, + training__pb2.ModelDetailsResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def update_model_access(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/training.Model/update_model_access', + training__pb2.UpdateModelRequest.SerializeToString, + training__pb2.ModelDetailsResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def get_all_models(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/training.Model/get_all_models', + training__pb2.AccessibleModelsRequest.SerializeToString, + training__pb2.AccessibleModelsResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/snet/sdk/resources/proto/unixfs.proto b/snet/sdk/resources/proto/unixfs.proto new file mode 100644 index 0000000..c190079 --- /dev/null +++ b/snet/sdk/resources/proto/unixfs.proto @@ -0,0 +1,25 @@ +syntax = "proto2"; +package unixfs.pb; + +message Data { + enum DataType { + Raw = 0; + Directory = 1; + File = 2; + Metadata = 3; + Symlink = 4; + HAMTShard = 5; + } + + required DataType Type = 1; + optional bytes Data = 2; + optional uint64 filesize = 3; + repeated uint64 blocksizes = 4; + + optional uint64 hashType = 5; + optional uint64 fanout = 6; +} + +message Metadata { + optional string MimeType = 1; +} \ No newline at end of file diff --git a/snet/sdk/resources/proto/unixfs_pb2.py b/snet/sdk/resources/proto/unixfs_pb2.py new file mode 100644 index 0000000..394f454 --- /dev/null +++ b/snet/sdk/resources/proto/unixfs_pb2.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: unixfs.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cunixfs.proto\x12\tunixfs.pb\"\xdc\x01\n\x04\x44\x61ta\x12&\n\x04Type\x18\x01 \x02(\x0e\x32\x18.unixfs.pb.Data.DataType\x12\x0c\n\x04\x44\x61ta\x18\x02 \x01(\x0c\x12\x10\n\x08\x66ilesize\x18\x03 \x01(\x04\x12\x12\n\nblocksizes\x18\x04 \x03(\x04\x12\x10\n\x08hashType\x18\x05 \x01(\x04\x12\x0e\n\x06\x66\x61nout\x18\x06 \x01(\x04\"V\n\x08\x44\x61taType\x12\x07\n\x03Raw\x10\x00\x12\r\n\tDirectory\x10\x01\x12\x08\n\x04\x46ile\x10\x02\x12\x0c\n\x08Metadata\x10\x03\x12\x0b\n\x07Symlink\x10\x04\x12\r\n\tHAMTShard\x10\x05\"\x1c\n\x08Metadata\x12\x10\n\x08MimeType\x18\x01 \x01(\t') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'unixfs_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_DATA']._serialized_start=28 + _globals['_DATA']._serialized_end=248 + _globals['_DATA_DATATYPE']._serialized_start=162 + _globals['_DATA_DATATYPE']._serialized_end=248 + _globals['_METADATA']._serialized_start=250 + _globals['_METADATA']._serialized_end=278 +# @@protoc_insertion_point(module_scope) diff --git a/snet/sdk/resources/proto/unixfs_pb2_grpc.py b/snet/sdk/resources/proto/unixfs_pb2_grpc.py new file mode 100644 index 0000000..2daafff --- /dev/null +++ b/snet/sdk/resources/proto/unixfs_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/snet/sdk/resources/root_certificate.py b/snet/sdk/resources/root_certificate.py new file mode 100644 index 0000000..eb383d3 --- /dev/null +++ b/snet/sdk/resources/root_certificate.py @@ -0,0 +1 @@ +certificate = b'# This Source Code Form is subject to the terms of the Mozilla Public\r\n# License, v. 2.0. If a copy of the MPL was not distributed with this\r\n# file, You can obtain one at http://mozilla.org/MPL/2.0/.\r\n\r\n# Issuer: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA\r\n# Subject: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA\r\n# Label: "GlobalSign Root CA"\r\n# Serial: 4835703278459707669005204\r\n# MD5 Fingerprint: 3e:45:52:15:09:51:92:e1:b7:5d:37:9f:b1:87:29:8a\r\n# SHA1 Fingerprint: b1:bc:96:8b:d4:f4:9d:62:2a:a8:9a:81:f2:15:01:52:a4:1d:82:9c\r\n# SHA256 Fingerprint: eb:d4:10:40:e4:bb:3e:c7:42:c9:e3:81:d3:1e:f2:a4:1a:48:b6:68:5c:96:e7:ce:f3:c1:df:6c:d4:33:1c:99\r\n-----BEGIN CERTIFICATE-----\r\nMIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG\r\nA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv\r\nb3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw\r\nMDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i\r\nYWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT\r\naWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ\r\njc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp\r\nxy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp\r\n1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG\r\nsnUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ\r\nU26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8\r\n9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E\r\nBTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B\r\nAQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz\r\nyj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE\r\n38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP\r\nAbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad\r\nDKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME\r\nHMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R2\r\n# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R2\r\n# Label: "GlobalSign Root CA - R2"\r\n# Serial: 4835703278459682885658125\r\n# MD5 Fingerprint: 94:14:77:7e:3e:5e:fd:8f:30:bd:41:b0:cf:e7:d0:30\r\n# SHA1 Fingerprint: 75:e0:ab:b6:13:85:12:27:1c:04:f8:5f:dd:de:38:e4:b7:24:2e:fe\r\n# SHA256 Fingerprint: ca:42:dd:41:74:5f:d0:b8:1e:b9:02:36:2c:f9:d8:bf:71:9d:a1:bd:1b:1e:fc:94:6f:5b:4c:99:f4:2c:1b:9e\r\n-----BEGIN CERTIFICATE-----\r\nMIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G\r\nA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp\r\nZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1\r\nMDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG\r\nA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI\r\nhvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL\r\nv4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8\r\neoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq\r\ntTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd\r\nC9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa\r\nzq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB\r\nmTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH\r\nV2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n\r\nbG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG\r\n3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs\r\nJ0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO\r\n291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS\r\not+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd\r\nAfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7\r\nTBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited\r\n# Subject: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited\r\n# Label: "Entrust.net Premium 2048 Secure Server CA"\r\n# Serial: 946069240\r\n# MD5 Fingerprint: ee:29:31:bc:32:7e:9a:e6:e8:b5:f7:51:b4:34:71:90\r\n# SHA1 Fingerprint: 50:30:06:09:1d:97:d4:f5:ae:39:f7:cb:e7:92:7d:7d:65:2d:34:31\r\n# SHA256 Fingerprint: 6d:c4:71:72:e0:1c:bc:b0:bf:62:58:0d:89:5f:e2:b8:ac:9a:d4:f8:73:80:1e:0c:10:b9:c8:37:d2:1e:b1:77\r\n-----BEGIN CERTIFICATE-----\r\nMIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML\r\nRW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp\r\nbmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5\r\nIEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp\r\nZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0yOTA3\r\nMjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3\r\nLmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp\r\nYWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG\r\nA1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp\r\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq\r\nK0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe\r\nsYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX\r\nMlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT\r\nXTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/\r\nHoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH\r\n4QIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\r\nHQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJKoZIhvcNAQEFBQADggEBADub\r\nj1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPyT/4xmf3IDExo\r\nU8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf\r\nzX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5b\r\nu/8j72gZyxKTJ1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+\r\nbYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er\r\nfF6adulZkMV8gzURZVE=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Baltimore CyberTrust Root O=Baltimore OU=CyberTrust\r\n# Subject: CN=Baltimore CyberTrust Root O=Baltimore OU=CyberTrust\r\n# Label: "Baltimore CyberTrust Root"\r\n# Serial: 33554617\r\n# MD5 Fingerprint: ac:b6:94:a5:9c:17:e0:d7:91:52:9b:b1:97:06:a6:e4\r\n# SHA1 Fingerprint: d4:de:20:d0:5e:66:fc:53:fe:1a:50:88:2c:78:db:28:52:ca:e4:74\r\n# SHA256 Fingerprint: 16:af:57:a9:f6:76:b0:ab:12:60:95:aa:5e:ba:de:f2:2a:b3:11:19:d6:44:ac:95:cd:4b:93:db:f3:f2:6a:eb\r\n-----BEGIN CERTIFICATE-----\r\nMIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ\r\nRTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD\r\nVQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX\r\nDTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y\r\nZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy\r\nVHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr\r\nmD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr\r\nIZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK\r\nmpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu\r\nXmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy\r\ndc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye\r\njl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1\r\nBE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3\r\nDQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92\r\n9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx\r\njkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0\r\nEpn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz\r\nksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS\r\nR9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc.\r\n# Subject: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc.\r\n# Label: "Entrust Root Certification Authority"\r\n# Serial: 1164660820\r\n# MD5 Fingerprint: d6:a5:c3:ed:5d:dd:3e:00:c1:3d:87:92:1f:1d:3f:e4\r\n# SHA1 Fingerprint: b3:1e:b1:b7:40:e3:6c:84:02:da:dc:37:d4:4d:f5:d4:67:49:52:f9\r\n# SHA256 Fingerprint: 73:c1:76:43:4f:1b:c6:d5:ad:f4:5b:0e:76:e7:27:28:7c:8d:e5:76:16:c1:e6:e6:14:1a:2b:2c:bc:7d:8e:4c\r\n-----BEGIN CERTIFICATE-----\r\nMIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC\r\nVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0\r\nLm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW\r\nKGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl\r\ncnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw\r\nNTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw\r\nNwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy\r\nZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV\r\nBAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ\r\nKoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo\r\nNu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4\r\n4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9\r\nKlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI\r\nrb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi\r\n94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB\r\nsDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi\r\ngA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo\r\nkORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE\r\nvW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA\r\nA4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t\r\nO1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua\r\nAGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP\r\n9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/\r\neu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m\r\n0vdXcDazv/wor3ElhVsT/h5/WrQ8\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=AAA Certificate Services O=Comodo CA Limited\r\n# Subject: CN=AAA Certificate Services O=Comodo CA Limited\r\n# Label: "Comodo AAA Services root"\r\n# Serial: 1\r\n# MD5 Fingerprint: 49:79:04:b0:eb:87:19:ac:47:b0:bc:11:51:9b:74:d0\r\n# SHA1 Fingerprint: d1:eb:23:a4:6d:17:d6:8f:d9:25:64:c2:f1:f1:60:17:64:d8:e3:49\r\n# SHA256 Fingerprint: d7:a7:a0:fb:5d:7e:27:31:d7:71:e9:48:4e:bc:de:f7:1d:5f:0c:3e:0a:29:48:78:2b:c8:3e:e0:ea:69:9e:f4\r\n-----BEGIN CERTIFICATE-----\r\nMIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb\r\nMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow\r\nGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj\r\nYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL\r\nMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE\r\nBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM\r\nGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP\r\nADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua\r\nBtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe\r\n3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4\r\nYgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR\r\nrOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm\r\nez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU\r\noBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF\r\nMAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v\r\nQUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t\r\nb2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF\r\nAAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q\r\nGE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz\r\nRt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2\r\nG9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi\r\nl2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3\r\nsmPi9WIsgtRqAEFQ8TmDn5XpNpaYbg==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=QuoVadis Root CA 2 O=QuoVadis Limited\r\n# Subject: CN=QuoVadis Root CA 2 O=QuoVadis Limited\r\n# Label: "QuoVadis Root CA 2"\r\n# Serial: 1289\r\n# MD5 Fingerprint: 5e:39:7b:dd:f8:ba:ec:82:e9:ac:62:ba:0c:54:00:2b\r\n# SHA1 Fingerprint: ca:3a:fb:cf:12:40:36:4b:44:b2:16:20:88:80:48:39:19:93:7c:f7\r\n# SHA256 Fingerprint: 85:a0:dd:7d:d7:20:ad:b7:ff:05:f8:3d:54:2b:20:9d:c7:ff:45:28:f7:d6:77:b1:83:89:fe:a5:e5:c4:9e:86\r\n-----BEGIN CERTIFICATE-----\r\nMIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x\r\nGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv\r\nb3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV\r\nBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W\r\nYWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa\r\nGMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg\r\nFyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J\r\nWpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB\r\nrrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp\r\n+ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1\r\nksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i\r\nUcw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz\r\nPtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og\r\n/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH\r\noycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI\r\nyV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud\r\nEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2\r\nA8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL\r\nMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT\r\nElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f\r\nBluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn\r\ng/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl\r\nfF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K\r\nWWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha\r\nB0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc\r\nhLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR\r\nTUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD\r\nmbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z\r\nohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y\r\n4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza\r\n8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=QuoVadis Root CA 3 O=QuoVadis Limited\r\n# Subject: CN=QuoVadis Root CA 3 O=QuoVadis Limited\r\n# Label: "QuoVadis Root CA 3"\r\n# Serial: 1478\r\n# MD5 Fingerprint: 31:85:3c:62:94:97:63:b9:aa:fd:89:4e:af:6f:e0:cf\r\n# SHA1 Fingerprint: 1f:49:14:f7:d8:74:95:1d:dd:ae:02:c0:be:fd:3a:2d:82:75:51:85\r\n# SHA256 Fingerprint: 18:f1:fc:7f:20:5d:f8:ad:dd:eb:7f:e0:07:dd:57:e3:af:37:5a:9c:4d:8d:73:54:6b:f4:f1:fe:d1:e1:8d:35\r\n-----BEGIN CERTIFICATE-----\r\nMIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x\r\nGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv\r\nb3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV\r\nBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W\r\nYWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM\r\nV0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNggDhoB\r\n4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUr\r\nH556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd\r\n8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv\r\nvWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLT\r\nmZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhe\r\nbtfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjc\r\nT5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDt\r\nWAEXMJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZ\r\nc6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A\r\n4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYD\r\nVR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMwgcUwgZMG\r\nCCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0\r\naXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0\r\naWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu\r\ndC4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2Nw\r\nczALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4G\r\nA1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJC\r\nTTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UEAxMSUXVvVmFkaXMg\r\nUm9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZVqyM0\r\n7ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSem\r\nd1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd\r\n+LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B\r\n4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadN\r\nt54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6x\r\nDYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57\r\nk8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6s\r\nzHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j\r\nWy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT\r\nmJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK\r\n4SVhM7JZG+Ju1zdXtg2pEto=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: O=SECOM Trust.net OU=Security Communication RootCA1\r\n# Subject: O=SECOM Trust.net OU=Security Communication RootCA1\r\n# Label: "Security Communication Root CA"\r\n# Serial: 0\r\n# MD5 Fingerprint: f1:bc:63:6a:54:e0:b5:27:f5:cd:e7:1a:e3:4d:6e:4a\r\n# SHA1 Fingerprint: 36:b1:2b:49:f9:81:9e:d7:4c:9e:bc:38:0f:c6:56:8f:5d:ac:b2:f7\r\n# SHA256 Fingerprint: e7:5e:72:ed:9f:56:0e:ec:6e:b4:80:00:73:a4:3f:c3:ad:19:19:5a:39:22:82:01:78:95:97:4a:99:02:6b:6c\r\n-----BEGIN CERTIFICATE-----\r\nMIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEY\r\nMBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21t\r\ndW5pY2F0aW9uIFJvb3RDQTEwHhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5\r\nWjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYD\r\nVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEwggEiMA0GCSqGSIb3\r\nDQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw8yl8\r\n9f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJ\r\nDKaVv0uMDPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9\r\nMs+k2Y7CI9eNqPPYJayX5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/N\r\nQV3Is00qVUarH9oe4kA92819uZKAnDfdDJZkndwi92SL32HeFZRSFaB9UslLqCHJ\r\nxrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2JChzAgMBAAGjPzA9MB0G\r\nA1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYwDwYDVR0T\r\nAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vG\r\nkl3g0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfr\r\nUj94nK9NrvjVT8+amCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5\r\nBw+SUEmK3TGXX8npN6o7WWWXlDLJs58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJU\r\nJRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ6rBK+1YWc26sTfcioU+tHXot\r\nRSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAiFL39vmwLAw==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com\r\n# Subject: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com\r\n# Label: "XRamp Global CA Root"\r\n# Serial: 107108908803651509692980124233745014957\r\n# MD5 Fingerprint: a1:0b:44:b3:ca:10:d8:00:6e:9d:0f:d8:0f:92:0a:d1\r\n# SHA1 Fingerprint: b8:01:86:d1:eb:9c:86:a5:41:04:cf:30:54:f3:4c:52:b7:e5:58:c6\r\n# SHA256 Fingerprint: ce:cd:dc:90:50:99:d8:da:df:c5:b1:d2:09:b7:37:cb:e2:c1:8c:fb:2c:10:c0:ff:0b:cf:0d:32:86:fc:1a:a2\r\n-----BEGIN CERTIFICATE-----\r\nMIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB\r\ngjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk\r\nMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY\r\nUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx\r\nNDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3\r\ndy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy\r\ndmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB\r\ndXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6\r\n38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP\r\nKZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q\r\nDxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4\r\nqEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa\r\nJSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi\r\nPvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P\r\nBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs\r\njVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0\r\neS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD\r\nggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR\r\nvbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt\r\nqZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa\r\nIR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy\r\ni6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ\r\nO+7ETPTsJ3xCwnR8gooJybQDJbw=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: O=The Go Daddy Group, Inc. OU=Go Daddy Class 2 Certification Authority\r\n# Subject: O=The Go Daddy Group, Inc. OU=Go Daddy Class 2 Certification Authority\r\n# Label: "Go Daddy Class 2 CA"\r\n# Serial: 0\r\n# MD5 Fingerprint: 91:de:06:25:ab:da:fd:32:17:0c:bb:25:17:2a:84:67\r\n# SHA1 Fingerprint: 27:96:ba:e6:3f:18:01:e2:77:26:1b:a0:d7:77:70:02:8f:20:ee:e4\r\n# SHA256 Fingerprint: c3:84:6b:f2:4b:9e:93:ca:64:27:4c:0e:c6:7c:1e:cc:5e:02:4f:fc:ac:d2:d7:40:19:35:0e:81:fe:54:6a:e4\r\n-----BEGIN CERTIFICATE-----\r\nMIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh\r\nMB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE\r\nYWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3\r\nMDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo\r\nZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg\r\nMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN\r\nADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA\r\nPVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w\r\nwdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi\r\nEqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY\r\navx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+\r\nYihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE\r\nsNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h\r\n/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5\r\nIEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj\r\nYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD\r\nggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy\r\nOO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P\r\nTMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ\r\nHmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER\r\ndEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf\r\nReYNnyicsbkqWletNw+vHX/bvZ8=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: O=Starfield Technologies, Inc. OU=Starfield Class 2 Certification Authority\r\n# Subject: O=Starfield Technologies, Inc. OU=Starfield Class 2 Certification Authority\r\n# Label: "Starfield Class 2 CA"\r\n# Serial: 0\r\n# MD5 Fingerprint: 32:4a:4b:bb:c8:63:69:9b:be:74:9a:c6:dd:1d:46:24\r\n# SHA1 Fingerprint: ad:7e:1c:28:b0:64:ef:8f:60:03:40:20:14:c3:d0:e3:37:0e:b5:8a\r\n# SHA256 Fingerprint: 14:65:fa:20:53:97:b8:76:fa:a6:f0:a9:95:8e:55:90:e4:0f:cc:7f:aa:4f:b7:c2:c8:67:75:21:fb:5f:b6:58\r\n-----BEGIN CERTIFICATE-----\r\nMIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl\r\nMCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp\r\nU3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw\r\nNjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE\r\nChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp\r\nZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3\r\nDQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf\r\n8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN\r\n+lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0\r\nX9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa\r\nK4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA\r\n1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G\r\nA1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR\r\nzt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0\r\nYXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD\r\nbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w\r\nDQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3\r\nL7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D\r\neruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl\r\nxy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp\r\nVSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY\r\nWQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com\r\n# Subject: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com\r\n# Label: "DigiCert Assured ID Root CA"\r\n# Serial: 17154717934120587862167794914071425081\r\n# MD5 Fingerprint: 87:ce:0b:7b:2a:0e:49:00:e1:58:71:9b:37:a8:93:72\r\n# SHA1 Fingerprint: 05:63:b8:63:0d:62:d7:5a:bb:c8:ab:1e:4b:df:b5:a8:99:b2:4d:43\r\n# SHA256 Fingerprint: 3e:90:99:b5:01:5e:8f:48:6c:00:bc:ea:9d:11:1e:e7:21:fa:ba:35:5a:89:bc:f1:df:69:56:1e:3d:c6:32:5c\r\n-----BEGIN CERTIFICATE-----\r\nMIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl\r\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\r\nd3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv\r\nb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG\r\nEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl\r\ncnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi\r\nMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c\r\nJpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP\r\nmDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+\r\nwRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4\r\nVYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/\r\nAUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB\r\nAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW\r\nBBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun\r\npyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC\r\ndWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf\r\nfwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm\r\nNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx\r\nH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe\r\n+o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com\r\n# Subject: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com\r\n# Label: "DigiCert Global Root CA"\r\n# Serial: 10944719598952040374951832963794454346\r\n# MD5 Fingerprint: 79:e4:a9:84:0d:7d:3a:96:d7:c0:4f:e2:43:4c:89:2e\r\n# SHA1 Fingerprint: a8:98:5d:3a:65:e5:e5:c4:b2:d7:d6:6d:40:c6:dd:2f:b1:9c:54:36\r\n# SHA256 Fingerprint: 43:48:a0:e9:44:4c:78:cb:26:5e:05:8d:5e:89:44:b4:d8:4f:96:62:bd:26:db:25:7f:89:34:a4:43:c7:01:61\r\n-----BEGIN CERTIFICATE-----\r\nMIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh\r\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\r\nd3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD\r\nQTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT\r\nMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\r\nb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG\r\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB\r\nCSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97\r\nnh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt\r\n43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P\r\nT19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4\r\ngdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO\r\nBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR\r\nTLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw\r\nDQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr\r\nhMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg\r\n06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF\r\nPnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls\r\nYSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk\r\nCAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com\r\n# Subject: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com\r\n# Label: "DigiCert High Assurance EV Root CA"\r\n# Serial: 3553400076410547919724730734378100087\r\n# MD5 Fingerprint: d4:74:de:57:5c:39:b2:d3:9c:85:83:c5:c0:65:49:8a\r\n# SHA1 Fingerprint: 5f:b7:ee:06:33:e2:59:db:ad:0c:4c:9a:e6:d3:8f:1a:61:c7:dc:25\r\n# SHA256 Fingerprint: 74:31:e5:f4:c3:c1:ce:46:90:77:4f:0b:61:e0:54:40:88:3b:a9:a0:1e:d0:0b:a6:ab:d7:80:6e:d3:b1:18:cf\r\n-----BEGIN CERTIFICATE-----\r\nMIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs\r\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\r\nd3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j\r\nZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL\r\nMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3\r\nLmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug\r\nRVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm\r\n+9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW\r\nPNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM\r\nxChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB\r\nIk5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3\r\nhzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg\r\nEsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF\r\nMAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA\r\nFLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec\r\nnzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z\r\neM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF\r\nhS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2\r\nYzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe\r\nvEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep\r\n+OkuE6N36B9K\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=SwissSign Gold CA - G2 O=SwissSign AG\r\n# Subject: CN=SwissSign Gold CA - G2 O=SwissSign AG\r\n# Label: "SwissSign Gold CA - G2"\r\n# Serial: 13492815561806991280\r\n# MD5 Fingerprint: 24:77:d9:a8:91:d1:3b:fa:88:2d:c2:ff:f8:cd:33:93\r\n# SHA1 Fingerprint: d8:c5:38:8a:b7:30:1b:1b:6e:d4:7a:e6:45:25:3a:6f:9f:1a:27:61\r\n# SHA256 Fingerprint: 62:dd:0b:e9:b9:f5:0a:16:3e:a0:f8:e7:5c:05:3b:1e:ca:57:ea:55:c8:68:8f:64:7c:68:81:f2:c8:35:7b:95\r\n-----BEGIN CERTIFICATE-----\r\nMIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV\r\nBAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln\r\nbiBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF\r\nMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT\r\nd2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC\r\nCgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8\r\n76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+\r\nbbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c\r\n6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE\r\nemA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd\r\nMmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt\r\nMDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y\r\nMszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y\r\nFGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi\r\naG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM\r\ngI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB\r\nqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7\r\nlqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn\r\n8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov\r\nL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6\r\n45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO\r\nUYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5\r\nO1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC\r\nbwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv\r\nGPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a\r\n77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC\r\nhdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3\r\n92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp\r\nLd6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w\r\nZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt\r\nQc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=SwissSign Silver CA - G2 O=SwissSign AG\r\n# Subject: CN=SwissSign Silver CA - G2 O=SwissSign AG\r\n# Label: "SwissSign Silver CA - G2"\r\n# Serial: 5700383053117599563\r\n# MD5 Fingerprint: e0:06:a1:c9:7d:cf:c9:fc:0d:c0:56:75:96:d8:62:13\r\n# SHA1 Fingerprint: 9b:aa:e5:9f:56:ee:21:cb:43:5a:be:25:93:df:a7:f0:40:d1:1d:cb\r\n# SHA256 Fingerprint: be:6c:4d:a2:bb:b9:ba:59:b6:f3:93:97:68:37:42:46:c3:c0:05:99:3f:a9:8f:02:0d:1d:ed:be:d4:8a:81:d5\r\n-----BEGIN CERTIFICATE-----\r\nMIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UE\r\nBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWdu\r\nIFNpbHZlciBDQSAtIEcyMB4XDTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0Nlow\r\nRzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMY\r\nU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A\r\nMIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644N0Mv\r\nFz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7br\r\nYT7QbNHm+/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieF\r\nnbAVlDLaYQ1HTWBCrpJH6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH\r\n6ATK72oxh9TAtvmUcXtnZLi2kUpCe2UuMGoM9ZDulebyzYLs2aFK7PayS+VFheZt\r\neJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5hqAaEuSh6XzjZG6k4sIN/\r\nc8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5FZGkECwJ\r\nMoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRH\r\nHTBsROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTf\r\njNFusB3hB48IHpmccelM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb6\r\n5i/4z3GcRm25xBWNOHkDRUjvxF3XCO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOB\r\nrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU\r\nF6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRBtjpbO8tFnb0c\r\nwpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0\r\ncDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIB\r\nAHPGgeAn0i0P4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShp\r\nWJHckRE1qTodvBqlYJ7YH39FkWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9\r\nxCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L3XWgwF15kIwb4FDm3jH+mHtwX6WQ\r\n2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx/uNncqCxv1yL5PqZ\r\nIseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFaDGi8\r\naRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2X\r\nem1ZqSqPe97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQR\r\ndAtq/gsD/KNVV4n+SsuuWxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/\r\nOMpXEA29MC/HpeZBoNquBYeaoKRlbEwJDIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+\r\nhAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ubDgEj8Z+7fNzcbBGXJbLy\r\ntGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=SecureTrust CA O=SecureTrust Corporation\r\n# Subject: CN=SecureTrust CA O=SecureTrust Corporation\r\n# Label: "SecureTrust CA"\r\n# Serial: 17199774589125277788362757014266862032\r\n# MD5 Fingerprint: dc:32:c3:a7:6d:25:57:c7:68:09:9d:ea:2d:a9:a2:d1\r\n# SHA1 Fingerprint: 87:82:c6:c3:04:35:3b:cf:d2:96:92:d2:59:3e:7d:44:d9:34:ff:11\r\n# SHA256 Fingerprint: f1:c1:b5:0a:e5:a2:0d:d8:03:0e:c9:f6:bc:24:82:3d:d3:67:b5:25:57:59:b4:e7:1b:61:fc:e9:f7:37:5d:73\r\n-----BEGIN CERTIFICATE-----\r\nMIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI\r\nMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x\r\nFzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz\r\nMTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv\r\ncnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN\r\nAQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz\r\nZum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO\r\n0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao\r\nwW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj\r\n7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS\r\n8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT\r\nBgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB\r\n/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg\r\nJYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC\r\nNxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3\r\n6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/\r\n3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm\r\nD5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS\r\nCPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR\r\n3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Secure Global CA O=SecureTrust Corporation\r\n# Subject: CN=Secure Global CA O=SecureTrust Corporation\r\n# Label: "Secure Global CA"\r\n# Serial: 9751836167731051554232119481456978597\r\n# MD5 Fingerprint: cf:f4:27:0d:d4:ed:dc:65:16:49:6d:3d:da:bf:6e:de\r\n# SHA1 Fingerprint: 3a:44:73:5a:e5:81:90:1f:24:86:61:46:1e:3b:9c:c4:5f:f5:3a:1b\r\n# SHA256 Fingerprint: 42:00:f5:04:3a:c8:59:0e:bb:52:7d:20:9e:d1:50:30:29:fb:cb:d4:1c:a1:b5:06:ec:27:f1:5a:de:7d:ac:69\r\n-----BEGIN CERTIFICATE-----\r\nMIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK\r\nMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x\r\nGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx\r\nMjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg\r\nQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG\r\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ\r\niQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa\r\n/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ\r\njnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI\r\nHmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7\r\nsFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w\r\ngZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF\r\nMAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw\r\nKaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG\r\nAQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L\r\nURYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO\r\nH0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm\r\nI50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY\r\niNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc\r\nf8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=COMODO Certification Authority O=COMODO CA Limited\r\n# Subject: CN=COMODO Certification Authority O=COMODO CA Limited\r\n# Label: "COMODO Certification Authority"\r\n# Serial: 104350513648249232941998508985834464573\r\n# MD5 Fingerprint: 5c:48:dc:f7:42:72:ec:56:94:6d:1c:cc:71:35:80:75\r\n# SHA1 Fingerprint: 66:31:bf:9e:f7:4f:9e:b6:c9:d5:a6:0c:ba:6a:be:d1:f7:bd:ef:7b\r\n# SHA256 Fingerprint: 0c:2c:d6:3d:f7:80:6f:a3:99:ed:e8:09:11:6b:57:5b:f8:79:89:f0:65:18:f9:80:8c:86:05:03:17:8b:af:66\r\n-----BEGIN CERTIFICATE-----\r\nMIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB\r\ngTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\r\nA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV\r\nBAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw\r\nMDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl\r\nYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P\r\nRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0\r\naG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3\r\nUcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI\r\n2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8\r\nQ5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp\r\n+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+\r\nDT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O\r\nnKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW\r\n/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g\r\nPKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u\r\nQXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY\r\nSdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv\r\nIC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/\r\nRxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4\r\nzJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd\r\nBA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB\r\nZQ==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Network Solutions Certificate Authority O=Network Solutions L.L.C.\r\n# Subject: CN=Network Solutions Certificate Authority O=Network Solutions L.L.C.\r\n# Label: "Network Solutions Certificate Authority"\r\n# Serial: 116697915152937497490437556386812487904\r\n# MD5 Fingerprint: d3:f3:a6:16:c0:fa:6b:1d:59:b1:2d:96:4d:0e:11:2e\r\n# SHA1 Fingerprint: 74:f8:a3:c3:ef:e7:b3:90:06:4b:83:90:3c:21:64:60:20:e5:df:ce\r\n# SHA256 Fingerprint: 15:f0:ba:00:a3:ac:7a:f3:ac:88:4c:07:2b:10:11:a0:77:bd:77:c0:97:f4:01:64:b2:f8:59:8a:bd:83:86:0c\r\n-----BEGIN CERTIFICATE-----\r\nMIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBi\r\nMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu\r\nMTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3Jp\r\ndHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMxMjM1OTU5WjBiMQswCQYDVQQGEwJV\r\nUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydO\r\nZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0GCSqG\r\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwz\r\nc7MEL7xxjOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPP\r\nOCwGJgl6cvf6UDL4wpPTaaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rl\r\nmGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXTcrA/vGp97Eh/jcOrqnErU2lBUzS1sLnF\r\nBgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc/Qzpf14Dl847ABSHJ3A4\r\nqY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMBAAGjgZcw\r\ngZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIB\r\nBjAPBgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwu\r\nbmV0c29sc3NsLmNvbS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3Jp\r\ndHkuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc8\r\n6fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q4LqILPxFzBiwmZVRDuwduIj/\r\nh1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/GGUsyfJj4akH\r\n/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv\r\nwKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHN\r\npGxlaKFJdlxDydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=COMODO ECC Certification Authority O=COMODO CA Limited\r\n# Subject: CN=COMODO ECC Certification Authority O=COMODO CA Limited\r\n# Label: "COMODO ECC Certification Authority"\r\n# Serial: 41578283867086692638256921589707938090\r\n# MD5 Fingerprint: 7c:62:ff:74:9d:31:53:5e:68:4a:d5:78:aa:1e:bf:23\r\n# SHA1 Fingerprint: 9f:74:4e:9f:2b:4d:ba:ec:0f:31:2c:50:b6:56:3b:8e:2d:93:c3:11\r\n# SHA256 Fingerprint: 17:93:92:7a:06:14:54:97:89:ad:ce:2f:8f:34:f7:f0:b6:6d:0f:3a:e3:a3:b8:4d:21:ec:15:db:ba:4f:ad:c7\r\n-----BEGIN CERTIFICATE-----\r\nMIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL\r\nMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE\r\nBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT\r\nIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw\r\nMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy\r\nZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N\r\nT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv\r\nbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR\r\nFtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J\r\ncfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW\r\nBBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/\r\nBAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm\r\nfQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv\r\nGDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Certigna O=Dhimyotis\r\n# Subject: CN=Certigna O=Dhimyotis\r\n# Label: "Certigna"\r\n# Serial: 18364802974209362175\r\n# MD5 Fingerprint: ab:57:a6:5b:7d:42:82:19:b5:d8:58:26:28:5e:fd:ff\r\n# SHA1 Fingerprint: b1:2e:13:63:45:86:a4:6f:1a:b2:60:68:37:58:2d:c4:ac:fd:94:97\r\n# SHA256 Fingerprint: e3:b6:a2:db:2e:d7:ce:48:84:2f:7a:c5:32:41:c7:b7:1d:54:14:4b:fb:40:c1:1f:3f:1d:0b:42:f5:ee:a1:2d\r\n-----BEGIN CERTIFICATE-----\r\nMIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV\r\nBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X\r\nDTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ\r\nBgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3\r\nDQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4\r\nQCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny\r\ngQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw\r\nzBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q\r\n130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2\r\nJsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw\r\nDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw\r\nZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT\r\nAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj\r\nAQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG\r\n9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h\r\nbV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc\r\nfca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu\r\nHWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w\r\nt0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw\r\nWyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Cybertrust Global Root O=Cybertrust, Inc\r\n# Subject: CN=Cybertrust Global Root O=Cybertrust, Inc\r\n# Label: "Cybertrust Global Root"\r\n# Serial: 4835703278459682877484360\r\n# MD5 Fingerprint: 72:e4:4a:87:e3:69:40:80:77:ea:bc:e3:f4:ff:f0:e1\r\n# SHA1 Fingerprint: 5f:43:e5:b1:bf:f8:78:8c:ac:1c:c7:ca:4a:9a:c6:22:2b:cc:34:c6\r\n# SHA256 Fingerprint: 96:0a:df:00:63:e9:63:56:75:0c:29:65:dd:0a:08:67:da:0b:9c:bd:6e:77:71:4a:ea:fb:23:49:ab:39:3d:a3\r\n-----BEGIN CERTIFICATE-----\r\nMIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYG\r\nA1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2Jh\r\nbCBSb290MB4XDTA2MTIxNTA4MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UE\r\nChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBS\r\nb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+Mi8vRRQZhP/8NN5\r\n7CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW0ozS\r\nJ8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2y\r\nHLtgwEZLAfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iP\r\nt3sMpTjr3kfb1V05/Iin89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNz\r\nFtApD0mpSPCzqrdsxacwOUBdrsTiXSZT8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAY\r\nXSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/\r\nMB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2MDSgMqAw\r\nhi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3Js\r\nMB8GA1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUA\r\nA4IBAQBW7wojoFROlZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMj\r\nWqd8BfP9IjsO0QbE2zZMcwSO5bAi5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUx\r\nXOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2hO0j9n0Hq0V+09+zv+mKts2o\r\nomcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+TX3EJIrduPuoc\r\nA06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW\r\nWL1WMRJOEcgh4LMRkWXbtKaIOM5V\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: O=Chunghwa Telecom Co., Ltd. OU=ePKI Root Certification Authority\r\n# Subject: O=Chunghwa Telecom Co., Ltd. OU=ePKI Root Certification Authority\r\n# Label: "ePKI Root Certification Authority"\r\n# Serial: 28956088682735189655030529057352760477\r\n# MD5 Fingerprint: 1b:2e:00:ca:26:06:90:3d:ad:fe:6f:15:68:d3:6b:b3\r\n# SHA1 Fingerprint: 67:65:0d:f1:7e:8e:7e:5b:82:40:a4:f4:56:4b:cf:e2:3d:69:c6:f0\r\n# SHA256 Fingerprint: c0:a6:f4:dc:63:a2:4b:fd:cf:54:ef:2a:6a:08:2a:0a:72:de:35:80:3e:2f:f5:ff:52:7a:e5:d8:72:06:df:d5\r\n-----BEGIN CERTIFICATE-----\r\nMIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe\r\nMQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0\r\nZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe\r\nFw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw\r\nIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL\r\nSSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF\r\nAAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH\r\nSyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh\r\nijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X\r\nDZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1\r\nTBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ\r\nfzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA\r\nsgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU\r\nWH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS\r\nnT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH\r\ndmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip\r\nNiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC\r\nAwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF\r\nMAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH\r\nClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB\r\nuvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl\r\nPwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP\r\nJXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/\r\ngpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2\r\nj6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6\r\n5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB\r\no2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS\r\n/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z\r\nGp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE\r\nW9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D\r\nhNQ+IIX3Sj0rnP0qCglN6oH4EZw=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: O=certSIGN OU=certSIGN ROOT CA\r\n# Subject: O=certSIGN OU=certSIGN ROOT CA\r\n# Label: "certSIGN ROOT CA"\r\n# Serial: 35210227249154\r\n# MD5 Fingerprint: 18:98:c0:d6:e9:3a:fc:f9:b0:f5:0c:f7:4b:01:44:17\r\n# SHA1 Fingerprint: fa:b7:ee:36:97:26:62:fb:2d:b0:2a:f6:bf:03:fd:e8:7c:4b:2f:9b\r\n# SHA256 Fingerprint: ea:a9:62:c4:fa:4a:6b:af:eb:e4:15:19:6d:35:1c:cd:88:8d:4f:53:f3:fa:8a:e6:d7:c4:66:a9:4e:60:42:bb\r\n-----BEGIN CERTIFICATE-----\r\nMIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT\r\nAlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD\r\nQTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP\r\nMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC\r\nASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do\r\n0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ\r\nUySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d\r\nRdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ\r\nOA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv\r\nJoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C\r\nAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O\r\nBBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ\r\nLjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY\r\nMnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ\r\n44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I\r\nJd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw\r\ni/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN\r\n9u6wWk5JRFRYX0KD\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=NetLock Arany (Class Gold) F\xc5\x91tan\xc3\xbas\xc3\xadtv\xc3\xa1ny O=NetLock Kft. OU=Tan\xc3\xbas\xc3\xadtv\xc3\xa1nykiad\xc3\xb3k (Certification Services)\r\n# Subject: CN=NetLock Arany (Class Gold) F\xc5\x91tan\xc3\xbas\xc3\xadtv\xc3\xa1ny O=NetLock Kft. OU=Tan\xc3\xbas\xc3\xadtv\xc3\xa1nykiad\xc3\xb3k (Certification Services)\r\n# Label: "NetLock Arany (Class Gold) F\xc5\x91tan\xc3\xbas\xc3\xadtv\xc3\xa1ny"\r\n# Serial: 80544274841616\r\n# MD5 Fingerprint: c5:a1:b7:ff:73:dd:d6:d7:34:32:18:df:fc:3c:ad:88\r\n# SHA1 Fingerprint: 06:08:3f:59:3f:15:a1:04:a0:69:a4:6b:a9:03:d0:06:b7:97:09:91\r\n# SHA256 Fingerprint: 6c:61:da:c3:a2:de:f0:31:50:6b:e0:36:d2:a6:fe:40:19:94:fb:d1:3d:f9:c8:d4:66:59:92:74:c4:46:ec:98\r\n-----BEGIN CERTIFICATE-----\r\nMIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG\r\nEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3\r\nMDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl\r\ncnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR\r\ndGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB\r\npzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM\r\nb2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm\r\naWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz\r\nIEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\r\nMIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT\r\nlF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz\r\nAZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5\r\nVA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG\r\nILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2\r\nBJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG\r\nAQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M\r\nU9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh\r\nbvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C\r\n+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC\r\nbLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F\r\nuLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2\r\nXjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Hongkong Post Root CA 1 O=Hongkong Post\r\n# Subject: CN=Hongkong Post Root CA 1 O=Hongkong Post\r\n# Label: "Hongkong Post Root CA 1"\r\n# Serial: 1000\r\n# MD5 Fingerprint: a8:0d:6f:39:78:b9:43:6d:77:42:6d:98:5a:cc:23:ca\r\n# SHA1 Fingerprint: d6:da:a8:20:8d:09:d2:15:4d:24:b5:2f:cb:34:6e:b2:58:b2:8a:58\r\n# SHA256 Fingerprint: f9:e6:7d:33:6c:51:00:2a:c0:54:c6:32:02:2d:66:dd:a2:e7:e3:ff:f1:0a:d0:61:ed:31:d8:bb:b4:10:cf:b2\r\n-----BEGIN CERTIFICATE-----\r\nMIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsx\r\nFjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3Qg\r\nUm9vdCBDQSAxMB4XDTAzMDUxNTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkG\r\nA1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdr\r\nb25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\r\nAQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1ApzQ\r\njVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEn\r\nPzlTCeqrauh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjh\r\nZY4bXSNmO7ilMlHIhqqhqZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9\r\nnnV0ttgCXjqQesBCNnLsak3c78QA3xMYV18meMjWCnl3v/evt3a5pQuEF10Q6m/h\r\nq5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNVHRMBAf8ECDAGAQH/AgED\r\nMA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7ih9legYsC\r\nmEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI3\r\n7piol7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clB\r\noiMBdDhViw+5LmeiIAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJs\r\nEhTkYY2sEJCehFC78JZvRZ+K88psT/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpO\r\nfMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilTc4afU9hDDl3WY4JxHYB0yvbi\r\nAmvZWg==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=SecureSign RootCA11 O=Japan Certification Services, Inc.\r\n# Subject: CN=SecureSign RootCA11 O=Japan Certification Services, Inc.\r\n# Label: "SecureSign RootCA11"\r\n# Serial: 1\r\n# MD5 Fingerprint: b7:52:74:e2:92:b4:80:93:f2:75:e4:cc:d7:f2:ea:26\r\n# SHA1 Fingerprint: 3b:c4:9f:48:f8:f3:73:a0:9c:1e:bd:f8:5b:b1:c3:65:c7:d8:11:b3\r\n# SHA256 Fingerprint: bf:0f:ee:fb:9e:3a:58:1a:d5:f9:e9:db:75:89:98:57:43:d2:61:08:5c:4d:31:4f:6f:5d:72:59:aa:42:16:12\r\n-----BEGIN CERTIFICATE-----\r\nMIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDEr\r\nMCkGA1UEChMiSmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoG\r\nA1UEAxMTU2VjdXJlU2lnbiBSb290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0\r\nMDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSswKQYDVQQKEyJKYXBhbiBDZXJ0aWZp\r\nY2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1cmVTaWduIFJvb3RD\r\nQTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvLTJsz\r\ni1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8\r\nh9uuywGOwvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOV\r\nMdrAG/LuYpmGYz+/3ZMqg6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9\r\nUK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rPO7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni\r\n8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitAbpSACW22s293bzUIUPsC\r\nh8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZXt94wDgYD\r\nVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEB\r\nAKChOBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xm\r\nKbabfSVSSUOrTC4rbnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQ\r\nX5Ucv+2rIrVls4W6ng+4reV6G4pQOh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWr\r\nQbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01y8hSyn+B/tlr0/cR7SXf+Of5\r\npPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061lgeLKBObjBmN\r\nQSdJQO7e5iNEOdyhIta6A/I=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd.\r\n# Subject: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd.\r\n# Label: "Microsec e-Szigno Root CA 2009"\r\n# Serial: 14014712776195784473\r\n# MD5 Fingerprint: f8:49:f4:03:bc:44:2d:83:be:48:69:7d:29:64:fc:b1\r\n# SHA1 Fingerprint: 89:df:74:fe:5c:f4:0f:4a:80:f9:e3:37:7d:54:da:91:e1:01:31:8e\r\n# SHA256 Fingerprint: 3c:5f:81:fe:a5:fa:b8:2c:64:bf:a2:ea:ec:af:cd:e8:e0:77:fc:86:20:a7:ca:e5:37:16:3d:f3:6e:db:f3:78\r\n-----BEGIN CERTIFICATE-----\r\nMIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD\r\nVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0\r\nZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G\r\nCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y\r\nOTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx\r\nFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp\r\nZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o\r\ndTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP\r\nkd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc\r\ncbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U\r\nfIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7\r\nN4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC\r\nxkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1\r\n+rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G\r\nA1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM\r\nPcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG\r\nSIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h\r\nmLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk\r\nddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775\r\ntyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c\r\n2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t\r\nHMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3\r\n# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3\r\n# Label: "GlobalSign Root CA - R3"\r\n# Serial: 4835703278459759426209954\r\n# MD5 Fingerprint: c5:df:b8:49:ca:05:13:55:ee:2d:ba:1a:c3:3e:b0:28\r\n# SHA1 Fingerprint: d6:9b:56:11:48:f0:1c:77:c5:45:78:c1:09:26:df:5b:85:69:76:ad\r\n# SHA256 Fingerprint: cb:b5:22:d7:b7:f1:27:ad:6a:01:13:86:5b:df:1c:d4:10:2e:7d:07:59:af:63:5a:7c:f4:72:0d:c9:63:c5:3b\r\n-----BEGIN CERTIFICATE-----\r\nMIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G\r\nA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp\r\nZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4\r\nMTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG\r\nA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI\r\nhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8\r\nRgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT\r\ngHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm\r\nKPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd\r\nQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ\r\nXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw\r\nDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o\r\nLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU\r\nRUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp\r\njjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK\r\n6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX\r\nmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs\r\nMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH\r\nWD9f\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068\r\n# Subject: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068\r\n# Label: "Autoridad de Certificacion Firmaprofesional CIF A62634068"\r\n# Serial: 6047274297262753887\r\n# MD5 Fingerprint: 73:3a:74:7a:ec:bb:a3:96:a6:c2:e4:e2:c8:9b:c0:c3\r\n# SHA1 Fingerprint: ae:c5:fb:3f:c8:e1:bf:c4:e5:4f:03:07:5a:9a:e8:00:b7:f7:b6:fa\r\n# SHA256 Fingerprint: 04:04:80:28:bf:1f:28:64:d4:8f:9a:d4:d8:32:94:36:6a:82:88:56:55:3f:3b:14:30:3f:90:14:7f:5d:40:ef\r\n-----BEGIN CERTIFICATE-----\r\nMIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UE\r\nBhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h\r\ncHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEy\r\nMzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg\r\nQ2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi\r\nMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9\r\nthDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM\r\ncas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG\r\nL9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i\r\nNA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h\r\nX68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b\r\nm8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy\r\nZ/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja\r\nEbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T\r\nKI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF\r\n6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh\r\nOSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYD\r\nVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNHDhpkLzCBpgYD\r\nVR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp\r\ncm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBv\r\nACAAZABlACAAbABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBl\r\nAGwAbwBuAGEAIAAwADgAMAAxADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF\r\n661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx51tkljYyGOylMnfX40S2wBEqgLk9\r\nam58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qkR71kMrv2JYSiJ0L1\r\nILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaPT481\r\nPyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS\r\n3a/DTg4fJl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5k\r\nSeTy36LssUzAKh3ntLFlosS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF\r\n3dvd6qJ2gHN99ZwExEWN57kci57q13XRcrHedUTnQn3iV2t93Jm8PYMo6oCTjcVM\r\nZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoRsaS8I8nkvof/uZS2+F0g\r\nStRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTDKCOM/icz\r\nQ0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQB\r\njLMi6Et8Vcad+qMUu2WFbm5PEn4KPJ2V\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Izenpe.com O=IZENPE S.A.\r\n# Subject: CN=Izenpe.com O=IZENPE S.A.\r\n# Label: "Izenpe.com"\r\n# Serial: 917563065490389241595536686991402621\r\n# MD5 Fingerprint: a6:b0:cd:85:80:da:5c:50:34:a3:39:90:2f:55:67:73\r\n# SHA1 Fingerprint: 2f:78:3d:25:52:18:a7:4a:65:39:71:b5:2c:a2:9c:45:15:6f:e9:19\r\n# SHA256 Fingerprint: 25:30:cc:8e:98:32:15:02:ba:d9:6f:9b:1f:ba:1b:09:9e:2d:29:9e:0f:45:48:bb:91:4f:36:3b:c0:d4:53:1f\r\n-----BEGIN CERTIFICATE-----\r\nMIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4\r\nMQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6\r\nZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD\r\nVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j\r\nb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq\r\nscIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO\r\nxdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H\r\nLmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX\r\nuaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD\r\nyCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+\r\nJrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q\r\nrLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN\r\nBjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L\r\nhij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB\r\nQFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+\r\nHMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu\r\nZm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg\r\nQTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB\r\nBgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx\r\nMCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\r\nAQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA\r\nA4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb\r\nlaQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56\r\nawmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo\r\nJNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw\r\nLDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT\r\nVyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk\r\nLhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb\r\nUjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/\r\nQnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+\r\nnaM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls\r\nQyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc.\r\n# Subject: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc.\r\n# Label: "Go Daddy Root Certificate Authority - G2"\r\n# Serial: 0\r\n# MD5 Fingerprint: 80:3a:bc:22:c1:e6:fb:8d:9b:3b:27:4a:32:1b:9a:01\r\n# SHA1 Fingerprint: 47:be:ab:c9:22:ea:e8:0e:78:78:34:62:a7:9f:45:c2:54:fd:e6:8b\r\n# SHA256 Fingerprint: 45:14:0b:32:47:eb:9c:c8:c5:b4:f0:d7:b5:30:91:f7:32:92:08:9e:6e:5a:63:e2:74:9d:d3:ac:a9:19:8e:da\r\n-----BEGIN CERTIFICATE-----\r\nMIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx\r\nEDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT\r\nEUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp\r\nZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz\r\nNTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH\r\nEwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE\r\nAxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw\r\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD\r\nE6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH\r\n/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy\r\nDfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh\r\nGkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR\r\ntDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA\r\nAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE\r\nFDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX\r\nWWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu\r\n9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr\r\ngIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo\r\n2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO\r\nLPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI\r\n4uJEvlz36hz1\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc.\r\n# Subject: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc.\r\n# Label: "Starfield Root Certificate Authority - G2"\r\n# Serial: 0\r\n# MD5 Fingerprint: d6:39:81:c6:52:7e:96:69:fc:fc:ca:66:ed:05:f2:96\r\n# SHA1 Fingerprint: b5:1c:06:7c:ee:2b:0c:3d:f8:55:ab:2d:92:f4:fe:39:d4:e7:0f:0e\r\n# SHA256 Fingerprint: 2c:e1:cb:0b:f9:d2:f9:e1:02:99:3f:be:21:51:52:c3:b2:dd:0c:ab:de:1c:68:e5:31:9b:83:91:54:db:b7:f5\r\n-----BEGIN CERTIFICATE-----\r\nMIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx\r\nEDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT\r\nHFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs\r\nZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw\r\nMFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6\r\nb25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj\r\naG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp\r\nY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\r\nggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg\r\nnLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1\r\nHOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N\r\nHwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN\r\ndloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0\r\nHZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO\r\nBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G\r\nCSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU\r\nsHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3\r\n4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg\r\n8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K\r\npL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1\r\nmMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc.\r\n# Subject: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc.\r\n# Label: "Starfield Services Root Certificate Authority - G2"\r\n# Serial: 0\r\n# MD5 Fingerprint: 17:35:74:af:7b:61:1c:eb:f4:f9:3c:e2:ee:40:f9:a2\r\n# SHA1 Fingerprint: 92:5a:8f:8d:2c:6d:04:e0:66:5f:59:6a:ff:22:d8:63:e8:25:6f:3f\r\n# SHA256 Fingerprint: 56:8d:69:05:a2:c8:87:08:a4:b3:02:51:90:ed:cf:ed:b1:97:4a:60:6a:13:c6:e5:29:0f:cb:2a:e6:3e:da:b5\r\n-----BEGIN CERTIFICATE-----\r\nMIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx\r\nEDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT\r\nHFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs\r\nZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5\r\nMDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD\r\nVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy\r\nZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy\r\ndmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI\r\nhvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p\r\nOsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2\r\n8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K\r\nTs9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe\r\nhRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk\r\n6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw\r\nDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q\r\nAdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI\r\nbw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB\r\nve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z\r\nqwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd\r\niEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn\r\n0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN\r\nsSi6\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=AffirmTrust Commercial O=AffirmTrust\r\n# Subject: CN=AffirmTrust Commercial O=AffirmTrust\r\n# Label: "AffirmTrust Commercial"\r\n# Serial: 8608355977964138876\r\n# MD5 Fingerprint: 82:92:ba:5b:ef:cd:8a:6f:a6:3d:55:f9:84:f6:d6:b7\r\n# SHA1 Fingerprint: f9:b5:b6:32:45:5f:9c:be:ec:57:5f:80:dc:e9:6e:2c:c7:b2:78:b7\r\n# SHA256 Fingerprint: 03:76:ab:1d:54:c5:f9:80:3c:e4:b2:e2:01:a0:ee:7e:ef:7b:57:b6:36:e8:a9:3c:9b:8d:48:60:c9:6f:5f:a7\r\n-----BEGIN CERTIFICATE-----\r\nMIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE\r\nBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz\r\ndCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL\r\nMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp\r\ncm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\r\nAQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP\r\nHx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr\r\nba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL\r\nMeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1\r\nyHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr\r\nVwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/\r\nnx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ\r\nKoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG\r\nXUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj\r\nvbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt\r\nZ8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g\r\nN53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC\r\nnlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=AffirmTrust Networking O=AffirmTrust\r\n# Subject: CN=AffirmTrust Networking O=AffirmTrust\r\n# Label: "AffirmTrust Networking"\r\n# Serial: 8957382827206547757\r\n# MD5 Fingerprint: 42:65:ca:be:01:9a:9a:4c:a9:8c:41:49:cd:c0:d5:7f\r\n# SHA1 Fingerprint: 29:36:21:02:8b:20:ed:02:f5:66:c5:32:d1:d6:ed:90:9f:45:00:2f\r\n# SHA256 Fingerprint: 0a:81:ec:5a:92:97:77:f1:45:90:4a:f3:8d:5d:50:9f:66:b5:e2:c5:8f:cd:b5:31:05:8b:0e:17:f3:f0:b4:1b\r\n-----BEGIN CERTIFICATE-----\r\nMIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE\r\nBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz\r\ndCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL\r\nMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp\r\ncm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\r\nAQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y\r\nYJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua\r\nkCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL\r\nQESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp\r\n6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG\r\nyH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i\r\nQLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ\r\nKoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO\r\ntDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu\r\nQY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ\r\nLgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u\r\nolu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48\r\nx3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=AffirmTrust Premium O=AffirmTrust\r\n# Subject: CN=AffirmTrust Premium O=AffirmTrust\r\n# Label: "AffirmTrust Premium"\r\n# Serial: 7893706540734352110\r\n# MD5 Fingerprint: c4:5d:0e:48:b6:ac:28:30:4e:0a:bc:f9:38:16:87:57\r\n# SHA1 Fingerprint: d8:a6:33:2c:e0:03:6f:b1:85:f6:63:4f:7d:6a:06:65:26:32:28:27\r\n# SHA256 Fingerprint: 70:a7:3f:7f:37:6b:60:07:42:48:90:45:34:b1:14:82:d5:bf:0e:69:8e:cc:49:8d:f5:25:77:eb:f2:e9:3b:9a\r\n-----BEGIN CERTIFICATE-----\r\nMIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE\r\nBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz\r\ndCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG\r\nA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U\r\ncnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf\r\nqV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ\r\nJG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ\r\n+jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS\r\ns8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5\r\nHMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7\r\n70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG\r\nV+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S\r\nqHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S\r\n5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia\r\nC1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX\r\nOwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE\r\nFJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/\r\nBAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2\r\nKI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg\r\nNt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B\r\n8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ\r\nMKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc\r\n0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ\r\nu4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF\r\nu+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH\r\nYoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8\r\nGKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO\r\nRtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e\r\nKeC2uAloGRwYQw==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=AffirmTrust Premium ECC O=AffirmTrust\r\n# Subject: CN=AffirmTrust Premium ECC O=AffirmTrust\r\n# Label: "AffirmTrust Premium ECC"\r\n# Serial: 8401224907861490260\r\n# MD5 Fingerprint: 64:b0:09:55:cf:b1:d5:99:e2:be:13:ab:a6:5d:ea:4d\r\n# SHA1 Fingerprint: b8:23:6b:00:2f:1d:16:86:53:01:55:6c:11:a4:37:ca:eb:ff:c3:bb\r\n# SHA256 Fingerprint: bd:71:fd:f6:da:97:e4:cf:62:d1:64:7a:dd:25:81:b0:7d:79:ad:f8:39:7e:b4:ec:ba:9c:5e:84:88:82:14:23\r\n-----BEGIN CERTIFICATE-----\r\nMIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC\r\nVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ\r\ncmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ\r\nBgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt\r\nVHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D\r\n0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9\r\nss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G\r\nA1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G\r\nA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs\r\naobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I\r\nflc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority\r\n# Subject: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority\r\n# Label: "Certum Trusted Network CA"\r\n# Serial: 279744\r\n# MD5 Fingerprint: d5:e9:81:40:c5:18:69:fc:46:2c:89:75:62:0f:aa:78\r\n# SHA1 Fingerprint: 07:e0:32:e0:20:b7:2c:3f:19:2f:06:28:a2:59:3a:19:a7:0f:06:9e\r\n# SHA256 Fingerprint: 5c:58:46:8d:55:f5:8e:49:7e:74:39:82:d2:b5:00:10:b6:d1:65:37:4a:cf:83:a7:d4:a3:2d:b7:68:c4:40:8e\r\n-----BEGIN CERTIFICATE-----\r\nMIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM\r\nMSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D\r\nZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU\r\ncnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3\r\nWjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg\r\nUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw\r\nIAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B\r\nAQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH\r\nUV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM\r\nTXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU\r\nBBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM\r\nkUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x\r\nAcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV\r\nHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV\r\nHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y\r\nsHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL\r\nI9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8\r\nJ9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY\r\nVoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI\r\n03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA\r\n# Subject: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA\r\n# Label: "TWCA Root Certification Authority"\r\n# Serial: 1\r\n# MD5 Fingerprint: aa:08:8f:f6:f9:7b:b7:f2:b1:a7:1e:9b:ea:ea:bd:79\r\n# SHA1 Fingerprint: cf:9e:87:6d:d3:eb:fc:42:26:97:a3:b5:a3:7a:a0:76:a9:06:23:48\r\n# SHA256 Fingerprint: bf:d8:8f:e1:10:1c:41:ae:3e:80:1b:f8:be:56:35:0e:e9:ba:d1:a6:b9:bd:51:5e:dc:5c:6d:5b:87:11:ac:44\r\n-----BEGIN CERTIFICATE-----\r\nMIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES\r\nMBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU\r\nV0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz\r\nWhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO\r\nLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm\r\naWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB\r\nAQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE\r\nAcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH\r\nK3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX\r\nRfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z\r\nrX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx\r\n3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\r\nHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq\r\nhkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC\r\nMErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls\r\nXebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D\r\nlhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn\r\naspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ\r\nYiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2\r\n# Subject: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2\r\n# Label: "Security Communication RootCA2"\r\n# Serial: 0\r\n# MD5 Fingerprint: 6c:39:7d:a4:0e:55:59:b2:3f:d6:41:b1:12:50:de:43\r\n# SHA1 Fingerprint: 5f:3b:8c:f2:f8:10:b3:7d:78:b4:ce:ec:19:19:c3:73:34:b9:c7:74\r\n# SHA256 Fingerprint: 51:3b:2c:ec:b8:10:d4:cd:e5:dd:85:39:1a:df:c6:c2:dd:60:d8:7b:b7:36:d2:b5:21:48:4a:a4:7a:0e:be:f6\r\n-----BEGIN CERTIFICATE-----\r\nMIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl\r\nMCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe\r\nU2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX\r\nDTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy\r\ndXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj\r\nYXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV\r\nOVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr\r\nzbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM\r\nVAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ\r\nhNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO\r\nojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw\r\nawNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs\r\nOPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3\r\nDQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF\r\ncoJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc\r\nokgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8\r\nt/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy\r\n1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/\r\nSjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=EC-ACC O=Agencia Catalana de Certificacio (NIF Q-0801176-I) OU=Serveis Publics de Certificacio/Vegeu https://www.catcert.net/verarrel (c)03/Jerarquia Entitats de Certificacio Catalanes\r\n# Subject: CN=EC-ACC O=Agencia Catalana de Certificacio (NIF Q-0801176-I) OU=Serveis Publics de Certificacio/Vegeu https://www.catcert.net/verarrel (c)03/Jerarquia Entitats de Certificacio Catalanes\r\n# Label: "EC-ACC"\r\n# Serial: -23701579247955709139626555126524820479\r\n# MD5 Fingerprint: eb:f5:9d:29:0d:61:f9:42:1f:7c:c2:ba:6d:e3:15:09\r\n# SHA1 Fingerprint: 28:90:3a:63:5b:52:80:fa:e6:77:4c:0b:6d:a7:d6:ba:a6:4a:f2:e8\r\n# SHA256 Fingerprint: 88:49:7f:01:60:2f:31:54:24:6a:e2:8c:4d:5a:ef:10:f1:d8:7e:bb:76:62:6f:4a:e0:b7:f9:5b:a7:96:87:99\r\n-----BEGIN CERTIFICATE-----\r\nMIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB\r\n8zELMAkGA1UEBhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2Vy\r\ndGlmaWNhY2lvIChOSUYgUS0wODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1\r\nYmxpY3MgZGUgQ2VydGlmaWNhY2lvMTUwMwYDVQQLEyxWZWdldSBodHRwczovL3d3\r\ndy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAoYykwMzE1MDMGA1UECxMsSmVyYXJxdWlh\r\nIEVudGl0YXRzIGRlIENlcnRpZmljYWNpbyBDYXRhbGFuZXMxDzANBgNVBAMTBkVD\r\nLUFDQzAeFw0wMzAxMDcyMzAwMDBaFw0zMTAxMDcyMjU5NTlaMIHzMQswCQYDVQQG\r\nEwJFUzE7MDkGA1UEChMyQWdlbmNpYSBDYXRhbGFuYSBkZSBDZXJ0aWZpY2FjaW8g\r\nKE5JRiBRLTA4MDExNzYtSSkxKDAmBgNVBAsTH1NlcnZlaXMgUHVibGljcyBkZSBD\r\nZXJ0aWZpY2FjaW8xNTAzBgNVBAsTLFZlZ2V1IGh0dHBzOi8vd3d3LmNhdGNlcnQu\r\nbmV0L3ZlcmFycmVsIChjKTAzMTUwMwYDVQQLEyxKZXJhcnF1aWEgRW50aXRhdHMg\r\nZGUgQ2VydGlmaWNhY2lvIENhdGFsYW5lczEPMA0GA1UEAxMGRUMtQUNDMIIBIjAN\r\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyLHT+KXQpWIR4NA9h0X84NzJB5R\r\n85iKw5K4/0CQBXCHYMkAqbWUZRkiFRfCQ2xmRJoNBD45b6VLeqpjt4pEndljkYRm\r\n4CgPukLjbo73FCeTae6RDqNfDrHrZqJyTxIThmV6PttPB/SnCWDaOkKZx7J/sxaV\r\nHMf5NLWUhdWZXqBIoH7nF2W4onW4HvPlQn2v7fOKSGRdghST2MDk/7NQcvJ29rNd\r\nQlB50JQ+awwAvthrDk4q7D7SzIKiGGUzE3eeml0aE9jD2z3Il3rucO2n5nzbcc8t\r\nlGLfbdb1OL4/pYUKGbio2Al1QnDE6u/LDsg0qBIimAy4E5S2S+zw0JDnJwIDAQAB\r\no4HjMIHgMB0GA1UdEQQWMBSBEmVjX2FjY0BjYXRjZXJ0Lm5ldDAPBgNVHRMBAf8E\r\nBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoMOLRKo3pUW/l4Ba0fF4\r\nopvpXY0wfwYDVR0gBHgwdjB0BgsrBgEEAfV4AQMBCjBlMCwGCCsGAQUFBwIBFiBo\r\ndHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbDA1BggrBgEFBQcCAjApGidW\r\nZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAwDQYJKoZIhvcN\r\nAQEFBQADggEBAKBIW4IB9k1IuDlVNZyAelOZ1Vr/sXE7zDkJlF7W2u++AVtd0x7Y\r\n/X1PzaBB4DSTv8vihpw3kpBWHNzrKQXlxJ7HNd+KDM3FIUPpqojlNcAZQmNaAl6k\r\nSBg6hW/cnbw/nZzBh7h6YQjpdwt/cKt63dmXLGQehb+8dJahw3oS7AwaboMMPOhy\r\nRp/7SNVel+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOS\r\nAgu+TGbrIP65y7WZf+a2E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xl\r\nnJ2lYJU6Un/10asIbvPuW/mIPX64b24D5EI=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Hellenic Academic and Research Institutions RootCA 2011 O=Hellenic Academic and Research Institutions Cert. Authority\r\n# Subject: CN=Hellenic Academic and Research Institutions RootCA 2011 O=Hellenic Academic and Research Institutions Cert. Authority\r\n# Label: "Hellenic Academic and Research Institutions RootCA 2011"\r\n# Serial: 0\r\n# MD5 Fingerprint: 73:9f:4c:4b:73:5b:79:e9:fa:ba:1c:ef:6e:cb:d5:c9\r\n# SHA1 Fingerprint: fe:45:65:9b:79:03:5b:98:a1:61:b5:51:2e:ac:da:58:09:48:22:4d\r\n# SHA256 Fingerprint: bc:10:4f:15:a4:8b:e7:09:dc:a5:42:a7:e1:d4:b9:df:6f:05:45:27:e8:02:ea:a9:2d:59:54:44:25:8a:fe:71\r\n-----BEGIN CERTIFICATE-----\r\nMIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1Ix\r\nRDBCBgNVBAoTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1\r\ndGlvbnMgQ2VydC4gQXV0aG9yaXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1p\r\nYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIFJvb3RDQSAyMDExMB4XDTExMTIw\r\nNjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYTAkdSMUQwQgYDVQQK\r\nEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIENl\r\ncnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl\r\nc2VhcmNoIEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEB\r\nBQADggEPADCCAQoCggEBAKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPz\r\ndYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJ\r\nfel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa71HFK9+WXesyHgLacEns\r\nbgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u8yBRQlqD\r\n75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSP\r\nFEDH3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNV\r\nHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp\r\n5dgTBCPuQSUwRwYDVR0eBEAwPqA8MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQu\r\nb3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQub3JnMA0GCSqGSIb3DQEBBQUA\r\nA4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVtXdMiKahsog2p\r\n6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8\r\nTqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7\r\ndIsXRSZMFpGD/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8Acys\r\nNnq/onN694/BtZqhFLKPM58N7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXI\r\nl7WdmplNsDz4SgCbZN2fOUvRJ9e4\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967\r\n# Subject: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967\r\n# Label: "Actalis Authentication Root CA"\r\n# Serial: 6271844772424770508\r\n# MD5 Fingerprint: 69:c1:0d:4f:07:a3:1b:c3:fe:56:3d:04:bc:11:f6:a6\r\n# SHA1 Fingerprint: f3:73:b3:87:06:5a:28:84:8a:f2:f3:4a:ce:19:2b:dd:c7:8e:9c:ac\r\n# SHA256 Fingerprint: 55:92:60:84:ec:96:3a:64:b9:6e:2a:be:01:ce:0b:a8:6a:64:fb:fe:bc:c7:aa:b5:af:c1:55:b3:7f:d7:60:66\r\n-----BEGIN CERTIFICATE-----\r\nMIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE\r\nBhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w\r\nMzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290\r\nIENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC\r\nSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1\r\nODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB\r\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv\r\nUTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX\r\n4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9\r\nKK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/\r\ngCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb\r\nrxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ\r\n51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F\r\nbe8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe\r\nKF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F\r\nv6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn\r\nfpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7\r\njPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz\r\nezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt\r\nifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL\r\ne3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70\r\njsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz\r\nWochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V\r\nSM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j\r\npwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX\r\nX04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok\r\nfcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R\r\nK4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU\r\nZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU\r\nLysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT\r\nLnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Buypass Class 2 Root CA O=Buypass AS-983163327\r\n# Subject: CN=Buypass Class 2 Root CA O=Buypass AS-983163327\r\n# Label: "Buypass Class 2 Root CA"\r\n# Serial: 2\r\n# MD5 Fingerprint: 46:a7:d2:fe:45:fb:64:5a:a8:59:90:9b:78:44:9b:29\r\n# SHA1 Fingerprint: 49:0a:75:74:de:87:0a:47:fe:58:ee:f6:c7:6b:eb:c6:0b:12:40:99\r\n# SHA256 Fingerprint: 9a:11:40:25:19:7c:5b:b9:5d:94:e6:3d:55:cd:43:79:08:47:b6:46:b2:3c:df:11:ad:a4:a0:0e:ff:15:fb:48\r\n-----BEGIN CERTIFICATE-----\r\nMIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd\r\nMBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg\r\nQ2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow\r\nTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw\r\nHgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB\r\nBQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr\r\n6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV\r\nL4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91\r\n1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx\r\nMlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ\r\nQmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB\r\narcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr\r\nUs3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi\r\nFRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS\r\nP/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN\r\n9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP\r\nAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz\r\nuvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h\r\n9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s\r\nA20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t\r\nOluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo\r\n+fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7\r\nKcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2\r\nDISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us\r\nH8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ\r\nI+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7\r\n5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h\r\n3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz\r\nY11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Buypass Class 3 Root CA O=Buypass AS-983163327\r\n# Subject: CN=Buypass Class 3 Root CA O=Buypass AS-983163327\r\n# Label: "Buypass Class 3 Root CA"\r\n# Serial: 2\r\n# MD5 Fingerprint: 3d:3b:18:9e:2c:64:5a:e8:d5:88:ce:0e:f9:37:c2:ec\r\n# SHA1 Fingerprint: da:fa:f7:fa:66:84:ec:06:8f:14:50:bd:c7:c2:81:a5:bc:a9:64:57\r\n# SHA256 Fingerprint: ed:f7:eb:bc:a2:7a:2a:38:4d:38:7b:7d:40:10:c6:66:e2:ed:b4:84:3e:4c:29:b4:ae:1d:5b:93:32:e6:b2:4d\r\n-----BEGIN CERTIFICATE-----\r\nMIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd\r\nMBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg\r\nQ2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow\r\nTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw\r\nHgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB\r\nBQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y\r\nZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E\r\nN3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9\r\ntznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX\r\n0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c\r\n/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X\r\nKhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY\r\nzIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS\r\nO1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D\r\n34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP\r\nK9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3\r\nAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv\r\nTg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj\r\nQTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV\r\ncSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS\r\nIGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2\r\nHJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa\r\nO5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv\r\n033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u\r\ndmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE\r\nkbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41\r\n3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD\r\nu79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq\r\n4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center\r\n# Subject: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center\r\n# Label: "T-TeleSec GlobalRoot Class 3"\r\n# Serial: 1\r\n# MD5 Fingerprint: ca:fb:40:a8:4e:39:92:8a:1d:fe:8e:2f:c4:27:ea:ef\r\n# SHA1 Fingerprint: 55:a6:72:3e:cb:f2:ec:cd:c3:23:74:70:19:9d:2a:be:11:e3:81:d1\r\n# SHA256 Fingerprint: fd:73:da:d3:1c:64:4f:f1:b4:3b:ef:0c:cd:da:96:71:0b:9c:d9:87:5e:ca:7e:31:70:7a:f3:e9:6d:52:2b:bd\r\n-----BEGIN CERTIFICATE-----\r\nMIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx\r\nKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd\r\nBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl\r\nYyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1\r\nOTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy\r\naXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50\r\nZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G\r\nCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN\r\n8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/\r\nRLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4\r\nhqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5\r\nZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM\r\nEnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj\r\nQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1\r\nA/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy\r\nWL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ\r\n1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30\r\n6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT\r\n91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml\r\ne9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p\r\nTpPDpFQUWw==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH\r\n# Subject: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH\r\n# Label: "D-TRUST Root Class 3 CA 2 2009"\r\n# Serial: 623603\r\n# MD5 Fingerprint: cd:e0:25:69:8d:47:ac:9c:89:35:90:f7:fd:51:3d:2f\r\n# SHA1 Fingerprint: 58:e8:ab:b0:36:15:33:fb:80:f7:9b:1b:6d:29:d3:ff:8d:5f:00:f0\r\n# SHA256 Fingerprint: 49:e7:a4:42:ac:f0:ea:62:87:05:00:54:b5:25:64:b6:50:e4:f4:9e:42:e3:48:d6:aa:38:e0:39:e9:57:b1:c1\r\n-----BEGIN CERTIFICATE-----\r\nMIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF\r\nMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD\r\nbGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha\r\nME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM\r\nHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB\r\nBQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03\r\nUAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42\r\ntSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R\r\nySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM\r\nlFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp\r\n/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G\r\nA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G\r\nA1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj\r\ndG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy\r\nMENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl\r\ncmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js\r\nL2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL\r\nBQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni\r\nacfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0\r\no3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K\r\nzCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8\r\nPIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y\r\nJohw1+qRzT65ysCQblrGXnRl11z+o+I=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH\r\n# Subject: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH\r\n# Label: "D-TRUST Root Class 3 CA 2 EV 2009"\r\n# Serial: 623604\r\n# MD5 Fingerprint: aa:c6:43:2c:5e:2d:cd:c4:34:c0:50:4f:11:02:4f:b6\r\n# SHA1 Fingerprint: 96:c9:1b:0b:95:b4:10:98:42:fa:d0:d8:22:79:fe:60:fa:b9:16:83\r\n# SHA256 Fingerprint: ee:c5:49:6b:98:8c:e9:86:25:b9:34:09:2e:ec:29:08:be:d0:b0:f3:16:c2:d4:73:0c:84:ea:f1:f3:d3:48:81\r\n-----BEGIN CERTIFICATE-----\r\nMIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF\r\nMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD\r\nbGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw\r\nNDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV\r\nBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI\r\nhvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn\r\nljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0\r\n3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z\r\nqQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR\r\np75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8\r\nHgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw\r\nggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea\r\nHNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw\r\nOi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh\r\nc3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E\r\nRT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt\r\ndHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku\r\nY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp\r\n3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05\r\nnsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF\r\nCSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na\r\nxpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX\r\nKVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=CA Disig Root R2 O=Disig a.s.\r\n# Subject: CN=CA Disig Root R2 O=Disig a.s.\r\n# Label: "CA Disig Root R2"\r\n# Serial: 10572350602393338211\r\n# MD5 Fingerprint: 26:01:fb:d8:27:a7:17:9a:45:54:38:1a:43:01:3b:03\r\n# SHA1 Fingerprint: b5:61:eb:ea:a4:de:e4:25:4b:69:1a:98:a5:57:47:c2:34:c7:d9:71\r\n# SHA256 Fingerprint: e2:3d:4a:03:6d:7b:70:e9:f5:95:b1:42:20:79:d2:b9:1e:df:bb:1f:b6:51:a0:63:3e:aa:8a:9d:c5:f8:07:03\r\n-----BEGIN CERTIFICATE-----\r\nMIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV\r\nBAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu\r\nMRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy\r\nMDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx\r\nEzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw\r\nggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe\r\nNcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH\r\nPWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I\r\nx2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe\r\nQTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR\r\nyyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO\r\nQG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912\r\nH9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ\r\nQfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD\r\ni/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs\r\nnLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1\r\nrqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud\r\nDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI\r\nhvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM\r\ntCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf\r\nGopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb\r\nlvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka\r\n+elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal\r\nTFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i\r\nnSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3\r\ngzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr\r\nG5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os\r\nzMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x\r\nL4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV\r\n# Subject: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV\r\n# Label: "ACCVRAIZ1"\r\n# Serial: 6828503384748696800\r\n# MD5 Fingerprint: d0:a0:5a:ee:05:b6:09:94:21:a1:7d:f1:b2:29:82:02\r\n# SHA1 Fingerprint: 93:05:7a:88:15:c6:4f:ce:88:2f:fa:91:16:52:28:78:bc:53:64:17\r\n# SHA256 Fingerprint: 9a:6e:c0:12:e1:a7:da:9d:be:34:19:4d:47:8a:d7:c0:db:18:22:fb:07:1d:f1:29:81:49:6e:d1:04:38:41:13\r\n-----BEGIN CERTIFICATE-----\r\nMIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE\r\nAwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw\r\nCQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ\r\nBgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND\r\nVjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb\r\nqau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY\r\nHtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo\r\nG2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA\r\nlHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr\r\nIA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/\r\n0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH\r\nk6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47\r\n4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO\r\nm3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa\r\ncXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl\r\nuUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI\r\nKwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls\r\nZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG\r\nAQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2\r\nVuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT\r\nVfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG\r\nCCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA\r\ncgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA\r\nQwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA\r\n7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA\r\ncgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA\r\nQwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA\r\nczAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu\r\naHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt\r\naW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud\r\nDwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF\r\nBQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp\r\nD70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU\r\nJyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m\r\nAM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD\r\nvV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms\r\ntn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH\r\n7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h\r\nI6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA\r\nh1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF\r\nd3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H\r\npPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA\r\n# Subject: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA\r\n# Label: "TWCA Global Root CA"\r\n# Serial: 3262\r\n# MD5 Fingerprint: f9:03:7e:cf:e6:9e:3c:73:7a:2a:90:07:69:ff:2b:96\r\n# SHA1 Fingerprint: 9c:bb:48:53:f6:a4:f6:d3:52:a4:e8:32:52:55:60:13:f5:ad:af:65\r\n# SHA256 Fingerprint: 59:76:90:07:f7:68:5d:0f:cd:50:87:2f:9f:95:d5:75:5a:5b:2b:45:7d:81:f3:69:2b:61:0a:98:67:2f:0e:1b\r\n-----BEGIN CERTIFICATE-----\r\nMIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx\r\nEjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT\r\nVFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5\r\nNTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT\r\nB1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG\r\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF\r\n10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz\r\n0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh\r\nMBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH\r\nzIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc\r\n46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2\r\nyKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi\r\nlaLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP\r\noA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA\r\nBDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE\r\nqYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm\r\n4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB\r\n/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL\r\n1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn\r\nLhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF\r\nH6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo\r\nRI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+\r\nnile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh\r\n15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW\r\n6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW\r\nnsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j\r\nwa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz\r\naGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy\r\nKwbQBM0=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=TeliaSonera Root CA v1 O=TeliaSonera\r\n# Subject: CN=TeliaSonera Root CA v1 O=TeliaSonera\r\n# Label: "TeliaSonera Root CA v1"\r\n# Serial: 199041966741090107964904287217786801558\r\n# MD5 Fingerprint: 37:41:49:1b:18:56:9a:26:f5:ad:c2:66:fb:40:a5:4c\r\n# SHA1 Fingerprint: 43:13:bb:96:f1:d5:86:9b:c1:4e:6a:92:f6:cf:f6:34:69:87:82:37\r\n# SHA256 Fingerprint: dd:69:36:fe:21:f8:f0:77:c1:23:a1:a5:21:c1:22:24:f7:22:55:b7:3e:03:a7:26:06:93:e8:a2:4b:0f:a3:89\r\n-----BEGIN CERTIFICATE-----\r\nMIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw\r\nNzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv\r\nb3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD\r\nVQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2\r\nMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F\r\nVRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1\r\n7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X\r\nZ75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+\r\n/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs\r\n81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm\r\ndtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe\r\nOh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu\r\nsDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4\r\npgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs\r\nslESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ\r\narMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD\r\nVR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG\r\n9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl\r\ndxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx\r\n0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj\r\nTQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed\r\nY2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7\r\nQ4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI\r\nOylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7\r\nvVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW\r\nt88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn\r\nHL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx\r\nSK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=E-Tugra Certification Authority O=E-Tu\xc4\x9fra EBG Bili\xc5\x9fim Teknolojileri ve Hizmetleri A.\xc5\x9e. OU=E-Tugra Sertifikasyon Merkezi\r\n# Subject: CN=E-Tugra Certification Authority O=E-Tu\xc4\x9fra EBG Bili\xc5\x9fim Teknolojileri ve Hizmetleri A.\xc5\x9e. OU=E-Tugra Sertifikasyon Merkezi\r\n# Label: "E-Tugra Certification Authority"\r\n# Serial: 7667447206703254355\r\n# MD5 Fingerprint: b8:a1:03:63:b0:bd:21:71:70:8a:6f:13:3a:bb:79:49\r\n# SHA1 Fingerprint: 51:c6:e7:08:49:06:6e:f3:92:d4:5c:a0:0d:6d:a3:62:8f:c3:52:39\r\n# SHA256 Fingerprint: b0:bf:d5:2b:b0:d7:d9:bd:92:bf:5d:4d:c1:3d:a2:55:c0:2c:54:2f:37:83:65:ea:89:39:11:f5:5e:55:f2:3c\r\n-----BEGIN CERTIFICATE-----\r\nMIIGSzCCBDOgAwIBAgIIamg+nFGby1MwDQYJKoZIhvcNAQELBQAwgbIxCzAJBgNV\r\nBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBC\r\naWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhpem1ldGxlcmkgQS7Fni4xJjAkBgNV\r\nBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBNZXJrZXppMSgwJgYDVQQDDB9FLVR1\r\nZ3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMDMwNTEyMDk0OFoXDTIz\r\nMDMwMzEyMDk0OFowgbIxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+\r\nBgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhp\r\nem1ldGxlcmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBN\r\nZXJrZXppMSgwJgYDVQQDDB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5\r\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4vU/kwVRHoViVF56C/UY\r\nB4Oufq9899SKa6VjQzm5S/fDxmSJPZQuVIBSOTkHS0vdhQd2h8y/L5VMzH2nPbxH\r\nD5hw+IyFHnSOkm0bQNGZDbt1bsipa5rAhDGvykPL6ys06I+XawGb1Q5KCKpbknSF\r\nQ9OArqGIW66z6l7LFpp3RMih9lRozt6Plyu6W0ACDGQXwLWTzeHxE2bODHnv0ZEo\r\nq1+gElIwcxmOj+GMB6LDu0rw6h8VqO4lzKRG+Bsi77MOQ7osJLjFLFzUHPhdZL3D\r\nk14opz8n8Y4e0ypQBaNV2cvnOVPAmJ6MVGKLJrD3fY185MaeZkJVgkfnsliNZvcH\r\nfC425lAcP9tDJMW/hkd5s3kc91r0E+xs+D/iWR+V7kI+ua2oMoVJl0b+SzGPWsut\r\ndEcf6ZG33ygEIqDUD13ieU/qbIWGvaimzuT6w+Gzrt48Ue7LE3wBf4QOXVGUnhMM\r\nti6lTPk5cDZvlsouDERVxcr6XQKj39ZkjFqzAQqptQpHF//vkUAqjqFGOjGY5RH8\r\nzLtJVor8udBhmm9lbObDyz51Sf6Pp+KJxWfXnUYTTjF2OySznhFlhqt/7x3U+Lzn\r\nrFpct1pHXFXOVbQicVtbC/DP3KBhZOqp12gKY6fgDT+gr9Oq0n7vUaDmUStVkhUX\r\nU8u3Zg5mTPj5dUyQ5xJwx0UCAwEAAaNjMGEwHQYDVR0OBBYEFC7j27JJ0JxUeVz6\r\nJyr+zE7S6E5UMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAULuPbsknQnFR5\r\nXPonKv7MTtLoTlQwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAF\r\nNzr0TbdF4kV1JI+2d1LoHNgQk2Xz8lkGpD4eKexd0dCrfOAKkEh47U6YA5n+KGCR\r\nHTAduGN8qOY1tfrTYXbm1gdLymmasoR6d5NFFxWfJNCYExL/u6Au/U5Mh/jOXKqY\r\nGwXgAEZKgoClM4so3O0409/lPun++1ndYYRP0lSWE2ETPo+Aab6TR7U1Q9Jauz1c\r\n77NCR807VRMGsAnb/WP2OogKmW9+4c4bU2pEZiNRCHu8W1Ki/QY3OEBhj0qWuJA3\r\n+GbHeJAAFS6LrVE1Uweoa2iu+U48BybNCAVwzDk/dr2l02cmAYamU9JgO3xDf1WK\r\nvJUawSg5TB9D0pH0clmKuVb8P7Sd2nCcdlqMQ1DujjByTd//SffGqWfZbawCEeI6\r\nFiWnWAjLb1NBnEg4R2gz0dfHj9R0IdTDBZB6/86WiLEVKV0jq9BgoRJP3vQXzTLl\r\nyb/IQ639Lo7xr+L0mPoSHyDYwKcMhcWQ9DstliaxLL5Mq+ux0orJ23gTDx4JnW2P\r\nAJ8C2sH6H3p6CcRK5ogql5+Ji/03X186zjhZhkuvcQu02PJwT58yE+Owp1fl2tpD\r\ny4Q08ijE6m30Ku/Ba3ba+367hTzSU8JNvnHhRdH9I2cNE3X7z2VnIp2usAnRCf8d\r\nNL/+I5c30jn6PQ0GC7TbO6Orb1wdtn7os4I07QZcJA==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center\r\n# Subject: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center\r\n# Label: "T-TeleSec GlobalRoot Class 2"\r\n# Serial: 1\r\n# MD5 Fingerprint: 2b:9b:9e:e4:7b:6c:1f:00:72:1a:cc:c1:77:79:df:6a\r\n# SHA1 Fingerprint: 59:0d:2d:7d:88:4f:40:2e:61:7e:a5:62:32:17:65:cf:17:d8:94:e9\r\n# SHA256 Fingerprint: 91:e2:f5:78:8d:58:10:eb:a7:ba:58:73:7d:e1:54:8a:8e:ca:cd:01:45:98:bc:0b:14:3e:04:1b:17:05:25:52\r\n-----BEGIN CERTIFICATE-----\r\nMIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx\r\nKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd\r\nBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl\r\nYyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1\r\nOTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy\r\naXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50\r\nZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G\r\nCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd\r\nAqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC\r\nFoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi\r\n1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq\r\njnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ\r\nwI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj\r\nQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/\r\nWSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy\r\nNsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC\r\nuvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw\r\nIEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6\r\ng1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN\r\n9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP\r\nBSeOE6Fuwg==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Atos TrustedRoot 2011 O=Atos\r\n# Subject: CN=Atos TrustedRoot 2011 O=Atos\r\n# Label: "Atos TrustedRoot 2011"\r\n# Serial: 6643877497813316402\r\n# MD5 Fingerprint: ae:b9:c4:32:4b:ac:7f:5d:66:cc:77:94:bb:2a:77:56\r\n# SHA1 Fingerprint: 2b:b1:f5:3e:55:0c:1d:c5:f1:d4:e6:b7:6a:46:4b:55:06:02:ac:21\r\n# SHA256 Fingerprint: f3:56:be:a2:44:b7:a9:1e:b3:5d:53:ca:9a:d7:86:4a:ce:01:8e:2d:35:d5:f8:f9:6d:df:68:a6:f4:1a:a4:74\r\n-----BEGIN CERTIFICATE-----\r\nMIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE\r\nAwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG\r\nEwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM\r\nFUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC\r\nREUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp\r\nNb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM\r\nVD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+\r\nSZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ\r\n4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L\r\ncp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi\r\neowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV\r\nHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG\r\nA1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3\r\nDQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j\r\nvZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP\r\nDpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc\r\nmaHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D\r\nlmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv\r\nKrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited\r\n# Subject: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited\r\n# Label: "QuoVadis Root CA 1 G3"\r\n# Serial: 687049649626669250736271037606554624078720034195\r\n# MD5 Fingerprint: a4:bc:5b:3f:fe:37:9a:fa:64:f0:e2:fa:05:3d:0b:ab\r\n# SHA1 Fingerprint: 1b:8e:ea:57:96:29:1a:c9:39:ea:b8:0a:81:1a:73:73:c0:93:79:67\r\n# SHA256 Fingerprint: 8a:86:6f:d1:b2:76:b5:7e:57:8e:92:1c:65:82:8a:2b:ed:58:e9:f2:f2:88:05:41:34:b7:f1:f4:bf:c9:cc:74\r\n-----BEGIN CERTIFICATE-----\r\nMIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL\r\nBQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc\r\nBgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00\r\nMjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM\r\naW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG\r\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV\r\nwedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe\r\nrNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341\r\n68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh\r\n4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp\r\nUhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o\r\nabw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc\r\n3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G\r\nKubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt\r\nhfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO\r\nTk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt\r\nzCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB\r\nBjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD\r\nggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC\r\nMTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2\r\ncDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN\r\nqXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5\r\nYCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv\r\nb2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2\r\n8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k\r\nNSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj\r\nZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp\r\nq1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt\r\nnh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited\r\n# Subject: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited\r\n# Label: "QuoVadis Root CA 2 G3"\r\n# Serial: 390156079458959257446133169266079962026824725800\r\n# MD5 Fingerprint: af:0c:86:6e:bf:40:2d:7f:0b:3e:12:50:ba:12:3d:06\r\n# SHA1 Fingerprint: 09:3c:61:f3:8b:8b:dc:7d:55:df:75:38:02:05:00:e1:25:f5:c8:36\r\n# SHA256 Fingerprint: 8f:e4:fb:0a:f9:3a:4d:0d:67:db:0b:eb:b2:3e:37:c7:1b:f3:25:dc:bc:dd:24:0e:a0:4d:af:58:b4:7e:18:40\r\n-----BEGIN CERTIFICATE-----\r\nMIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL\r\nBQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc\r\nBgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00\r\nMjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM\r\naW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG\r\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf\r\nqq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW\r\nn4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym\r\nc5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+\r\nO7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1\r\no9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j\r\nIaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq\r\nIcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz\r\n8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh\r\nvNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l\r\n7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG\r\ncC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB\r\nBjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD\r\nggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66\r\nAarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC\r\nroijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga\r\nW/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n\r\nlv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE\r\n+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV\r\ncsaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd\r\ndbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg\r\nKCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM\r\nHVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4\r\nWSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited\r\n# Subject: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited\r\n# Label: "QuoVadis Root CA 3 G3"\r\n# Serial: 268090761170461462463995952157327242137089239581\r\n# MD5 Fingerprint: df:7d:b9:ad:54:6f:68:a1:df:89:57:03:97:43:b0:d7\r\n# SHA1 Fingerprint: 48:12:bd:92:3c:a8:c4:39:06:e7:30:6d:27:96:e6:a4:cf:22:2e:7d\r\n# SHA256 Fingerprint: 88:ef:81:de:20:2e:b0:18:45:2e:43:f8:64:72:5c:ea:5f:bd:1f:c2:d9:d2:05:73:07:09:c5:d8:b8:69:0f:46\r\n-----BEGIN CERTIFICATE-----\r\nMIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL\r\nBQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc\r\nBgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00\r\nMjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM\r\naW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG\r\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR\r\n/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu\r\nFoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR\r\nU7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c\r\nra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR\r\nFHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k\r\nA9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw\r\neyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl\r\nsSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp\r\nVzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q\r\nA4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+\r\nydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB\r\nBjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD\r\nggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px\r\nKGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI\r\nFUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv\r\noxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg\r\nu/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP\r\n0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf\r\n3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl\r\n8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+\r\nDhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN\r\nPlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/\r\nywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com\r\n# Subject: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com\r\n# Label: "DigiCert Assured ID Root G2"\r\n# Serial: 15385348160840213938643033620894905419\r\n# MD5 Fingerprint: 92:38:b9:f8:63:24:82:65:2c:57:33:e6:fe:81:8f:9d\r\n# SHA1 Fingerprint: a1:4b:48:d9:43:ee:0a:0e:40:90:4f:3c:e0:a4:c0:91:93:51:5d:3f\r\n# SHA256 Fingerprint: 7d:05:eb:b6:82:33:9f:8c:94:51:ee:09:4e:eb:fe:fa:79:53:a1:14:ed:b2:f4:49:49:45:2f:ab:7d:2f:c1:85\r\n-----BEGIN CERTIFICATE-----\r\nMIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl\r\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\r\nd3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv\r\nb3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG\r\nEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl\r\ncnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi\r\nMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA\r\nn61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc\r\nbiJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp\r\nEgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA\r\nbx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu\r\nYjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB\r\nAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW\r\nBBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI\r\nQW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I\r\n0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni\r\nlmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9\r\nB5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv\r\nON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo\r\nIhNzbM8m9Yop5w==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com\r\n# Subject: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com\r\n# Label: "DigiCert Assured ID Root G3"\r\n# Serial: 15459312981008553731928384953135426796\r\n# MD5 Fingerprint: 7c:7f:65:31:0c:81:df:8d:ba:3e:99:e2:5c:ad:6e:fb\r\n# SHA1 Fingerprint: f5:17:a2:4f:9a:48:c6:c9:f8:a2:00:26:9f:dc:0f:48:2c:ab:30:89\r\n# SHA256 Fingerprint: 7e:37:cb:8b:4c:47:09:0c:ab:36:55:1b:a6:f4:5d:b8:40:68:0f:ba:16:6a:95:2d:b1:00:71:7f:43:05:3f:c2\r\n-----BEGIN CERTIFICATE-----\r\nMIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw\r\nCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu\r\nZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg\r\nRzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV\r\nUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu\r\nY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq\r\nhkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf\r\nZn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q\r\nRSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/\r\nBAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD\r\nAwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY\r\nJjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv\r\n6pZjamVFkpUBtA==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com\r\n# Subject: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com\r\n# Label: "DigiCert Global Root G2"\r\n# Serial: 4293743540046975378534879503202253541\r\n# MD5 Fingerprint: e4:a6:8a:c8:54:ac:52:42:46:0a:fd:72:48:1b:2a:44\r\n# SHA1 Fingerprint: df:3c:24:f9:bf:d6:66:76:1b:26:80:73:fe:06:d1:cc:8d:4f:82:a4\r\n# SHA256 Fingerprint: cb:3c:cb:b7:60:31:e5:e0:13:8f:8d:d3:9a:23:f9:de:47:ff:c3:5e:43:c1:14:4c:ea:27:d4:6a:5a:b1:cb:5f\r\n-----BEGIN CERTIFICATE-----\r\nMIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh\r\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\r\nd3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\r\nMjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT\r\nMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\r\nb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG\r\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI\r\n2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx\r\n1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ\r\nq2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz\r\ntCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ\r\nvIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP\r\nBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV\r\n5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY\r\n1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4\r\nNeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG\r\nFdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91\r\n8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe\r\npLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl\r\nMrY=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com\r\n# Subject: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com\r\n# Label: "DigiCert Global Root G3"\r\n# Serial: 7089244469030293291760083333884364146\r\n# MD5 Fingerprint: f5:5d:a4:50:a5:fb:28:7e:1e:0f:0d:cc:96:57:56:ca\r\n# SHA1 Fingerprint: 7e:04:de:89:6a:3e:66:6d:00:e6:87:d3:3f:fa:d9:3b:e8:3d:34:9e\r\n# SHA256 Fingerprint: 31:ad:66:48:f8:10:41:38:c7:38:f3:9e:a4:32:01:33:39:3e:3a:18:cc:02:29:6e:f9:7c:2a:c9:ef:67:31:d0\r\n-----BEGIN CERTIFICATE-----\r\nMIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw\r\nCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu\r\nZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe\r\nFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw\r\nEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x\r\nIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF\r\nK4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG\r\nfp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO\r\nZ9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd\r\nBgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx\r\nAK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/\r\noAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8\r\nsycX\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com\r\n# Subject: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com\r\n# Label: "DigiCert Trusted Root G4"\r\n# Serial: 7451500558977370777930084869016614236\r\n# MD5 Fingerprint: 78:f2:fc:aa:60:1f:2f:b4:eb:c9:37:ba:53:2e:75:49\r\n# SHA1 Fingerprint: dd:fb:16:cd:49:31:c9:73:a2:03:7d:3f:c8:3a:4d:7d:77:5d:05:e4\r\n# SHA256 Fingerprint: 55:2f:7b:dc:f1:a7:af:9e:6c:e6:72:01:7f:4f:12:ab:f7:72:40:c7:8e:76:1a:c2:03:d1:d9:d2:0a:c8:99:88\r\n-----BEGIN CERTIFICATE-----\r\nMIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi\r\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\r\nd3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg\r\nRzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV\r\nUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu\r\nY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG\r\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y\r\nithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If\r\nxp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV\r\nySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO\r\nDCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ\r\njdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/\r\nCNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi\r\nEhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM\r\nfRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY\r\nuKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK\r\nchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t\r\n9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB\r\nhjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD\r\nggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2\r\nSV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd\r\n+SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc\r\nfFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa\r\nsjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N\r\ncCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N\r\n0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie\r\n4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI\r\nr/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1\r\n/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm\r\ngKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=COMODO RSA Certification Authority O=COMODO CA Limited\r\n# Subject: CN=COMODO RSA Certification Authority O=COMODO CA Limited\r\n# Label: "COMODO RSA Certification Authority"\r\n# Serial: 101909084537582093308941363524873193117\r\n# MD5 Fingerprint: 1b:31:b0:71:40:36:cc:14:36:91:ad:c4:3e:fd:ec:18\r\n# SHA1 Fingerprint: af:e5:d2:44:a8:d1:19:42:30:ff:47:9f:e2:f8:97:bb:cd:7a:8c:b4\r\n# SHA256 Fingerprint: 52:f0:e1:c4:e5:8e:c6:29:29:1b:60:31:7f:07:46:71:b8:5d:7e:a8:0d:5b:07:27:34:63:53:4b:32:b4:02:34\r\n-----BEGIN CERTIFICATE-----\r\nMIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB\r\nhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\r\nA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV\r\nBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5\r\nMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT\r\nEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR\r\nQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh\r\ndGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR\r\n6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X\r\npz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC\r\n9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV\r\n/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf\r\nZd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z\r\n+pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w\r\nqP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah\r\nSL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC\r\nu9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf\r\nFobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq\r\ncrxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E\r\nFgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB\r\n/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl\r\nwFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM\r\n4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV\r\n2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna\r\nFxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ\r\nCuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK\r\nboHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke\r\njkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL\r\nS0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb\r\nQOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl\r\n0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB\r\nNVOFBkpdn627G190\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=USERTrust RSA Certification Authority O=The USERTRUST Network\r\n# Subject: CN=USERTrust RSA Certification Authority O=The USERTRUST Network\r\n# Label: "USERTrust RSA Certification Authority"\r\n# Serial: 2645093764781058787591871645665788717\r\n# MD5 Fingerprint: 1b:fe:69:d1:91:b7:19:33:a3:72:a8:0f:e1:55:e5:b5\r\n# SHA1 Fingerprint: 2b:8f:1b:57:33:0d:bb:a2:d0:7a:6c:51:f7:0e:e9:0d:da:b9:ad:8e\r\n# SHA256 Fingerprint: e7:93:c9:b0:2f:d8:aa:13:e2:1c:31:22:8a:cc:b0:81:19:64:3b:74:9c:89:89:64:b1:74:6d:46:c3:d4:cb:d2\r\n-----BEGIN CERTIFICATE-----\r\nMIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB\r\niDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl\r\ncnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV\r\nBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw\r\nMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV\r\nBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU\r\naGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy\r\ndGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\r\nAoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B\r\n3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY\r\ntJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/\r\nFp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2\r\nVN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT\r\n79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6\r\nc0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT\r\nYo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l\r\nc6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee\r\nUB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE\r\nHg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd\r\nBgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G\r\nA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF\r\nUp/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO\r\nVWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3\r\nATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs\r\n8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR\r\niQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze\r\nSf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ\r\nXHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/\r\nqS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB\r\nVXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB\r\nL6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG\r\njjxDah2nGN59PRbxYvnKkKj9\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=USERTrust ECC Certification Authority O=The USERTRUST Network\r\n# Subject: CN=USERTrust ECC Certification Authority O=The USERTRUST Network\r\n# Label: "USERTrust ECC Certification Authority"\r\n# Serial: 123013823720199481456569720443997572134\r\n# MD5 Fingerprint: fa:68:bc:d9:b5:7f:ad:fd:c9:1d:06:83:28:cc:24:c1\r\n# SHA1 Fingerprint: d1:cb:ca:5d:b2:d5:2a:7f:69:3b:67:4d:e5:f0:5a:1d:0c:95:7d:f0\r\n# SHA256 Fingerprint: 4f:f4:60:d5:4b:9c:86:da:bf:bc:fc:57:12:e0:40:0d:2b:ed:3f:bc:4d:4f:bd:aa:86:e0:6a:dc:d2:a9:ad:7a\r\n-----BEGIN CERTIFICATE-----\r\nMIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL\r\nMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl\r\neSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT\r\nJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx\r\nMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT\r\nCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg\r\nVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm\r\naWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo\r\nI+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng\r\no4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G\r\nA1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD\r\nVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB\r\nzzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW\r\nRNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4\r\n# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4\r\n# Label: "GlobalSign ECC Root CA - R4"\r\n# Serial: 14367148294922964480859022125800977897474\r\n# MD5 Fingerprint: 20:f0:27:68:d1:7e:a0:9d:0e:e6:2a:ca:df:5c:89:8e\r\n# SHA1 Fingerprint: 69:69:56:2e:40:80:f4:24:a1:e7:19:9f:14:ba:f3:ee:58:ab:6a:bb\r\n# SHA256 Fingerprint: be:c9:49:11:c2:95:56:76:db:6c:0a:55:09:86:d7:6e:3b:a0:05:66:7c:44:2c:97:62:b4:fb:b7:73:de:22:8c\r\n-----BEGIN CERTIFICATE-----\r\nMIIB4TCCAYegAwIBAgIRKjikHJYKBN5CsiilC+g0mAIwCgYIKoZIzj0EAwIwUDEk\r\nMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpH\r\nbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX\r\nDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD\r\nQSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu\r\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMZ5049sJQ6fLjkZHAOkrprlOQcJ\r\nFspjsbmG+IpXwVfOQvpzofdlQv8ewQCybnMO/8ch5RikqtlxP6jUuc6MHaNCMEAw\r\nDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFSwe61F\r\nuOJAf/sKbvu+M8k8o4TVMAoGCCqGSM49BAMCA0gAMEUCIQDckqGgE6bPA7DmxCGX\r\nkPoUVy0D7O48027KqGx2vKLeuwIgJ6iFJzWbVsaj8kfSt24bAgAXqmemFZHe+pTs\r\newv4n4Q=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5\r\n# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5\r\n# Label: "GlobalSign ECC Root CA - R5"\r\n# Serial: 32785792099990507226680698011560947931244\r\n# MD5 Fingerprint: 9f:ad:3b:1c:02:1e:8a:ba:17:74:38:81:0c:a2:bc:08\r\n# SHA1 Fingerprint: 1f:24:c6:30:cd:a4:18:ef:20:69:ff:ad:4f:dd:5f:46:3a:1b:69:aa\r\n# SHA256 Fingerprint: 17:9f:bc:14:8a:3d:d0:0f:d2:4e:a1:34:58:cc:43:bf:a7:f5:9c:81:82:d7:83:a5:13:f6:eb:ec:10:0c:89:24\r\n-----BEGIN CERTIFICATE-----\r\nMIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk\r\nMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH\r\nbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX\r\nDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD\r\nQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu\r\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc\r\n8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke\r\nhOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD\r\nVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI\r\nKoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg\r\n515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO\r\nxwy8p2Fp8fc74SrL+SvzZpA3\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Staat der Nederlanden EV Root CA O=Staat der Nederlanden\r\n# Subject: CN=Staat der Nederlanden EV Root CA O=Staat der Nederlanden\r\n# Label: "Staat der Nederlanden EV Root CA"\r\n# Serial: 10000013\r\n# MD5 Fingerprint: fc:06:af:7b:e8:1a:f1:9a:b4:e8:d2:70:1f:c0:f5:ba\r\n# SHA1 Fingerprint: 76:e2:7e:c1:4f:db:82:c1:c0:a6:75:b5:05:be:3d:29:b4:ed:db:bb\r\n# SHA256 Fingerprint: 4d:24:91:41:4c:fe:95:67:46:ec:4c:ef:a6:cf:6f:72:e2:8a:13:29:43:2f:9d:8a:90:7a:c4:cb:5d:ad:c1:5a\r\n-----BEGIN CERTIFICATE-----\r\nMIIFcDCCA1igAwIBAgIEAJiWjTANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJO\r\nTDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSkwJwYDVQQDDCBTdGFh\r\ndCBkZXIgTmVkZXJsYW5kZW4gRVYgUm9vdCBDQTAeFw0xMDEyMDgxMTE5MjlaFw0y\r\nMjEyMDgxMTEwMjhaMFgxCzAJBgNVBAYTAk5MMR4wHAYDVQQKDBVTdGFhdCBkZXIg\r\nTmVkZXJsYW5kZW4xKTAnBgNVBAMMIFN0YWF0IGRlciBOZWRlcmxhbmRlbiBFViBS\r\nb290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA48d+ifkkSzrS\r\nM4M1LGns3Amk41GoJSt5uAg94JG6hIXGhaTK5skuU6TJJB79VWZxXSzFYGgEt9nC\r\nUiY4iKTWO0Cmws0/zZiTs1QUWJZV1VD+hq2kY39ch/aO5ieSZxeSAgMs3NZmdO3d\r\nZ//BYY1jTw+bbRcwJu+r0h8QoPnFfxZpgQNH7R5ojXKhTbImxrpsX23Wr9GxE46p\r\nrfNeaXUmGD5BKyF/7otdBwadQ8QpCiv8Kj6GyzyDOvnJDdrFmeK8eEEzduG/L13l\r\npJhQDBXd4Pqcfzho0LKmeqfRMb1+ilgnQ7O6M5HTp5gVXJrm0w912fxBmJc+qiXb\r\nj5IusHsMX/FjqTf5m3VpTCgmJdrV8hJwRVXj33NeN/UhbJCONVrJ0yPr08C+eKxC\r\nKFhmpUZtcALXEPlLVPxdhkqHz3/KRawRWrUgUY0viEeXOcDPusBCAUCZSCELa6fS\r\n/ZbV0b5GnUngC6agIk440ME8MLxwjyx1zNDFjFE7PZQIZCZhfbnDZY8UnCHQqv0X\r\ncgOPvZuM5l5Tnrmd74K74bzickFbIZTTRTeU0d8JOV3nI6qaHcptqAqGhYqCvkIH\r\n1vI4gnPah1vlPNOePqc7nvQDs/nxfRN0Av+7oeX6AHkcpmZBiFxgV6YuCcS6/ZrP\r\npx9Aw7vMWgpVSzs4dlG4Y4uElBbmVvMCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB\r\n/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFP6rAJCYniT8qcwaivsnuL8wbqg7\r\nMA0GCSqGSIb3DQEBCwUAA4ICAQDPdyxuVr5Os7aEAJSrR8kN0nbHhp8dB9O2tLsI\r\neK9p0gtJ3jPFrK3CiAJ9Brc1AsFgyb/E6JTe1NOpEyVa/m6irn0F3H3zbPB+po3u\r\n2dfOWBfoqSmuc0iH55vKbimhZF8ZE/euBhD/UcabTVUlT5OZEAFTdfETzsemQUHS\r\nv4ilf0X8rLiltTMMgsT7B/Zq5SWEXwbKwYY5EdtYzXc7LMJMD16a4/CrPmEbUCTC\r\nwPTxGfARKbalGAKb12NMcIxHowNDXLldRqANb/9Zjr7dn3LDWyvfjFvO5QxGbJKy\r\nCqNMVEIYFRIYvdr8unRu/8G2oGTYqV9Vrp9canaW2HNnh/tNf1zuacpzEPuKqf2e\r\nvTY4SUmH9A4U8OmHuD+nT3pajnnUk+S7aFKErGzp85hwVXIy+TSrK0m1zSBi5Dp6\r\nZ2Orltxtrpfs/J92VoguZs9btsmksNcFuuEnL5O7Jiqik7Ab846+HUCjuTaPPoIa\r\nGl6I6lD4WeKDRikL40Rc4ZW2aZCaFG+XroHPaO+Zmr615+F/+PoTRxZMzG0IQOeL\r\neG9QgkRQP2YGiqtDhFZKDyAthg710tvSeopLzaXoTvFeJiUBWSOgftL2fiFX1ye8\r\nFVdMpEbB4IMeDExNH08GGeL5qPQ6gqGyeUN51q1veieQA6TqJIc/2b3Z6fJfUEkc\r\n7uzXLg==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=IdenTrust Commercial Root CA 1 O=IdenTrust\r\n# Subject: CN=IdenTrust Commercial Root CA 1 O=IdenTrust\r\n# Label: "IdenTrust Commercial Root CA 1"\r\n# Serial: 13298821034946342390520003877796839426\r\n# MD5 Fingerprint: b3:3e:77:73:75:ee:a0:d3:e3:7e:49:63:49:59:bb:c7\r\n# SHA1 Fingerprint: df:71:7e:aa:4a:d9:4e:c9:55:84:99:60:2d:48:de:5f:bc:f0:3a:25\r\n# SHA256 Fingerprint: 5d:56:49:9b:e4:d2:e0:8b:cf:ca:d0:8a:3e:38:72:3d:50:50:3b:de:70:69:48:e4:2f:55:60:30:19:e5:28:ae\r\n-----BEGIN CERTIFICATE-----\r\nMIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK\r\nMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu\r\nVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw\r\nMTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw\r\nJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG\r\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT\r\n3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU\r\n+ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp\r\nS0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1\r\nbVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi\r\nT0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL\r\nvYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK\r\nVsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK\r\ndHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT\r\nc+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv\r\nl7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N\r\niGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB\r\n/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD\r\nggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH\r\n6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt\r\nLRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93\r\nnAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3\r\n+wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK\r\nW2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT\r\nAwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq\r\nl1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG\r\n4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ\r\nmUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A\r\n7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust\r\n# Subject: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust\r\n# Label: "IdenTrust Public Sector Root CA 1"\r\n# Serial: 13298821034946342390521976156843933698\r\n# MD5 Fingerprint: 37:06:a5:b0:fc:89:9d:ba:f4:6b:8c:1a:64:cd:d5:ba\r\n# SHA1 Fingerprint: ba:29:41:60:77:98:3f:f4:f3:ef:f2:31:05:3b:2e:ea:6d:4d:45:fd\r\n# SHA256 Fingerprint: 30:d0:89:5a:9a:44:8a:26:20:91:63:55:22:d1:f5:20:10:b5:86:7a:ca:e1:2c:78:ef:95:8f:d4:f4:38:9f:2f\r\n-----BEGIN CERTIFICATE-----\r\nMIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN\r\nMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu\r\nVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN\r\nMzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0\r\nMSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi\r\nMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7\r\nekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy\r\nRBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS\r\nbdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF\r\n/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R\r\n3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw\r\nEUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy\r\n9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V\r\nGxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ\r\n2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV\r\nWaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD\r\nW/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/\r\nBAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN\r\nAQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj\r\nt2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV\r\nDRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9\r\nTaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G\r\nlwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW\r\nmhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df\r\nWN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5\r\n+bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ\r\ntshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA\r\nGaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv\r\n8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Entrust Root Certification Authority - G2 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2009 Entrust, Inc. - for authorized use only\r\n# Subject: CN=Entrust Root Certification Authority - G2 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2009 Entrust, Inc. - for authorized use only\r\n# Label: "Entrust Root Certification Authority - G2"\r\n# Serial: 1246989352\r\n# MD5 Fingerprint: 4b:e2:c9:91:96:65:0c:f4:0e:5a:93:92:a0:0a:fe:b2\r\n# SHA1 Fingerprint: 8c:f4:27:fd:79:0c:3a:d1:66:06:8d:e8:1e:57:ef:bb:93:22:72:d4\r\n# SHA256 Fingerprint: 43:df:57:74:b0:3e:7f:ef:5f:e4:0d:93:1a:7b:ed:f1:bb:2e:6b:42:73:8c:4e:6d:38:41:10:3d:3a:a7:f3:39\r\n-----BEGIN CERTIFICATE-----\r\nMIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC\r\nVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50\r\ncnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs\r\nIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz\r\ndCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy\r\nNTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu\r\ndHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt\r\ndGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0\r\naG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj\r\nYXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\r\nAoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T\r\nRU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN\r\ncCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW\r\nwcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1\r\nU1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0\r\njaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP\r\nBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN\r\nBgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/\r\njTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ\r\nRkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v\r\n1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R\r\nnAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH\r\nVHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Entrust Root Certification Authority - EC1 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2012 Entrust, Inc. - for authorized use only\r\n# Subject: CN=Entrust Root Certification Authority - EC1 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2012 Entrust, Inc. - for authorized use only\r\n# Label: "Entrust Root Certification Authority - EC1"\r\n# Serial: 51543124481930649114116133369\r\n# MD5 Fingerprint: b6:7e:1d:f0:58:c5:49:6c:24:3b:3d:ed:98:18:ed:bc\r\n# SHA1 Fingerprint: 20:d8:06:40:df:9b:25:f5:12:25:3a:11:ea:f7:59:8a:eb:14:b5:47\r\n# SHA256 Fingerprint: 02:ed:0e:b2:8c:14:da:45:16:5c:56:67:91:70:0d:64:51:d7:fb:56:f0:b2:ab:1d:3b:8e:b0:70:e5:6e:df:f5\r\n-----BEGIN CERTIFICATE-----\r\nMIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG\r\nA1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3\r\nd3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu\r\ndHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMq\r\nRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRUMxMB4XDTEy\r\nMTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYwFAYD\r\nVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0\r\nL2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0g\r\nZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD\r\nZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAi\r\nA2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBt\r\nByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlH\r\nBz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O\r\nBBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC\r\nR98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX\r\nhTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=CFCA EV ROOT O=China Financial Certification Authority\r\n# Subject: CN=CFCA EV ROOT O=China Financial Certification Authority\r\n# Label: "CFCA EV ROOT"\r\n# Serial: 407555286\r\n# MD5 Fingerprint: 74:e1:b6:ed:26:7a:7a:44:30:33:94:ab:7b:27:81:30\r\n# SHA1 Fingerprint: e2:b8:29:4b:55:84:ab:6b:58:c2:90:46:6c:ac:3f:b8:39:8f:84:83\r\n# SHA256 Fingerprint: 5c:c3:d7:8e:4e:1d:5e:45:54:7a:04:e6:87:3e:64:f9:0c:f9:53:6d:1c:cc:2e:f8:00:f3:55:c4:c5:fd:70:fd\r\n-----BEGIN CERTIFICATE-----\r\nMIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD\r\nTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y\r\naXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx\r\nMjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j\r\naWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP\r\nT1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03\r\nsQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL\r\nTIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5\r\n/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp\r\n7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz\r\nEpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt\r\nhxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP\r\na931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot\r\naK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg\r\nTnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV\r\nPKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv\r\ncWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL\r\ntbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd\r\nBgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB\r\nACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT\r\nej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL\r\njOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS\r\nESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy\r\nP5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19\r\nxIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d\r\nCi77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN\r\n5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe\r\n/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z\r\nAAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ\r\n5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed\r\n# Subject: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed\r\n# Label: "OISTE WISeKey Global Root GB CA"\r\n# Serial: 157768595616588414422159278966750757568\r\n# MD5 Fingerprint: a4:eb:b9:61:28:2e:b7:2f:98:b0:35:26:90:99:51:1d\r\n# SHA1 Fingerprint: 0f:f9:40:76:18:d3:d7:6a:4b:98:f0:a8:35:9e:0c:fd:27:ac:cc:ed\r\n# SHA256 Fingerprint: 6b:9c:08:e8:6e:b0:f7:67:cf:ad:65:cd:98:b6:21:49:e5:49:4a:67:f5:84:5e:7b:d1:ed:01:9f:27:b8:6b:d6\r\n-----BEGIN CERTIFICATE-----\r\nMIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt\r\nMQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg\r\nRm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i\r\nYWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x\r\nCzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG\r\nb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh\r\nbCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3\r\nHEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx\r\nWuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX\r\n1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk\r\nu7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P\r\n99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r\r\nM2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw\r\nAwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB\r\nBAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh\r\ncViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5\r\ngSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO\r\nZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf\r\naPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic\r\nNc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A.\r\n# Subject: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A.\r\n# Label: "SZAFIR ROOT CA2"\r\n# Serial: 357043034767186914217277344587386743377558296292\r\n# MD5 Fingerprint: 11:64:c1:89:b0:24:b1:8c:b1:07:7e:89:9e:51:9e:99\r\n# SHA1 Fingerprint: e2:52:fa:95:3f:ed:db:24:60:bd:6e:28:f3:9c:cc:cf:5e:b3:3f:de\r\n# SHA256 Fingerprint: a1:33:9d:33:28:1a:0b:56:e5:57:d3:d3:2b:1c:e7:f9:36:7e:b0:94:bd:5f:a7:2a:7e:50:04:c8:de:d7:ca:fe\r\n-----BEGIN CERTIFICATE-----\r\nMIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL\r\nBQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6\r\nZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw\r\nNzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L\r\ncmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg\r\nUk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN\r\nQLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT\r\n3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw\r\n3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6\r\n3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5\r\nBSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN\r\nXGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD\r\nAgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF\r\nAAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw\r\n8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG\r\nnXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP\r\noky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy\r\nd05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg\r\nLvWpCz/UXeHPhJ/iGcJfitYgHuNztw==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority\r\n# Subject: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority\r\n# Label: "Certum Trusted Network CA 2"\r\n# Serial: 44979900017204383099463764357512596969\r\n# MD5 Fingerprint: 6d:46:9e:d9:25:6d:08:23:5b:5e:74:7d:1e:27:db:f2\r\n# SHA1 Fingerprint: d3:dd:48:3e:2b:bf:4c:05:e8:af:10:f5:fa:76:26:cf:d3:dc:30:92\r\n# SHA256 Fingerprint: b6:76:f2:ed:da:e8:77:5c:d3:6c:b0:f6:3c:d1:d4:60:39:61:f4:9e:62:65:ba:01:3a:2f:03:07:b6:d0:b8:04\r\n-----BEGIN CERTIFICATE-----\r\nMIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB\r\ngDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu\r\nQS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG\r\nA1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz\r\nOTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ\r\nVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp\r\nZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3\r\nb3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA\r\nDGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn\r\n0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB\r\nOJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE\r\nfktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E\r\nSv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m\r\no130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i\r\nsx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW\r\nOZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez\r\nTv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS\r\nadgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n\r\n3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD\r\nAQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC\r\nAQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ\r\nF/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf\r\nCVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29\r\nXN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm\r\ndjWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/\r\nWjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb\r\nAoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq\r\nP/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko\r\nb7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj\r\nXALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P\r\n5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi\r\nDrW5viSP\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority\r\n# Subject: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority\r\n# Label: "Hellenic Academic and Research Institutions RootCA 2015"\r\n# Serial: 0\r\n# MD5 Fingerprint: ca:ff:e2:db:03:d9:cb:4b:e9:0f:ad:84:fd:7b:18:ce\r\n# SHA1 Fingerprint: 01:0c:06:95:a6:98:19:14:ff:bf:5f:c6:b0:b6:95:ea:29:e9:12:a6\r\n# SHA256 Fingerprint: a0:40:92:9a:02:ce:53:b4:ac:f4:f2:ff:c6:98:1c:e4:49:6f:75:5e:6d:45:fe:0b:2a:69:2b:cd:52:52:3f:36\r\n-----BEGIN CERTIFICATE-----\r\nMIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix\r\nDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k\r\nIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT\r\nN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v\r\ndENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG\r\nA1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh\r\nZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx\r\nQDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1\r\ndGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC\r\nAQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA\r\n4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0\r\nAoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10\r\n4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C\r\nojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV\r\n9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD\r\ngfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6\r\nY5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq\r\nNhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko\r\nLfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc\r\nBw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV\r\nHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd\r\nctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I\r\nXtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI\r\nM4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot\r\n9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V\r\nZ5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea\r\nj8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh\r\nX9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ\r\nl033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf\r\nbzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4\r\npcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK\r\ne7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0\r\nvm9qp/UsQu0yrbYhnr68\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority\r\n# Subject: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority\r\n# Label: "Hellenic Academic and Research Institutions ECC RootCA 2015"\r\n# Serial: 0\r\n# MD5 Fingerprint: 81:e5:b4:17:eb:c2:f5:e1:4b:0d:41:7b:49:92:fe:ef\r\n# SHA1 Fingerprint: 9f:f1:71:8d:92:d5:9a:f3:7d:74:97:b4:bc:6f:84:68:0b:ba:b6:66\r\n# SHA256 Fingerprint: 44:b5:45:aa:8a:25:e6:5a:73:ca:15:dc:27:fc:36:d2:4c:1c:b9:95:3a:06:65:39:b1:15:82:dc:48:7b:48:33\r\n-----BEGIN CERTIFICATE-----\r\nMIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN\r\nBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl\r\nc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl\r\nbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv\r\nb3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ\r\nBgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj\r\nYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5\r\nMUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0\r\ndXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg\r\nQehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa\r\njq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC\r\nMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi\r\nC4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep\r\nlSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof\r\nTUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=ISRG Root X1 O=Internet Security Research Group\r\n# Subject: CN=ISRG Root X1 O=Internet Security Research Group\r\n# Label: "ISRG Root X1"\r\n# Serial: 172886928669790476064670243504169061120\r\n# MD5 Fingerprint: 0c:d2:f9:e0:da:17:73:e9:ed:86:4d:a5:e3:70:e7:4e\r\n# SHA1 Fingerprint: ca:bd:2a:79:a1:07:6a:31:f2:1d:25:36:35:cb:03:9d:43:29:a5:e8\r\n# SHA256 Fingerprint: 96:bc:ec:06:26:49:76:f3:74:60:77:9a:cf:28:c5:a7:cf:e8:a3:c0:aa:e1:1a:8f:fc:ee:05:c0:bd:df:08:c6\r\n-----BEGIN CERTIFICATE-----\r\nMIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\r\nTzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\r\ncmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4\r\nWhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu\r\nZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY\r\nMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc\r\nh77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+\r\n0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U\r\nA5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW\r\nT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH\r\nB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC\r\nB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv\r\nKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn\r\nOlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn\r\njh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw\r\nqHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI\r\nrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\r\nHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq\r\nhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL\r\nubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ\r\n3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK\r\nNFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5\r\nORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur\r\nTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC\r\njNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc\r\noyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq\r\n4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA\r\nmRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d\r\nemyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: O=FNMT-RCM OU=AC RAIZ FNMT-RCM\r\n# Subject: O=FNMT-RCM OU=AC RAIZ FNMT-RCM\r\n# Label: "AC RAIZ FNMT-RCM"\r\n# Serial: 485876308206448804701554682760554759\r\n# MD5 Fingerprint: e2:09:04:b4:d3:bd:d1:a0:14:fd:1a:d2:47:c4:57:1d\r\n# SHA1 Fingerprint: ec:50:35:07:b2:15:c4:95:62:19:e2:a8:9a:5b:42:99:2c:4c:2c:20\r\n# SHA256 Fingerprint: eb:c5:57:0c:29:01:8c:4d:67:b1:aa:12:7b:af:12:f7:03:b4:61:1e:bc:17:b7:da:b5:57:38:94:17:9b:93:fa\r\n-----BEGIN CERTIFICATE-----\r\nMIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx\r\nCzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ\r\nWiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ\r\nBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG\r\nTk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/\r\nyBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf\r\nBBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz\r\nWHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF\r\ntBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z\r\n374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC\r\nIfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL\r\nmbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7\r\nwk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS\r\nMKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2\r\nZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet\r\nUqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw\r\nAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H\r\nYJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3\r\nLmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD\r\nnFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1\r\nRXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM\r\nLVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf\r\n77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N\r\nJpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm\r\nfZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp\r\n6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp\r\n1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B\r\n9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok\r\nRqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv\r\nuu8wd+RU4riEmViAqhOLUTpPSPaLtrM=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Amazon Root CA 1 O=Amazon\r\n# Subject: CN=Amazon Root CA 1 O=Amazon\r\n# Label: "Amazon Root CA 1"\r\n# Serial: 143266978916655856878034712317230054538369994\r\n# MD5 Fingerprint: 43:c6:bf:ae:ec:fe:ad:2f:18:c6:88:68:30:fc:c8:e6\r\n# SHA1 Fingerprint: 8d:a7:f9:65:ec:5e:fc:37:91:0f:1c:6e:59:fd:c1:cc:6a:6e:de:16\r\n# SHA256 Fingerprint: 8e:cd:e6:88:4f:3d:87:b1:12:5b:a3:1a:c3:fc:b1:3d:70:16:de:7f:57:cc:90:4f:e1:cb:97:c6:ae:98:19:6e\r\n-----BEGIN CERTIFICATE-----\r\nMIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF\r\nADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\r\nb24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL\r\nMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\r\nb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj\r\nca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM\r\n9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw\r\nIFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6\r\nVOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L\r\n93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm\r\njgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\r\nAYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA\r\nA4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI\r\nU5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs\r\nN+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv\r\no/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU\r\n5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy\r\nrqXRfboQnoZsG4q5WTP468SQvvG5\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Amazon Root CA 2 O=Amazon\r\n# Subject: CN=Amazon Root CA 2 O=Amazon\r\n# Label: "Amazon Root CA 2"\r\n# Serial: 143266982885963551818349160658925006970653239\r\n# MD5 Fingerprint: c8:e5:8d:ce:a8:42:e2:7a:c0:2a:5c:7c:9e:26:bf:66\r\n# SHA1 Fingerprint: 5a:8c:ef:45:d7:a6:98:59:76:7a:8c:8b:44:96:b5:78:cf:47:4b:1a\r\n# SHA256 Fingerprint: 1b:a5:b2:aa:8c:65:40:1a:82:96:01:18:f8:0b:ec:4f:62:30:4d:83:ce:c4:71:3a:19:c3:9c:01:1e:a4:6d:b4\r\n-----BEGIN CERTIFICATE-----\r\nMIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF\r\nADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\r\nb24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL\r\nMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\r\nb3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK\r\ngXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ\r\nW0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg\r\n1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K\r\n8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r\r\n2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me\r\nz/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR\r\n8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj\r\nmUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz\r\n7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6\r\n+XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI\r\n0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB\r\nAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm\r\nUjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2\r\nLIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY\r\n+gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS\r\nk5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl\r\n7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm\r\nbtmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl\r\nurR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+\r\nfUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63\r\nn749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE\r\n76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H\r\n9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT\r\n4PsJYGw=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Amazon Root CA 3 O=Amazon\r\n# Subject: CN=Amazon Root CA 3 O=Amazon\r\n# Label: "Amazon Root CA 3"\r\n# Serial: 143266986699090766294700635381230934788665930\r\n# MD5 Fingerprint: a0:d4:ef:0b:f7:b5:d8:49:95:2a:ec:f5:c4:fc:81:87\r\n# SHA1 Fingerprint: 0d:44:dd:8c:3c:8c:1a:1a:58:75:64:81:e9:0f:2e:2a:ff:b3:d2:6e\r\n# SHA256 Fingerprint: 18:ce:6c:fe:7b:f1:4e:60:b2:e3:47:b8:df:e8:68:cb:31:d0:2e:bb:3a:da:27:15:69:f5:03:43:b4:6d:b3:a4\r\n-----BEGIN CERTIFICATE-----\r\nMIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5\r\nMQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g\r\nUm9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG\r\nA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg\r\nQ0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl\r\nui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j\r\nQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr\r\nttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr\r\nBqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM\r\nYyRIHN8wfdVoOw==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Amazon Root CA 4 O=Amazon\r\n# Subject: CN=Amazon Root CA 4 O=Amazon\r\n# Label: "Amazon Root CA 4"\r\n# Serial: 143266989758080763974105200630763877849284878\r\n# MD5 Fingerprint: 89:bc:27:d5:eb:17:8d:06:6a:69:d5:fd:89:47:b4:cd\r\n# SHA1 Fingerprint: f6:10:84:07:d6:f8:bb:67:98:0c:c2:e2:44:c2:eb:ae:1c:ef:63:be\r\n# SHA256 Fingerprint: e3:5d:28:41:9e:d0:20:25:cf:a6:90:38:cd:62:39:62:45:8d:a5:c6:95:fb:de:a3:c2:2b:0b:fb:25:89:70:92\r\n-----BEGIN CERTIFICATE-----\r\nMIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5\r\nMQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g\r\nUm9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG\r\nA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg\r\nQ0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi\r\n9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk\r\nM6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB\r\n/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB\r\nMAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw\r\nCkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW\r\n1KyLa2tJElMzrdfkviT8tQp21KW8EA==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM\r\n# Subject: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM\r\n# Label: "TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1"\r\n# Serial: 1\r\n# MD5 Fingerprint: dc:00:81:dc:69:2f:3e:2f:b0:3b:f6:3d:5a:91:8e:49\r\n# SHA1 Fingerprint: 31:43:64:9b:ec:ce:27:ec:ed:3a:3f:0b:8f:0d:e4:e8:91:dd:ee:ca\r\n# SHA256 Fingerprint: 46:ed:c3:68:90:46:d5:3a:45:3f:b3:10:4a:b8:0d:ca:ec:65:8b:26:60:ea:16:29:dd:7e:86:79:90:64:87:16\r\n-----BEGIN CERTIFICATE-----\r\nMIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx\r\nGDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp\r\nbXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w\r\nKwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0\r\nBgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy\r\ndW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG\r\nEwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll\r\nIEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU\r\nQUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT\r\nTTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg\r\nLSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7\r\na9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr\r\nLqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr\r\nN3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X\r\nYacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/\r\niSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f\r\nAJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH\r\nV8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL\r\nBQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh\r\nAHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf\r\nIPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4\r\nlzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c\r\n8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf\r\nlo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD.\r\n# Subject: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD.\r\n# Label: "GDCA TrustAUTH R5 ROOT"\r\n# Serial: 9009899650740120186\r\n# MD5 Fingerprint: 63:cc:d9:3d:34:35:5c:6f:53:a3:e2:08:70:48:1f:b4\r\n# SHA1 Fingerprint: 0f:36:38:5b:81:1a:25:c3:9b:31:4e:83:ca:e9:34:66:70:cc:74:b4\r\n# SHA256 Fingerprint: bf:ff:8f:d0:44:33:48:7d:6a:8a:a6:0c:1a:29:76:7a:9f:c2:bb:b0:5e:42:0f:71:3a:13:b9:92:89:1d:38:93\r\n-----BEGIN CERTIFICATE-----\r\nMIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE\r\nBhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ\r\nIENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0\r\nMTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVowYjELMAkGA1UEBhMCQ04xMjAwBgNV\r\nBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8w\r\nHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0BAQEF\r\nAAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJj\r\nDp6L3TQsAlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBj\r\nTnnEt1u9ol2x8kECK62pOqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+u\r\nKU49tm7srsHwJ5uu4/Ts765/94Y9cnrrpftZTqfrlYwiOXnhLQiPzLyRuEH3FMEj\r\nqcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ9Cy5WmYqsBebnh52nUpm\r\nMUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQxXABZG12\r\nZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloP\r\nzgsMR6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3Gk\r\nL30SgLdTMEZeS1SZD2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeC\r\njGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4oR24qoAATILnsn8JuLwwoC8N9VKejveSswoA\r\nHQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx9hoh49pwBiFYFIeFd3mqgnkC\r\nAwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlRMA8GA1UdEwEB\r\n/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg\r\np8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZm\r\nDRd9FBUb1Ov9H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5\r\nCOmSdI31R9KrO9b7eGZONn356ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ry\r\nL3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd+PwyvzeG5LuOmCd+uh8W4XAR8gPf\r\nJWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQHtZa37dG/OaG+svg\r\nIHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBDF8Io\r\n2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV\r\n09tL7ECQ8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQ\r\nXR4EzzffHqhmsYzmIGrv/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrq\r\nT8p+ck0LcIymSLumoRT2+1hEmRSuqguTaaApJUqlyyvdimYHFngVV3Eb7PVHhPOe\r\nMTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=TrustCor RootCert CA-1 O=TrustCor Systems S. de R.L. OU=TrustCor Certificate Authority\r\n# Subject: CN=TrustCor RootCert CA-1 O=TrustCor Systems S. de R.L. OU=TrustCor Certificate Authority\r\n# Label: "TrustCor RootCert CA-1"\r\n# Serial: 15752444095811006489\r\n# MD5 Fingerprint: 6e:85:f1:dc:1a:00:d3:22:d5:b2:b2:ac:6b:37:05:45\r\n# SHA1 Fingerprint: ff:bd:cd:e7:82:c8:43:5e:3c:6f:26:86:5c:ca:a8:3a:45:5b:c3:0a\r\n# SHA256 Fingerprint: d4:0e:9c:86:cd:8f:e4:68:c1:77:69:59:f4:9e:a7:74:fa:54:86:84:b6:c4:06:f3:90:92:61:f4:dc:e2:57:5c\r\n-----BEGIN CERTIFICATE-----\r\nMIIEMDCCAxigAwIBAgIJANqb7HHzA7AZMA0GCSqGSIb3DQEBCwUAMIGkMQswCQYD\r\nVQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEk\r\nMCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U\r\ncnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRydXN0Q29y\r\nIFJvb3RDZXJ0IENBLTEwHhcNMTYwMjA0MTIzMjE2WhcNMjkxMjMxMTcyMzE2WjCB\r\npDELMAkGA1UEBhMCUEExDzANBgNVBAgMBlBhbmFtYTEUMBIGA1UEBwwLUGFuYW1h\r\nIENpdHkxJDAiBgNVBAoMG1RydXN0Q29yIFN5c3RlbXMgUy4gZGUgUi5MLjEnMCUG\r\nA1UECwweVHJ1c3RDb3IgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MR8wHQYDVQQDDBZU\r\ncnVzdENvciBSb290Q2VydCBDQS0xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\r\nCgKCAQEAv463leLCJhJrMxnHQFgKq1mqjQCj/IDHUHuO1CAmujIS2CNUSSUQIpid\r\nRtLByZ5OGy4sDjjzGiVoHKZaBeYei0i/mJZ0PmnK6bV4pQa81QBeCQryJ3pS/C3V\r\nseq0iWEk8xoT26nPUu0MJLq5nux+AHT6k61sKZKuUbS701e/s/OojZz0JEsq1pme\r\n9J7+wH5COucLlVPat2gOkEz7cD+PSiyU8ybdY2mplNgQTsVHCJCZGxdNuWxu72CV\r\nEY4hgLW9oHPY0LJ3xEXqWib7ZnZ2+AYfYW0PVcWDtxBWcgYHpfOxGgMFZA6dWorW\r\nhnAbJN7+KIor0Gqw/Hqi3LJ5DotlDwIDAQABo2MwYTAdBgNVHQ4EFgQU7mtJPHo/\r\nDeOxCbeKyKsZn3MzUOcwHwYDVR0jBBgwFoAU7mtJPHo/DeOxCbeKyKsZn3MzUOcw\r\nDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD\r\nggEBACUY1JGPE+6PHh0RU9otRCkZoB5rMZ5NDp6tPVxBb5UrJKF5mDo4Nvu7Zp5I\r\n/5CQ7z3UuJu0h3U/IJvOcs+hVcFNZKIZBqEHMwwLKeXx6quj7LUKdJDHfXLy11yf\r\nke+Ri7fc7Waiz45mO7yfOgLgJ90WmMCV1Aqk5IGadZQ1nJBfiDcGrVmVCrDRZ9MZ\r\nyonnMlo2HD6CqFqTvsbQZJG2z9m2GM/bftJlo6bEjhcxwft+dtvTheNYsnd6djts\r\nL1Ac59v2Z3kf9YKVmgenFK+P3CghZwnS1k1aHBkcjndcw5QkPTJrS37UeJSDvjdN\r\nzl/HHk484IkzlQsPpTLWPFp5LBk=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=TrustCor RootCert CA-2 O=TrustCor Systems S. de R.L. OU=TrustCor Certificate Authority\r\n# Subject: CN=TrustCor RootCert CA-2 O=TrustCor Systems S. de R.L. OU=TrustCor Certificate Authority\r\n# Label: "TrustCor RootCert CA-2"\r\n# Serial: 2711694510199101698\r\n# MD5 Fingerprint: a2:e1:f8:18:0b:ba:45:d5:c7:41:2a:bb:37:52:45:64\r\n# SHA1 Fingerprint: b8:be:6d:cb:56:f1:55:b9:63:d4:12:ca:4e:06:34:c7:94:b2:1c:c0\r\n# SHA256 Fingerprint: 07:53:e9:40:37:8c:1b:d5:e3:83:6e:39:5d:ae:a5:cb:83:9e:50:46:f1:bd:0e:ae:19:51:cf:10:fe:c7:c9:65\r\n-----BEGIN CERTIFICATE-----\r\nMIIGLzCCBBegAwIBAgIIJaHfyjPLWQIwDQYJKoZIhvcNAQELBQAwgaQxCzAJBgNV\r\nBAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQw\r\nIgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRy\r\ndXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0eTEfMB0GA1UEAwwWVHJ1c3RDb3Ig\r\nUm9vdENlcnQgQ0EtMjAeFw0xNjAyMDQxMjMyMjNaFw0zNDEyMzExNzI2MzlaMIGk\r\nMQswCQYDVQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEg\r\nQ2l0eTEkMCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYD\r\nVQQLDB5UcnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRy\r\ndXN0Q29yIFJvb3RDZXJ0IENBLTIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\r\nAoICAQCnIG7CKqJiJJWQdsg4foDSq8GbZQWU9MEKENUCrO2fk8eHyLAnK0IMPQo+\r\nQVqedd2NyuCb7GgypGmSaIwLgQ5WoD4a3SwlFIIvl9NkRvRUqdw6VC0xK5mC8tkq\r\n1+9xALgxpL56JAfDQiDyitSSBBtlVkxs1Pu2YVpHI7TYabS3OtB0PAx1oYxOdqHp\r\n2yqlO/rOsP9+aij9JxzIsekp8VduZLTQwRVtDr4uDkbIXvRR/u8OYzo7cbrPb1nK\r\nDOObXUm4TOJXsZiKQlecdu/vvdFoqNL0Cbt3Nb4lggjEFixEIFapRBF37120Hape\r\naz6LMvYHL1cEksr1/p3C6eizjkxLAjHZ5DxIgif3GIJ2SDpxsROhOdUuxTTCHWKF\r\n3wP+TfSvPd9cW436cOGlfifHhi5qjxLGhF5DUVCcGZt45vz27Ud+ez1m7xMTiF88\r\noWP7+ayHNZ/zgp6kPwqcMWmLmaSISo5uZk3vFsQPeSghYA2FFn3XVDjxklb9tTNM\r\ng9zXEJ9L/cb4Qr26fHMC4P99zVvh1Kxhe1fVSntb1IVYJ12/+CtgrKAmrhQhJ8Z3\r\nmjOAPF5GP/fDsaOGM8boXg25NSyqRsGFAnWAoOsk+xWq5Gd/bnc/9ASKL3x74xdh\r\n8N0JqSDIvgmk0H5Ew7IwSjiqqewYmgeCK9u4nBit2uBGF6zPXQIDAQABo2MwYTAd\r\nBgNVHQ4EFgQU2f4hQG6UnrybPZx9mCAZ5YwwYrIwHwYDVR0jBBgwFoAU2f4hQG6U\r\nnrybPZx9mCAZ5YwwYrIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYw\r\nDQYJKoZIhvcNAQELBQADggIBAJ5Fngw7tu/hOsh80QA9z+LqBrWyOrsGS2h60COX\r\ndKcs8AjYeVrXWoSK2BKaG9l9XE1wxaX5q+WjiYndAfrs3fnpkpfbsEZC89NiqpX+\r\nMWcUaViQCqoL7jcjx1BRtPV+nuN79+TMQjItSQzL/0kMmx40/W5ulop5A7Zv2wnL\r\n/V9lFDfhOPXzYRZY5LVtDQsEGz9QLX+zx3oaFoBg+Iof6Rsqxvm6ARppv9JYx1RX\r\nCI/hOWB3S6xZhBqI8d3LT3jX5+EzLfzuQfogsL7L9ziUwOHQhQ+77Sxzq+3+knYa\r\nZH9bDTMJBzN7Bj8RpFxwPIXAz+OQqIN3+tvmxYxoZxBnpVIt8MSZj3+/0WvitUfW\r\n2dCFmU2Umw9Lje4AWkcdEQOsQRivh7dvDDqPys/cA8GiCcjl/YBeyGBCARsaU1q7\r\nN6a3vLqE6R5sGtRk2tRD/pOLS/IseRYQ1JMLiI+h2IYURpFHmygk71dSTlxCnKr3\r\nSewn6EAes6aJInKc9Q0ztFijMDvd1GpUk74aTfOTlPf8hAs/hCBcNANExdqtvArB\r\nAs8e5ZTZ845b2EzwnexhF7sUMlQMAimTHpKG9n/v55IFDlndmQguLvqcAFLTxWYp\r\n5KeXRKQOKIETNcX2b2TmQcTVL8w0RSXPQQCWPUouwpaYT05KnJe32x+SMsj/D1Fu\r\n1uwJ\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=TrustCor ECA-1 O=TrustCor Systems S. de R.L. OU=TrustCor Certificate Authority\r\n# Subject: CN=TrustCor ECA-1 O=TrustCor Systems S. de R.L. OU=TrustCor Certificate Authority\r\n# Label: "TrustCor ECA-1"\r\n# Serial: 9548242946988625984\r\n# MD5 Fingerprint: 27:92:23:1d:0a:f5:40:7c:e9:e6:6b:9d:d8:f5:e7:6c\r\n# SHA1 Fingerprint: 58:d1:df:95:95:67:6b:63:c0:f0:5b:1c:17:4d:8b:84:0b:c8:78:bd\r\n# SHA256 Fingerprint: 5a:88:5d:b1:9c:01:d9:12:c5:75:93:88:93:8c:af:bb:df:03:1a:b2:d4:8e:91:ee:15:58:9b:42:97:1d:03:9c\r\n-----BEGIN CERTIFICATE-----\r\nMIIEIDCCAwigAwIBAgIJAISCLF8cYtBAMA0GCSqGSIb3DQEBCwUAMIGcMQswCQYD\r\nVQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEk\r\nMCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U\r\ncnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxFzAVBgNVBAMMDlRydXN0Q29y\r\nIEVDQS0xMB4XDTE2MDIwNDEyMzIzM1oXDTI5MTIzMTE3MjgwN1owgZwxCzAJBgNV\r\nBAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQw\r\nIgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRy\r\ndXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0eTEXMBUGA1UEAwwOVHJ1c3RDb3Ig\r\nRUNBLTEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPj+ARtZ+odnbb\r\n3w9U73NjKYKtR8aja+3+XzP4Q1HpGjORMRegdMTUpwHmspI+ap3tDvl0mEDTPwOA\r\nBoJA6LHip1GnHYMma6ve+heRK9jGrB6xnhkB1Zem6g23xFUfJ3zSCNV2HykVh0A5\r\n3ThFEXXQmqc04L/NyFIduUd+Dbi7xgz2c1cWWn5DkR9VOsZtRASqnKmcp0yJF4Ou\r\nowReUoCLHhIlERnXDH19MURB6tuvsBzvgdAsxZohmz3tQjtQJvLsznFhBmIhVE5/\r\nwZ0+fyCMgMsq2JdiyIMzkX2woloPV+g7zPIlstR8L+xNxqE6FXrntl019fZISjZF\r\nZtS6mFjBAgMBAAGjYzBhMB0GA1UdDgQWBBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAf\r\nBgNVHSMEGDAWgBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAPBgNVHRMBAf8EBTADAQH/\r\nMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAQEABT41XBVwm8nHc2Fv\r\ncivUwo/yQ10CzsSUuZQRg2dd4mdsdXa/uwyqNsatR5Nj3B5+1t4u/ukZMjgDfxT2\r\nAHMsWbEhBuH7rBiVDKP/mZb3Kyeb1STMHd3BOuCYRLDE5D53sXOpZCz2HAF8P11F\r\nhcCF5yWPldwX8zyfGm6wyuMdKulMY/okYWLW2n62HGz1Ah3UKt1VkOsqEUc8Ll50\r\nsoIipX1TH0XsJ5F95yIW6MBoNtjG8U+ARDL54dHRHareqKucBK+tIA5kmE2la8BI\r\nWJZpTdwHjFGTot+fDz2LYLSCjaoITmJF4PkL0uDgPFveXHEnJcLmA4GLEFPjx1Wi\r\ntJ/X5g==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=SSL.com Root Certification Authority RSA O=SSL Corporation\r\n# Subject: CN=SSL.com Root Certification Authority RSA O=SSL Corporation\r\n# Label: "SSL.com Root Certification Authority RSA"\r\n# Serial: 8875640296558310041\r\n# MD5 Fingerprint: 86:69:12:c0:70:f1:ec:ac:ac:c2:d5:bc:a5:5b:a1:29\r\n# SHA1 Fingerprint: b7:ab:33:08:d1:ea:44:77:ba:14:80:12:5a:6f:bd:a9:36:49:0c:bb\r\n# SHA256 Fingerprint: 85:66:6a:56:2e:e0:be:5c:e9:25:c1:d8:89:0a:6f:76:a8:7e:c1:6d:4d:7d:5f:29:ea:74:19:cf:20:12:3b:69\r\n-----BEGIN CERTIFICATE-----\r\nMIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE\r\nBhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK\r\nDA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp\r\nY2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz\r\nOTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv\r\ndXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv\r\nbSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN\r\nAQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R\r\nxFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX\r\nqhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC\r\nC52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3\r\n6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh\r\n/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF\r\nYD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E\r\nJNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc\r\nUS4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8\r\nZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm\r\n+Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi\r\nM+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV\r\nHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G\r\nA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV\r\ncpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc\r\nHadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs\r\nPgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/\r\nq5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0\r\ncuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr\r\na6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I\r\nH37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y\r\nK9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu\r\nnLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf\r\noYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY\r\nIc2wBlX7Jz9TkHCpBB5XJ7k=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=SSL.com Root Certification Authority ECC O=SSL Corporation\r\n# Subject: CN=SSL.com Root Certification Authority ECC O=SSL Corporation\r\n# Label: "SSL.com Root Certification Authority ECC"\r\n# Serial: 8495723813297216424\r\n# MD5 Fingerprint: 2e:da:e4:39:7f:9c:8f:37:d1:70:9f:26:17:51:3a:8e\r\n# SHA1 Fingerprint: c3:19:7c:39:24:e6:54:af:1b:c4:ab:20:95:7a:e2:c3:0e:13:02:6a\r\n# SHA256 Fingerprint: 34:17:bb:06:cc:60:07:da:1b:96:1c:92:0b:8a:b4:ce:3f:ad:82:0e:4a:a3:0b:9a:cb:c4:a7:4e:bd:ce:bc:65\r\n-----BEGIN CERTIFICATE-----\r\nMIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC\r\nVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T\r\nU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0\r\naW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz\r\nWjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0\r\nb24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS\r\nb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB\r\nBAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI\r\n7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg\r\nCemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud\r\nEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD\r\nVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T\r\nkdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+\r\ngA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation\r\n# Subject: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation\r\n# Label: "SSL.com EV Root Certification Authority RSA R2"\r\n# Serial: 6248227494352943350\r\n# MD5 Fingerprint: e1:1e:31:58:1a:ae:54:53:02:f6:17:6a:11:7b:4d:95\r\n# SHA1 Fingerprint: 74:3a:f0:52:9b:d0:32:a0:f4:4a:83:cd:d4:ba:a9:7b:7c:2e:c4:9a\r\n# SHA256 Fingerprint: 2e:7b:f1:6c:c2:24:85:a7:bb:e2:aa:86:96:75:07:61:b0:ae:39:be:3b:2f:e9:d0:cc:6d:4e:f7:34:91:42:5c\r\n-----BEGIN CERTIFICATE-----\r\nMIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV\r\nBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE\r\nCgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy\r\ndGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy\r\nMDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G\r\nA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD\r\nDC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy\r\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq\r\nM0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf\r\nOePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa\r\n4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9\r\nHSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR\r\naZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA\r\nb9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ\r\nGp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV\r\nPWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO\r\npgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu\r\nUDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY\r\nMBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV\r\nHSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4\r\n9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW\r\ns47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5\r\nSm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg\r\ncLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM\r\n79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz\r\n/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt\r\nll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm\r\nKf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK\r\nQbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ\r\nw/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi\r\nS9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07\r\nmKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation\r\n# Subject: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation\r\n# Label: "SSL.com EV Root Certification Authority ECC"\r\n# Serial: 3182246526754555285\r\n# MD5 Fingerprint: 59:53:22:65:83:42:01:54:c0:ce:42:b9:5a:7c:f2:90\r\n# SHA1 Fingerprint: 4c:dd:51:a3:d1:f5:20:32:14:b0:c6:c5:32:23:03:91:c7:46:42:6d\r\n# SHA256 Fingerprint: 22:a2:c1:f7:bd:ed:70:4c:c1:e7:01:b5:f4:08:c3:10:88:0f:e9:56:b5:de:2a:4a:44:f9:9c:87:3a:25:a7:c8\r\n-----BEGIN CERTIFICATE-----\r\nMIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC\r\nVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T\r\nU0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp\r\nY2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx\r\nNTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv\r\ndXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv\r\nbSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49\r\nAgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA\r\nVIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku\r\nWnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP\r\nMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX\r\n5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ\r\nytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg\r\nh5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6\r\n# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6\r\n# Label: "GlobalSign Root CA - R6"\r\n# Serial: 1417766617973444989252670301619537\r\n# MD5 Fingerprint: 4f:dd:07:e4:d4:22:64:39:1e:0c:37:42:ea:d1:c6:ae\r\n# SHA1 Fingerprint: 80:94:64:0e:b5:a7:a1:ca:11:9c:1f:dd:d5:9f:81:02:63:a7:fb:d1\r\n# SHA256 Fingerprint: 2c:ab:ea:fe:37:d0:6c:a2:2a:ba:73:91:c0:03:3d:25:98:29:52:c4:53:64:73:49:76:3a:3a:b5:ad:6c:cf:69\r\n-----BEGIN CERTIFICATE-----\r\nMIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg\r\nMB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh\r\nbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx\r\nMjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET\r\nMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ\r\nKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI\r\nxutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k\r\nZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD\r\naNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw\r\nLnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw\r\n1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX\r\nk7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2\r\nSXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h\r\nbguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n\r\nWUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY\r\nrZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce\r\nMgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD\r\nAQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu\r\nbAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN\r\nnsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt\r\nIxg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61\r\n55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj\r\nvUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf\r\ncDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz\r\noHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp\r\nnOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs\r\npA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v\r\nJJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R\r\n8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4\r\n5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed\r\n# Subject: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed\r\n# Label: "OISTE WISeKey Global Root GC CA"\r\n# Serial: 44084345621038548146064804565436152554\r\n# MD5 Fingerprint: a9:d6:b9:2d:2f:93:64:f8:a5:69:ca:91:e9:68:07:23\r\n# SHA1 Fingerprint: e0:11:84:5e:34:de:be:88:81:b9:9c:f6:16:26:d1:96:1f:c3:b9:31\r\n# SHA256 Fingerprint: 85:60:f9:1c:36:24:da:ba:95:70:b5:fe:a0:db:e3:6f:f1:1a:83:23:be:94:86:85:4f:b3:f3:4a:55:71:19:8d\r\n-----BEGIN CERTIFICATE-----\r\nMIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw\r\nCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91\r\nbmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg\r\nUm9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRaFw00MjA1MDkwOTU4MzNaMG0xCzAJ\r\nBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBGb3Vu\r\nZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2JhbCBS\r\nb290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4ni\r\neUqjFqdrVCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4W\r\np2OQ0jnUsYd4XxiWD1AbNTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8E\r\nBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T\r\nrYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV\r\n57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg\r\nMgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=GTS Root R1 O=Google Trust Services LLC\r\n# Subject: CN=GTS Root R1 O=Google Trust Services LLC\r\n# Label: "GTS Root R1"\r\n# Serial: 146587175971765017618439757810265552097\r\n# MD5 Fingerprint: 82:1a:ef:d4:d2:4a:f2:9f:e2:3d:97:06:14:70:72:85\r\n# SHA1 Fingerprint: e1:c9:50:e6:ef:22:f8:4c:56:45:72:8b:92:20:60:d7:d5:a7:a3:e8\r\n# SHA256 Fingerprint: 2a:57:54:71:e3:13:40:bc:21:58:1c:bd:2c:f1:3e:15:84:63:20:3e:ce:94:bc:f9:d3:cc:19:6b:f0:9a:54:72\r\n-----BEGIN CERTIFICATE-----\r\nMIIFWjCCA0KgAwIBAgIQbkepxUtHDA3sM9CJuRz04TANBgkqhkiG9w0BAQwFADBH\r\nMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM\r\nQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIy\r\nMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNl\r\ncnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEB\r\nAQUAA4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaM\r\nf/vo27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vX\r\nmX7wCl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7\r\nzUjwTcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0P\r\nfyblqAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtc\r\nvfaHszVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4\r\nZor8Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUsp\r\nzBmkMiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOO\r\nRc92wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYW\r\nk70paDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+\r\nDVrNVjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgF\r\nlQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\r\nHQ4EFgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBADiW\r\nCu49tJYeX++dnAsznyvgyv3SjgofQXSlfKqE1OXyHuY3UjKcC9FhHb8owbZEKTV1\r\nd5iyfNm9dKyKaOOpMQkpAWBz40d8U6iQSifvS9efk+eCNs6aaAyC58/UEBZvXw6Z\r\nXPYfcX3v73svfuo21pdwCxXu11xWajOl40k4DLh9+42FpLFZXvRq4d2h9mREruZR\r\ngyFmxhE+885H7pwoHyXa/6xmld01D1zvICxi/ZG6qcz8WpyTgYMpl0p8WnK0OdC3\r\nd8t5/Wk6kjftbjhlRn7pYL15iJdfOBL07q9bgsiG1eGZbYwE8na6SfZu6W0eX6Dv\r\nJ4J2QPim01hcDyxC2kLGe4g0x8HYRZvBPsVhHdljUEn2NIVq4BjFbkerQUIpm/Zg\r\nDdIx02OYI5NaAIFItO/Nis3Jz5nu2Z6qNuFoS3FJFDYoOj0dzpqPJeaAcWErtXvM\r\n+SUWgeExX6GjfhaknBZqlxi9dnKlC54dNuYvoS++cJEPqOba+MSSQGwlfnuzCdyy\r\nF62ARPBopY+Udf90WuioAnwMCeKpSwughQtiue+hMZL77/ZRBIls6Kl0obsXs7X9\r\nSQ98POyDGCBDTtWTurQ0sR8WNh8M5mQ5Fkzc4P4dyKliPUDqysU0ArSuiYgzNdws\r\nE3PYJ/HQcu51OyLemGhmW/HGY0dVHLqlCFF1pkgl\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=GTS Root R2 O=Google Trust Services LLC\r\n# Subject: CN=GTS Root R2 O=Google Trust Services LLC\r\n# Label: "GTS Root R2"\r\n# Serial: 146587176055767053814479386953112547951\r\n# MD5 Fingerprint: 44:ed:9a:0e:a4:09:3b:00:f2:ae:4c:a3:c6:61:b0:8b\r\n# SHA1 Fingerprint: d2:73:96:2a:2a:5e:39:9f:73:3f:e1:c7:1e:64:3f:03:38:34:fc:4d\r\n# SHA256 Fingerprint: c4:5d:7b:b0:8e:6d:67:e6:2e:42:35:11:0b:56:4e:5f:78:fd:92:ef:05:8c:84:0a:ea:4e:64:55:d7:58:5c:60\r\n-----BEGIN CERTIFICATE-----\r\nMIIFWjCCA0KgAwIBAgIQbkepxlqz5yDFMJo/aFLybzANBgkqhkiG9w0BAQwFADBH\r\nMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM\r\nQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIy\r\nMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNl\r\ncnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEB\r\nAQUAA4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3Lv\r\nCvptnfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3Kg\r\nGjSY6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9Bu\r\nXvAuMC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOd\r\nre7kRXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXu\r\nPuWgf9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1\r\nmKPV+3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K\r\n8YzodDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqj\r\nx5RWIr9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsR\r\nnTKaG73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0\r\nkzCqgc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9Ok\r\ntwIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\r\nHQ4EFgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBALZp\r\n8KZ3/p7uC4Gt4cCpx/k1HUCCq+YEtN/L9x0Pg/B+E02NjO7jMyLDOfxA325BS0JT\r\nvhaI8dI4XsRomRyYUpOM52jtG2pzegVATX9lO9ZY8c6DR2Dj/5epnGB3GFW1fgiT\r\nz9D2PGcDFWEJ+YF59exTpJ/JjwGLc8R3dtyDovUMSRqodt6Sm2T4syzFJ9MHwAiA\r\npJiS4wGWAqoC7o87xdFtCjMwc3i5T1QWvwsHoaRc5svJXISPD+AVdyx+Jn7axEvb\r\npxZ3B7DNdehyQtaVhJ2Gg/LkkM0JR9SLA3DaWsYDQvTtN6LwG1BUSw7YhN4ZKJmB\r\nR64JGz9I0cNv4rBgF/XuIwKl2gBbbZCr7qLpGzvpx0QnRY5rn/WkhLx3+WuXrD5R\r\nRaIRpsyF7gpo8j5QOHokYh4XIDdtak23CZvJ/KRY9bb7nE4Yu5UC56GtmwfuNmsk\r\n0jmGwZODUNKBRqhfYlcsu2xkiAhu7xNUX90txGdj08+JN7+dIPT7eoOboB6BAFDC\r\n5AwiWVIQ7UNWhwD4FFKnHYuTjKJNRn8nxnGbJN7k2oaLDX5rIMHAnuFl2GqjpuiF\r\nizoHCBy69Y9Vmhh1fuXsgWbRIXOhNUQLgD1bnF5vKheW0YMjiGZt5obicDIvUiLn\r\nyOd/xCxgXS/Dr55FBcOEArf9LAhST4Ldo/DUhgkC\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=GTS Root R3 O=Google Trust Services LLC\r\n# Subject: CN=GTS Root R3 O=Google Trust Services LLC\r\n# Label: "GTS Root R3"\r\n# Serial: 146587176140553309517047991083707763997\r\n# MD5 Fingerprint: 1a:79:5b:6b:04:52:9c:5d:c7:74:33:1b:25:9a:f9:25\r\n# SHA1 Fingerprint: 30:d4:24:6f:07:ff:db:91:89:8a:0b:e9:49:66:11:eb:8c:5e:46:e5\r\n# SHA256 Fingerprint: 15:d5:b8:77:46:19:ea:7d:54:ce:1c:a6:d0:b0:c4:03:e0:37:a9:17:f1:31:e8:a0:4e:1e:6b:7a:71:ba:bc:e5\r\n-----BEGIN CERTIFICATE-----\r\nMIICDDCCAZGgAwIBAgIQbkepx2ypcyRAiQ8DVd2NHTAKBggqhkjOPQQDAzBHMQsw\r\nCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU\r\nMBIGA1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw\r\nMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp\r\nY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQA\r\nIgNiAAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout\r\n736GjOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2A\r\nDDL24CejQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud\r\nDgQWBBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEAgFuk\r\nfCPAlaUs3L6JbyO5o91lAFJekazInXJ0glMLfalAvWhgxeG4VDvBNhcl2MG9AjEA\r\nnjWSdIUlUfUk7GRSJFClH9voy8l27OyCbvWFGFPouOOaKaqW04MjyaR7YbPMAuhd\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=GTS Root R4 O=Google Trust Services LLC\r\n# Subject: CN=GTS Root R4 O=Google Trust Services LLC\r\n# Label: "GTS Root R4"\r\n# Serial: 146587176229350439916519468929765261721\r\n# MD5 Fingerprint: 5d:b6:6a:c4:60:17:24:6a:1a:99:a8:4b:ee:5e:b4:26\r\n# SHA1 Fingerprint: 2a:1d:60:27:d9:4a:b1:0a:1c:4d:91:5c:cd:33:a0:cb:3e:2d:54:cb\r\n# SHA256 Fingerprint: 71:cc:a5:39:1f:9e:79:4b:04:80:25:30:b3:63:e1:21:da:8a:30:43:bb:26:66:2f:ea:4d:ca:7f:c9:51:a4:bd\r\n-----BEGIN CERTIFICATE-----\r\nMIICCjCCAZGgAwIBAgIQbkepyIuUtui7OyrYorLBmTAKBggqhkjOPQQDAzBHMQsw\r\nCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU\r\nMBIGA1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw\r\nMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp\r\nY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQA\r\nIgNiAATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzu\r\nhXyiQHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/l\r\nxKvRHYqjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud\r\nDgQWBBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNnADBkAjBqUFJ0\r\nCMRw3J5QdCHojXohw0+WbhXRIjVhLfoIN+4Zba3bssx9BzT1YBkstTTZbyACMANx\r\nsbqjYAuG7ZoIapVon+Kz4ZNkfF6Tpt95LY2F45TPI11xzPKwTdb+mciUqXWi4w==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=UCA Global G2 Root O=UniTrust\r\n# Subject: CN=UCA Global G2 Root O=UniTrust\r\n# Label: "UCA Global G2 Root"\r\n# Serial: 124779693093741543919145257850076631279\r\n# MD5 Fingerprint: 80:fe:f0:c4:4a:f0:5c:62:32:9f:1c:ba:78:a9:50:f8\r\n# SHA1 Fingerprint: 28:f9:78:16:19:7a:ff:18:25:18:aa:44:fe:c1:a0:ce:5c:b6:4c:8a\r\n# SHA256 Fingerprint: 9b:ea:11:c9:76:fe:01:47:64:c1:be:56:a6:f9:14:b5:a5:60:31:7a:bd:99:88:39:33:82:e5:16:1a:a0:49:3c\r\n-----BEGIN CERTIFICATE-----\r\nMIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9\r\nMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBH\r\nbG9iYWwgRzIgUm9vdDAeFw0xNjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0x\r\nCzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlUcnVzdDEbMBkGA1UEAwwSVUNBIEds\r\nb2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxeYr\r\nb3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmToni9\r\nkmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzm\r\nVHqUwCoV8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/R\r\nVogvGjqNO7uCEeBHANBSh6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDc\r\nC/Vkw85DvG1xudLeJ1uK6NjGruFZfc8oLTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIj\r\ntm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/R+zvWr9LesGtOxdQXGLY\r\nD0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBeKW4bHAyv\r\nj5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6Dl\r\nNaBa4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6\r\niIis7nCs+dwp4wwcOxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznP\r\nO6Q0ibd5Ei9Hxeepl2n8pndntd978XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/\r\nBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIHEjMz15DD/pQwIX4wV\r\nZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo5sOASD0Ee/oj\r\nL3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5\r\n1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl\r\n1qnN3e92mI0ADs0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oU\r\nb3n09tDh05S60FdRvScFDcH9yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LV\r\nPtateJLbXDzz2K36uGt/xDYotgIVilQsnLAXc47QN6MUPJiVAAwpBVueSUmxX8fj\r\ny88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHojhJi6IjMtX9Gl8Cb\r\nEGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZkbxqg\r\nDMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI\r\n+Vg7RE+xygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGy\r\nYiGqhkCyLmTTX8jjfhFnRR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bX\r\nUB+K+wb1whnw0A==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=UCA Extended Validation Root O=UniTrust\r\n# Subject: CN=UCA Extended Validation Root O=UniTrust\r\n# Label: "UCA Extended Validation Root"\r\n# Serial: 106100277556486529736699587978573607008\r\n# MD5 Fingerprint: a1:f3:5f:43:c6:34:9b:da:bf:8c:7e:05:53:ad:96:e2\r\n# SHA1 Fingerprint: a3:a1:b0:6f:24:61:23:4a:e3:36:a5:c2:37:fc:a6:ff:dd:f0:d7:3a\r\n# SHA256 Fingerprint: d4:3a:f9:b3:54:73:75:5c:96:84:fc:06:d7:d8:cb:70:ee:5c:28:e7:73:fb:29:4e:b4:1e:e7:17:22:92:4d:24\r\n-----BEGIN CERTIFICATE-----\r\nMIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBH\r\nMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBF\r\neHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMx\r\nMDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNV\r\nBAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIiMA0GCSqGSIb3DQEB\r\nAQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrsiWog\r\nD4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvS\r\nsPGP2KxFRv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aop\r\nO2z6+I9tTcg1367r3CTueUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dk\r\nsHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR59mzLC52LqGj3n5qiAno8geK+LLNEOfi\r\nc0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH0mK1lTnj8/FtDw5lhIpj\r\nVMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KRel7sFsLz\r\nKuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/\r\nTuDvB0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41G\r\nsx2VYVdWf6/wFlthWG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs\r\n1+lvK9JKBZP8nm9rZ/+I8U6laUpSNwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQD\r\nfwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS3H5aBZ8eNJr34RQwDwYDVR0T\r\nAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBADaN\r\nl8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR\r\nap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQ\r\nVBcZEhrxH9cMaVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5\r\nc6sq1WnIeJEmMX3ixzDx/BR4dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp\r\n4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb+7lsq+KePRXBOy5nAliRn+/4Qh8s\r\nt2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOWF3sGPjLtx7dCvHaj\r\n2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwiGpWO\r\nvpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2C\r\nxR9GUeOcGMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmx\r\ncmtpzyKEC2IPrNkZAJSidjzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbM\r\nfjKaiJUINlK73nZfdklJrX+9ZSCyycErdhh2n1ax\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036\r\n# Subject: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036\r\n# Label: "Certigna Root CA"\r\n# Serial: 269714418870597844693661054334862075617\r\n# MD5 Fingerprint: 0e:5c:30:62:27:eb:5b:bc:d7:ae:62:ba:e9:d5:df:77\r\n# SHA1 Fingerprint: 2d:0d:52:14:ff:9e:ad:99:24:01:74:20:47:6e:6c:85:27:27:f5:43\r\n# SHA256 Fingerprint: d4:8d:3d:23:ee:db:50:a4:59:e5:51:97:60:1c:27:77:4b:9d:7b:18:c9:4d:5a:05:95:11:a1:02:50:b9:31:68\r\n-----BEGIN CERTIFICATE-----\r\nMIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw\r\nWjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw\r\nMiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x\r\nMzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjdaMFoxCzAJBgNVBAYTAkZSMRIwEAYD\r\nVQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYzMDgxMDAwMzYxGTAX\r\nBgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw\r\nggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sO\r\nty3tRQgXstmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9M\r\nCiBtnyN6tMbaLOQdLNyzKNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPu\r\nI9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8JXrJhFwLrN1CTivngqIkicuQstDuI7pm\r\nTLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16XdG+RCYyKfHx9WzMfgIh\r\nC59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq4NYKpkDf\r\nePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3Yz\r\nIoejwpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWT\r\nCo/1VTp2lc5ZmIoJlXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1k\r\nJWumIWmbat10TWuXekG9qxf5kBdIjzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5\r\nhwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp//TBt2dzhauH8XwIDAQABo4IB\r\nGjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE\r\nFBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of\r\n1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczov\r\nL3d3d3cuY2VydGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilo\r\ndHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYr\r\naHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hcm9vdGNhLmNybDANBgkq\r\nhkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOItOoldaDgvUSILSo3L\r\n6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxPTGRG\r\nHVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH6\r\n0BGM+RFq7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncB\r\nlA2c5uk5jR+mUYyZDDl34bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdi\r\no2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1\r\ngPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS6Cvu5zHbugRqh5jnxV/v\r\nfaci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63\r\nNwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh\r\njWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw\r\n3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI\r\n# Subject: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI\r\n# Label: "emSign Root CA - G1"\r\n# Serial: 235931866688319308814040\r\n# MD5 Fingerprint: 9c:42:84:57:dd:cb:0b:a7:2e:95:ad:b6:f3:da:bc:ac\r\n# SHA1 Fingerprint: 8a:c7:ad:8f:73:ac:4e:c1:b5:75:4d:a5:40:f4:fc:cf:7c:b5:8e:8c\r\n# SHA256 Fingerprint: 40:f6:af:03:46:a9:9a:a1:cd:1d:55:5a:4e:9c:ce:62:c7:f9:63:46:03:ee:40:66:15:83:3d:c8:c8:d0:03:67\r\n-----BEGIN CERTIFICATE-----\r\nMIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD\r\nVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU\r\nZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH\r\nMTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO\r\nMRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv\r\nZ2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN\r\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz\r\nf2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO\r\n8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq\r\nd7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM\r\ntTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt\r\nOd9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB\r\no0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD\r\nAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x\r\nPaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM\r\nwiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d\r\nGNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH\r\n6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby\r\nRQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx\r\niN66zB+Afko=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI\r\n# Subject: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI\r\n# Label: "emSign ECC Root CA - G3"\r\n# Serial: 287880440101571086945156\r\n# MD5 Fingerprint: ce:0b:72:d1:9f:88:8e:d0:50:03:e8:e3:b8:8b:67:40\r\n# SHA1 Fingerprint: 30:43:fa:4f:f2:57:dc:a0:c3:80:ee:2e:58:ea:78:b2:3f:e6:bb:c1\r\n# SHA256 Fingerprint: 86:a1:ec:ba:08:9c:4a:8d:3b:be:27:34:c6:12:ba:34:1d:81:3e:04:3c:f9:e8:a8:62:cd:5c:57:a3:6b:be:6b\r\n-----BEGIN CERTIFICATE-----\r\nMIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG\r\nEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo\r\nbm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g\r\nRzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ\r\nTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s\r\nb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw\r\ndjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0\r\nWXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS\r\nfvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB\r\nzhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq\r\nhkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB\r\nCUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD\r\n+JbNR6iC8hZVdyR+EhCVBCyj\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI\r\n# Subject: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI\r\n# Label: "emSign Root CA - C1"\r\n# Serial: 825510296613316004955058\r\n# MD5 Fingerprint: d8:e3:5d:01:21:fa:78:5a:b0:df:ba:d2:ee:2a:5f:68\r\n# SHA1 Fingerprint: e7:2e:f1:df:fc:b2:09:28:cf:5d:d4:d5:67:37:b1:51:cb:86:4f:01\r\n# SHA256 Fingerprint: 12:56:09:aa:30:1d:a0:a2:49:b9:7a:82:39:cb:6a:34:21:6f:44:dc:ac:9f:39:54:b1:42:92:f2:e8:c8:60:8f\r\n-----BEGIN CERTIFICATE-----\r\nMIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG\r\nA1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg\r\nSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw\r\nMFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln\r\nbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9v\r\ndCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+upufGZ\r\nBczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZ\r\nHdPIWoU/Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH\r\n3DspVpNqs8FqOp099cGXOFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvH\r\nGPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4VI5b2P/AgNBbeCsbEBEV5f6f9vtKppa+c\r\nxSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleoomslMuoaJuvimUnzYnu3Yy1\r\naylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+XJGFehiq\r\nTbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL\r\nBQADggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87\r\n/kOXSTKZEhVb3xEp/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4\r\nkqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG\r\nYQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT\r\n+xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo\r\nWXzhriKi4gp6D/piq1JM4fHfyr6DDUI=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI\r\n# Subject: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI\r\n# Label: "emSign ECC Root CA - C3"\r\n# Serial: 582948710642506000014504\r\n# MD5 Fingerprint: 3e:53:b3:a3:81:ee:d7:10:f8:d3:b0:1d:17:92:f5:d5\r\n# SHA1 Fingerprint: b6:af:43:c2:9b:81:53:7d:f6:ef:6b:c3:1f:1f:60:15:0c:ee:48:66\r\n# SHA256 Fingerprint: bc:4d:80:9b:15:18:9d:78:db:3e:1d:8c:f4:f9:72:6a:79:5d:a1:64:3c:a5:f1:35:8e:1d:db:0e:dc:0d:7e:b3\r\n-----BEGIN CERTIFICATE-----\r\nMIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG\r\nEwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx\r\nIDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw\r\nMFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln\r\nbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQDExdlbVNpZ24gRUND\r\nIFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd6bci\r\nMK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4Ojavti\r\nsIGJAnB9SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0O\r\nBBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB\r\nAf8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c\r\n3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J\r\n0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Hongkong Post Root CA 3 O=Hongkong Post\r\n# Subject: CN=Hongkong Post Root CA 3 O=Hongkong Post\r\n# Label: "Hongkong Post Root CA 3"\r\n# Serial: 46170865288971385588281144162979347873371282084\r\n# MD5 Fingerprint: 11:fc:9f:bd:73:30:02:8a:fd:3f:f3:58:b9:cb:20:f0\r\n# SHA1 Fingerprint: 58:a2:d0:ec:20:52:81:5b:c1:f3:f8:64:02:24:4e:c2:8e:02:4b:02\r\n# SHA256 Fingerprint: 5a:2f:c0:3f:0c:83:b0:90:bb:fa:40:60:4b:09:88:44:6c:76:36:18:3d:f9:84:6e:17:10:1a:44:7f:b8:ef:d6\r\n-----BEGIN CERTIFICATE-----\r\nMIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL\r\nBQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ\r\nSG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n\r\na29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2MDMwMjI5NDZaFw00MjA2MDMwMjI5\r\nNDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtvbmcxEjAQBgNVBAcT\r\nCUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMXSG9u\r\nZ2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\r\nAoICAQCziNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFO\r\ndem1p+/l6TWZ5Mwc50tfjTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mI\r\nVoBc+L0sPOFMV4i707mV78vH9toxdCim5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV\r\n9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOesL4jpNrcyCse2m5FHomY\r\n2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj0mRiikKY\r\nvLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+Tt\r\nbNe/JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZb\r\nx39ri1UbSsUgYT2uy1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+\r\nl2oBlKN8W4UdKjk60FSh0Tlxnf0h+bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YK\r\nTE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsGxVd7GYYKecsAyVKvQv83j+Gj\r\nHno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwIDAQABo2MwYTAP\r\nBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e\r\ni9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEw\r\nDQYJKoZIhvcNAQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG\r\n7BJ8dNVI0lkUmcDrudHr9EgwW62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCk\r\nMpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWldy8joRTnU+kLBEUx3XZL7av9YROXr\r\ngZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov+BS5gLNdTaqX4fnk\r\nGMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDceqFS\r\n3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJm\r\nOzj/2ZQw9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+\r\nl6mc1X5VTMbeRRAc6uk7nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6c\r\nJfTzPV4e0hz5sy229zdcxsshTrD3mUcYhcErulWuBurQB7Lcq9CClnXO0lD+mefP\r\nL5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa\r\nLJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG\r\nmpv0\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Entrust Root Certification Authority - G4 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2015 Entrust, Inc. - for authorized use only\r\n# Subject: CN=Entrust Root Certification Authority - G4 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2015 Entrust, Inc. - for authorized use only\r\n# Label: "Entrust Root Certification Authority - G4"\r\n# Serial: 289383649854506086828220374796556676440\r\n# MD5 Fingerprint: 89:53:f1:83:23:b7:7c:8e:05:f1:8c:71:38:4e:1f:88\r\n# SHA1 Fingerprint: 14:88:4e:86:26:37:b0:26:af:59:62:5c:40:77:ec:35:29:ba:96:01\r\n# SHA256 Fingerprint: db:35:17:d1:f6:73:2a:2d:5a:b9:7c:53:3e:c7:07:79:ee:32:70:a6:2f:b4:ac:42:38:37:24:60:e6:f0:1e:88\r\n-----BEGIN CERTIFICATE-----\r\nMIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAw\r\ngb4xCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQL\r\nEx9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykg\r\nMjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAw\r\nBgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0\r\nMB4XDTE1MDUyNzExMTExNloXDTM3MTIyNzExNDExNlowgb4xCzAJBgNVBAYTAlVT\r\nMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1\r\nc3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJ\r\nbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3Qg\r\nUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0MIICIjANBgkqhkiG9w0B\r\nAQEFAAOCAg8AMIICCgKCAgEAsewsQu7i0TD/pZJH4i3DumSXbcr3DbVZwbPLqGgZ\r\n2K+EbTBwXX7zLtJTmeH+H17ZSK9dE43b/2MzTdMAArzE+NEGCJR5WIoV3imz/f3E\r\nT+iq4qA7ec2/a0My3dl0ELn39GjUu9CH1apLiipvKgS1sqbHoHrmSKvS0VnM1n4j\r\n5pds8ELl3FFLFUHtSUrJ3hCX1nbB76W1NhSXNdh4IjVS70O92yfbYVaCNNzLiGAM\r\nC1rlLAHGVK/XqsEQe9IFWrhAnoanw5CGAlZSCXqc0ieCU0plUmr1POeo8pyvi73T\r\nDtTUXm6Hnmo9RR3RXRv06QqsYJn7ibT/mCzPfB3pAqoEmh643IhuJbNsZvc8kPNX\r\nwbMv9W3y+8qh+CmdRouzavbmZwe+LGcKKh9asj5XxNMhIWNlUpEbsZmOeX7m640A\r\n2Vqq6nPopIICR5b+W45UYaPrL0swsIsjdXJ8ITzI9vF01Bx7owVV7rtNOzK+mndm\r\nnqxpkCIHH2E6lr7lmk/MBTwoWdPBDFSoWWG9yHJM6Nyfh3+9nEg2XpWjDrk4JFX8\r\ndWbrAuMINClKxuMrLzOg2qOGpRKX/YAr2hRC45K9PvJdXmd0LhyIRyk0X+IyqJwl\r\nN4y6mACXi0mWHv0liqzc2thddG5msP9E36EYxr5ILzeUePiVSj9/E15dWf10hkNj\r\nc0kCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD\r\nVR0OBBYEFJ84xFYjwznooHFs6FRM5Og6sb9nMA0GCSqGSIb3DQEBCwUAA4ICAQAS\r\n5UKme4sPDORGpbZgQIeMJX6tuGguW8ZAdjwD+MlZ9POrYs4QjbRaZIxowLByQzTS\r\nGwv2LFPSypBLhmb8qoMi9IsabyZIrHZ3CL/FmFz0Jomee8O5ZDIBf9PD3Vht7LGr\r\nhFV0d4QEJ1JrhkzO3bll/9bGXp+aEJlLdWr+aumXIOTkdnrG0CSqkM0gkLpHZPt/\r\nB7NTeLUKYvJzQ85BK4FqLoUWlFPUa19yIqtRLULVAJyZv967lDtX/Zr1hstWO1uI\r\nAeV8KEsD+UmDfLJ/fOPtjqF/YFOOVZ1QNBIPt5d7bIdKROf1beyAN/BYGW5KaHbw\r\nH5Lk6rWS02FREAutp9lfx1/cH6NcjKF+m7ee01ZvZl4HliDtC3T7Zk6LERXpgUl+\r\nb7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKWRGhXxNUzzxkvFMSUHHuk\r\n2fCfDrGA4tGeEWSpiBE6doLlYsKA2KSD7ZPvfC+QsDJMlhVoSFLUmQjAJOgc47Ol\r\nIQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk\r\n5F6G+TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuY\r\nn/PIjhs4ViFqUZPTkcpG2om3PVODLAgfi49T3f+sHw==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation\r\n# Subject: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation\r\n# Label: "Microsoft ECC Root Certificate Authority 2017"\r\n# Serial: 136839042543790627607696632466672567020\r\n# MD5 Fingerprint: dd:a1:03:e6:4a:93:10:d1:bf:f0:19:42:cb:fe:ed:67\r\n# SHA1 Fingerprint: 99:9a:64:c3:7f:f4:7d:9f:ab:95:f1:47:69:89:14:60:ee:c4:c3:c5\r\n# SHA256 Fingerprint: 35:8d:f3:9d:76:4a:f9:e1:b7:66:e9:c9:72:df:35:2e:e1:5c:fa:c2:27:af:6a:d1:d7:0e:8e:4a:6e:dc:ba:02\r\n-----BEGIN CERTIFICATE-----\r\nMIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw\r\nCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD\r\nVQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw\r\nMTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV\r\nUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy\r\nb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq\r\nhkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR\r\nogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb\r\nhGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E\r\nBTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3\r\nFQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV\r\nL8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB\r\niudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation\r\n# Subject: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation\r\n# Label: "Microsoft RSA Root Certificate Authority 2017"\r\n# Serial: 40975477897264996090493496164228220339\r\n# MD5 Fingerprint: 10:ff:00:ff:cf:c9:f8:c7:7a:c0:ee:35:8e:c9:0f:47\r\n# SHA1 Fingerprint: 73:a5:e6:4a:3b:ff:83:16:ff:0e:dc:cc:61:8a:90:6e:4e:ae:4d:74\r\n# SHA256 Fingerprint: c7:41:f7:0f:4b:2a:8d:88:bf:2e:71:c1:41:22:ef:53:ef:10:eb:a0:cf:a5:e6:4c:fa:20:f4:18:85:30:73:e0\r\n-----BEGIN CERTIFICATE-----\r\nMIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl\r\nMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw\r\nNAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5\r\nIDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG\r\nEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N\r\naWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi\r\nMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ\r\nNt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0\r\nZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1\r\nHLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm\r\ngGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ\r\njEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc\r\naDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG\r\nYaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6\r\nW6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K\r\nUGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH\r\n+FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q\r\nW5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/\r\nBAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC\r\nNxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC\r\nLgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC\r\ngMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6\r\ntZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh\r\nSnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2\r\nTaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3\r\npvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR\r\nxpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp\r\nGWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9\r\ndOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN\r\nAHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB\r\nRA+GsCyRxj3qrg+E\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=e-Szigno Root CA 2017 O=Microsec Ltd.\r\n# Subject: CN=e-Szigno Root CA 2017 O=Microsec Ltd.\r\n# Label: "e-Szigno Root CA 2017"\r\n# Serial: 411379200276854331539784714\r\n# MD5 Fingerprint: de:1f:f6:9e:84:ae:a7:b4:21:ce:1e:58:7d:d1:84:98\r\n# SHA1 Fingerprint: 89:d4:83:03:4f:9e:9a:48:80:5f:72:37:d4:a9:a6:ef:cb:7c:1f:d1\r\n# SHA256 Fingerprint: be:b0:0b:30:83:9b:9b:c3:2c:32:e4:44:79:05:95:06:41:f2:64:21:b1:5e:d0:89:19:8b:51:8a:e2:ea:1b:99\r\n-----BEGIN CERTIFICATE-----\r\nMIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV\r\nBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk\r\nLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv\r\nb3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ\r\nBgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg\r\nTHRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v\r\nIFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv\r\nxie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H\r\nWyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G\r\nA1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB\r\neAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo\r\njbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ\r\n+efcMQ==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: O=CERTSIGN SA OU=certSIGN ROOT CA G2\r\n# Subject: O=CERTSIGN SA OU=certSIGN ROOT CA G2\r\n# Label: "certSIGN Root CA G2"\r\n# Serial: 313609486401300475190\r\n# MD5 Fingerprint: 8c:f1:75:8a:c6:19:cf:94:b7:f7:65:20:87:c3:97:c7\r\n# SHA1 Fingerprint: 26:f9:93:b4:ed:3d:28:27:b0:b9:4b:a7:e9:15:1d:a3:8d:92:e5:32\r\n# SHA256 Fingerprint: 65:7c:fe:2f:a7:3f:aa:38:46:25:71:f3:32:a2:36:3a:46:fc:e7:02:09:51:71:07:02:cd:fb:b6:ee:da:33:05\r\n-----BEGIN CERTIFICATE-----\r\nMIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV\r\nBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g\r\nUk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ\r\nBgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ\r\nR04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF\r\ndRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw\r\nvlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ\r\nuIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp\r\nn+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs\r\ncpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW\r\nxPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P\r\nrCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF\r\nDsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx\r\nDTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy\r\nLcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C\r\neWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB\r\n/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ\r\nd8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq\r\nkX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC\r\nb6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl\r\nqiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0\r\nOJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c\r\nNywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk\r\nltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO\r\npwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj\r\n03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk\r\nPuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE\r\n1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX\r\nQRBdJ3NghVdJIgc=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Trustwave Global Certification Authority O=Trustwave Holdings, Inc.\r\n# Subject: CN=Trustwave Global Certification Authority O=Trustwave Holdings, Inc.\r\n# Label: "Trustwave Global Certification Authority"\r\n# Serial: 1846098327275375458322922162\r\n# MD5 Fingerprint: f8:1c:18:2d:2f:ba:5f:6d:a1:6c:bc:c7:ab:91:c7:0e\r\n# SHA1 Fingerprint: 2f:8f:36:4f:e1:58:97:44:21:59:87:a5:2a:9a:d0:69:95:26:7f:b5\r\n# SHA256 Fingerprint: 97:55:20:15:f5:dd:fc:3c:87:88:c0:06:94:45:55:40:88:94:45:00:84:f1:00:86:70:86:bc:1a:2b:b5:8d:c8\r\n-----BEGIN CERTIFICATE-----\r\nMIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQsw\r\nCQYDVQQGEwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28x\r\nITAfBgNVBAoMGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1\r\nc3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMx\r\nOTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJVUzERMA8GA1UECAwI\r\nSWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2ZSBI\r\nb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZp\r\nY2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB\r\nALldUShLPDeS0YLOvR29zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0Xzn\r\nswuvCAAJWX/NKSqIk4cXGIDtiLK0thAfLdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu\r\n7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4BqstTnoApTAbqOl5F2brz8\r\n1Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9oWN0EACyW\r\n80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotP\r\nJqX+OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1l\r\nRtzuzWniTY+HKE40Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfw\r\nhI0Vcnyh78zyiGG69Gm7DIwLdVcEuE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10\r\ncoos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm+9jaJXLE9gCxInm943xZYkqc\r\nBW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqjifLJS3tBEW1n\r\ntwiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud\r\nEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1Ud\r\nDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W\r\n0OhUKDtkLSGm+J1WE2pIPU/HPinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfe\r\nuyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0HZJDmHvUqoai7PF35owgLEQzxPy0Q\r\nlG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla4gt5kNdXElE1GYhB\r\naCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5RvbbE\r\nsLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPT\r\nMaCm/zjdzyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qe\r\nqu5AvzSxnI9O4fKSTx+O856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxh\r\nVicGaeVyQYHTtgGJoC86cnn+OjC/QezHYj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8\r\nh6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu3R3y4G5OBVixwJAWKqQ9\r\nEEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP29FpHOTK\r\nyeC2nOnOcXHebD8WpHk=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Trustwave Global ECC P256 Certification Authority O=Trustwave Holdings, Inc.\r\n# Subject: CN=Trustwave Global ECC P256 Certification Authority O=Trustwave Holdings, Inc.\r\n# Label: "Trustwave Global ECC P256 Certification Authority"\r\n# Serial: 4151900041497450638097112925\r\n# MD5 Fingerprint: 5b:44:e3:8d:5d:36:86:26:e8:0d:05:d2:59:a7:83:54\r\n# SHA1 Fingerprint: b4:90:82:dd:45:0c:be:8b:5b:b1:66:d3:e2:a4:08:26:cd:ed:42:cf\r\n# SHA256 Fingerprint: 94:5b:bc:82:5e:a5:54:f4:89:d1:fd:51:a7:3d:df:2e:a6:24:ac:70:19:a0:52:05:22:5c:22:a7:8c:cf:a8:b4\r\n-----BEGIN CERTIFICATE-----\r\nMIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYD\r\nVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf\r\nBgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3\r\nYXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x\r\nNzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYDVQQGEwJVUzERMA8G\r\nA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0\r\nd2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF\r\nQ0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqG\r\nSM49AwEHA0IABH77bOYj43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoN\r\nFWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqmP62jQzBBMA8GA1UdEwEB/wQFMAMBAf8w\r\nDwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt0UrrdaVKEJmzsaGLSvcw\r\nCgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjzRM4q3wgh\r\nDDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Trustwave Global ECC P384 Certification Authority O=Trustwave Holdings, Inc.\r\n# Subject: CN=Trustwave Global ECC P384 Certification Authority O=Trustwave Holdings, Inc.\r\n# Label: "Trustwave Global ECC P384 Certification Authority"\r\n# Serial: 2704997926503831671788816187\r\n# MD5 Fingerprint: ea:cf:60:c4:3b:b9:15:29:40:a1:97:ed:78:27:93:d6\r\n# SHA1 Fingerprint: e7:f3:a3:c8:cf:6f:c3:04:2e:6d:0e:67:32:c5:9e:68:95:0d:5e:d2\r\n# SHA256 Fingerprint: 55:90:38:59:c8:c0:c3:eb:b8:75:9e:ce:4e:25:57:22:5f:f5:75:8b:bd:38:eb:d4:82:76:60:1e:1b:d5:80:97\r\n-----BEGIN CERTIFICATE-----\r\nMIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYD\r\nVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf\r\nBgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3\r\nYXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x\r\nNzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYDVQQGEwJVUzERMA8G\r\nA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0\r\nd2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF\r\nQ0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuB\r\nBAAiA2IABGvaDXU1CDFHBa5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJ\r\nj9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr/TklZvFe/oyujUF5nQlgziip04pt89ZF\r\n1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwYAMB0G\r\nA1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNnADBkAjA3\r\nAZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsC\r\nMGclCrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVu\r\nSw==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp.\r\n# Subject: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp.\r\n# Label: "NAVER Global Root Certification Authority"\r\n# Serial: 9013692873798656336226253319739695165984492813\r\n# MD5 Fingerprint: c8:7e:41:f6:25:3b:f5:09:b3:17:e8:46:3d:bf:d0:9b\r\n# SHA1 Fingerprint: 8f:6b:f2:a9:27:4a:da:14:a0:c4:f4:8e:61:27:f9:c0:1e:78:5d:d1\r\n# SHA256 Fingerprint: 88:f4:38:dc:f8:ff:d1:fa:8f:42:91:15:ff:e5:f8:2a:e1:e0:6e:0c:70:c3:75:fa:ad:71:7b:34:a4:9e:72:65\r\n-----BEGIN CERTIFICATE-----\r\nMIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM\r\nBQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG\r\nT1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0\r\naW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4NDJaFw0zNzA4MTgyMzU5NTlaMGkx\r\nCzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBD\r\nb3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBB\r\ndXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVA\r\niQqrDZBbUGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH\r\n38dq6SZeWYp34+hInDEW+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lE\r\nHoSTGEq0n+USZGnQJoViAbbJAh2+g1G7XNr4rRVqmfeSVPc0W+m/6imBEtRTkZaz\r\nkVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2aacp+yPOiNgSnABIqKYP\r\nszuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4Yb8Obtoq\r\nvC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHf\r\nnZ3zVHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaG\r\nYQ5fG8Ir4ozVu53BA0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo\r\n0es+nPxdGoMuK8u180SdOqcXYZaicdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3a\r\nCJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejyYhbLgGvtPe31HzClrkvJE+2K\r\nAQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNVHQ4EFgQU0p+I\r\n36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB\r\nAf8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoN\r\nqo0hV4/GPnrK21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatj\r\ncu3cvuzHV+YwIHHW1xDBE1UBjCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm\r\n+LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bxhYTeodoS76TiEJd6eN4MUZeoIUCL\r\nhr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTgE34h5prCy8VCZLQe\r\nlHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTHD8z7\r\np/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8\r\npiKCk5XQA76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLR\r\nLBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX\r\n5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO\r\ndh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul\r\n9XXeifdy\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres\r\n# Subject: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres\r\n# Label: "AC RAIZ FNMT-RCM SERVIDORES SEGUROS"\r\n# Serial: 131542671362353147877283741781055151509\r\n# MD5 Fingerprint: 19:36:9c:52:03:2f:d2:d1:bb:23:cc:dd:1e:12:55:bb\r\n# SHA1 Fingerprint: 62:ff:d9:9e:c0:65:0d:03:ce:75:93:d2:ed:3f:2d:32:c9:e3:e5:4a\r\n# SHA256 Fingerprint: 55:41:53:b1:3d:2c:f9:dd:b7:53:bf:be:1a:4e:0a:e0:8d:0a:a4:18:70:58:fe:60:a2:b8:62:b2:e4:b8:7b:cb\r\n-----BEGIN CERTIFICATE-----\r\nMIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw\r\nCQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw\r\nFgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S\r\nQ00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5\r\nMzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL\r\nDAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS\r\nQUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB\r\nBAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH\r\nsbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK\r\nUm8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD\r\nVR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu\r\nSuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC\r\nMQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy\r\nv+c=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=GlobalSign Root R46 O=GlobalSign nv-sa\r\n# Subject: CN=GlobalSign Root R46 O=GlobalSign nv-sa\r\n# Label: "GlobalSign Root R46"\r\n# Serial: 1552617688466950547958867513931858518042577\r\n# MD5 Fingerprint: c4:14:30:e4:fa:66:43:94:2a:6a:1b:24:5f:19:d0:ef\r\n# SHA1 Fingerprint: 53:a2:b0:4b:ca:6b:d6:45:e6:39:8a:8e:c4:0d:d2:bf:77:c3:a2:90\r\n# SHA256 Fingerprint: 4f:a3:12:6d:8d:3a:11:d1:c4:85:5a:4f:80:7c:ba:d6:cf:91:9d:3a:5a:88:b0:3b:ea:2c:63:72:d9:3c:40:c9\r\n-----BEGIN CERTIFICATE-----\r\nMIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA\r\nMEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD\r\nVQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy\r\nMDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt\r\nc2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB\r\nAQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ\r\nOaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG\r\nvGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud\r\n316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo\r\n0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE\r\ny132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF\r\nzXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE\r\n+cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN\r\nI/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs\r\nx2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa\r\nByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC\r\n4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\r\nHQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4\r\n7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg\r\nJuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti\r\n2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk\r\npnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF\r\nFRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt\r\nrWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk\r\nZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5\r\nu+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP\r\n4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6\r\nN3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3\r\nvouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=GlobalSign Root E46 O=GlobalSign nv-sa\r\n# Subject: CN=GlobalSign Root E46 O=GlobalSign nv-sa\r\n# Label: "GlobalSign Root E46"\r\n# Serial: 1552617690338932563915843282459653771421763\r\n# MD5 Fingerprint: b5:b8:66:ed:de:08:83:e3:c9:e2:01:34:06:ac:51:6f\r\n# SHA1 Fingerprint: 39:b4:6c:d5:fe:80:06:eb:e2:2f:4a:bb:08:33:a0:af:db:b9:dd:84\r\n# SHA256 Fingerprint: cb:b9:c4:4d:84:b8:04:3e:10:50:ea:31:a6:9f:51:49:55:d7:bf:d2:e2:c6:b4:93:01:01:9a:d6:1d:9f:50:58\r\n-----BEGIN CERTIFICATE-----\r\nMIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx\r\nCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD\r\nExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw\r\nMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex\r\nHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA\r\nIgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq\r\nR+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd\r\nyXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud\r\nDgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ\r\n7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8\r\n+RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=GLOBALTRUST 2020 O=e-commerce monitoring GmbH\r\n# Subject: CN=GLOBALTRUST 2020 O=e-commerce monitoring GmbH\r\n# Label: "GLOBALTRUST 2020"\r\n# Serial: 109160994242082918454945253\r\n# MD5 Fingerprint: 8a:c7:6f:cb:6d:e3:cc:a2:f1:7c:83:fa:0e:78:d7:e8\r\n# SHA1 Fingerprint: d0:67:c1:13:51:01:0c:aa:d0:c7:6a:65:37:31:16:26:4f:53:71:a2\r\n# SHA256 Fingerprint: 9a:29:6a:51:82:d1:d4:51:a2:e3:7f:43:9b:74:da:af:a2:67:52:33:29:f9:0f:9a:0d:20:07:c3:34:e2:3c:9a\r\n-----BEGIN CERTIFICATE-----\r\nMIIFgjCCA2qgAwIBAgILWku9WvtPilv6ZeUwDQYJKoZIhvcNAQELBQAwTTELMAkG\r\nA1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkw\r\nFwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMB4XDTIwMDIxMDAwMDAwMFoXDTQwMDYx\r\nMDAwMDAwMFowTTELMAkGA1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9u\r\naXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMIICIjANBgkq\r\nhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAri5WrRsc7/aVj6B3GyvTY4+ETUWiD59b\r\nRatZe1E0+eyLinjF3WuvvcTfk0Uev5E4C64OFudBc/jbu9G4UeDLgztzOG53ig9Z\r\nYybNpyrOVPu44sB8R85gfD+yc/LAGbaKkoc1DZAoouQVBGM+uq/ufF7MpotQsjj3\r\nQWPKzv9pj2gOlTblzLmMCcpL3TGQlsjMH/1WljTbjhzqLL6FLmPdqqmV0/0plRPw\r\nyJiT2S0WR5ARg6I6IqIoV6Lr/sCMKKCmfecqQjuCgGOlYx8ZzHyyZqjC0203b+J+\r\nBlHZRYQfEs4kUmSFC0iAToexIiIwquuuvuAC4EDosEKAA1GqtH6qRNdDYfOiaxaJ\r\nSaSjpCuKAsR49GiKweR6NrFvG5Ybd0mN1MkGco/PU+PcF4UgStyYJ9ORJitHHmkH\r\nr96i5OTUawuzXnzUJIBHKWk7buis/UDr2O1xcSvy6Fgd60GXIsUf1DnQJ4+H4xj0\r\n4KlGDfV0OoIu0G4skaMxXDtG6nsEEFZegB31pWXogvziB4xiRfUg3kZwhqG8k9Me\r\ndKZssCz3AwyIDMvUclOGvGBG85hqwvG/Q/lwIHfKN0F5VVJjjVsSn8VoxIidrPIw\r\nq7ejMZdnrY8XD2zHc+0klGvIg5rQmjdJBKuxFshsSUktq6HQjJLyQUp5ISXbY9e2\r\nnKd+Qmn7OmMCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\r\nAQYwHQYDVR0OBBYEFNwuH9FhN3nkq9XVsxJxaD1qaJwiMB8GA1UdIwQYMBaAFNwu\r\nH9FhN3nkq9XVsxJxaD1qaJwiMA0GCSqGSIb3DQEBCwUAA4ICAQCR8EICaEDuw2jA\r\nVC/f7GLDw56KoDEoqoOOpFaWEhCGVrqXctJUMHytGdUdaG/7FELYjQ7ztdGl4wJC\r\nXtzoRlgHNQIw4Lx0SsFDKv/bGtCwr2zD/cuz9X9tAy5ZVp0tLTWMstZDFyySCstd\r\n6IwPS3BD0IL/qMy/pJTAvoe9iuOTe8aPmxadJ2W8esVCgmxcB9CpwYhgROmYhRZf\r\n+I/KARDOJcP5YBugxZfD0yyIMaK9MOzQ0MAS8cE54+X1+NZK3TTN+2/BT+MAi1bi\r\nkvcoskJ3ciNnxz8RFbLEAwW+uxF7Cr+obuf/WEPPm2eggAe2HcqtbepBEX4tdJP7\r\nwry+UUTF72glJ4DjyKDUEuzZpTcdN3y0kcra1LGWge9oXHYQSa9+pTeAsRxSvTOB\r\nTI/53WXZFM2KJVj04sWDpQmQ1GwUY7VA3+vA/MRYfg0UFodUJ25W5HCEuGwyEn6C\r\nMUO+1918oa2u1qsgEu8KwxCMSZY13At1XrFP1U80DhEgB3VDRemjEdqso5nCtnkn\r\n4rnvyOL2NSl6dPrFf4IFYqYK6miyeUcGbvJXqBUzxvd4Sj1Ce2t+/vdG6tHrju+I\r\naFvowdlxfv1k7/9nR4hYJS8+hge9+6jlgqispdNpQ80xiEmEU5LAsTkbOYMBMMTy\r\nqfrQA71yN2BWHzZ8vTmR9W0Nv3vXkg==\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz\r\n# Subject: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz\r\n# Label: "ANF Secure Server Root CA"\r\n# Serial: 996390341000653745\r\n# MD5 Fingerprint: 26:a6:44:5a:d9:af:4e:2f:b2:1d:b6:65:b0:4e:e8:96\r\n# SHA1 Fingerprint: 5b:6e:68:d0:cc:15:b6:a0:5f:1e:c1:5f:ae:02:fc:6b:2f:5d:6f:74\r\n# SHA256 Fingerprint: fb:8f:ec:75:91:69:b9:10:6b:1e:51:16:44:c6:18:c5:13:04:37:3f:6c:06:43:08:8d:8b:ef:fd:1b:99:75:99\r\n-----BEGIN CERTIFICATE-----\r\nMIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNV\r\nBAUTCUc2MzI4NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlk\r\nYWQgZGUgQ2VydGlmaWNhY2lvbjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNV\r\nBAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3QgQ0EwHhcNMTkwOTA0MTAwMDM4WhcN\r\nMzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEwMQswCQYDVQQGEwJF\r\nUzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQwEgYD\r\nVQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9v\r\ndCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCj\r\ncqQZAZ2cC4Ffc0m6p6zzBE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9q\r\nyGFOtibBTI3/TO80sh9l2Ll49a2pcbnvT1gdpd50IJeh7WhM3pIXS7yr/2WanvtH\r\n2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcvB2VSAKduyK9o7PQUlrZX\r\nH1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXsezx76W0OL\r\nzc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyR\r\np1RMVwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQz\r\nW7i1o0TJrH93PB0j7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/\r\nSiOL9V8BY9KHcyi1Swr1+KuCLH5zJTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJn\r\nLNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe8TZBAQIvfXOn3kLMTOmJDVb3\r\nn5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVOHj1tyRRM4y5B\r\nu8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj\r\no1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAO\r\nBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC\r\nAgEATh65isagmD9uw2nAalxJUqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L\r\n9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzxj6ptBZNscsdW699QIyjlRRA96Gej\r\nrw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDtdD+4E5UGUcjohybK\r\npFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM5gf0\r\nvPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjq\r\nOknkJjCb5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ\r\n/zo1PqVUSlJZS2Db7v54EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ9\r\n2zg/LFis6ELhDtjTO0wugumDLmsx2d1Hhk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI\r\n+PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGyg77FGr8H6lnco4g175x2\r\nMjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3r5+qPeoo\r\ntt7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority\r\n# Subject: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority\r\n# Label: "Certum EC-384 CA"\r\n# Serial: 160250656287871593594747141429395092468\r\n# MD5 Fingerprint: b6:65:b3:96:60:97:12:a1:ec:4e:e1:3d:a3:c6:c9:f1\r\n# SHA1 Fingerprint: f3:3e:78:3c:ac:df:f4:a2:cc:ac:67:55:69:56:d7:e5:16:3c:e1:ed\r\n# SHA256 Fingerprint: 6b:32:80:85:62:53:18:aa:50:d1:73:c9:8d:8b:da:09:d5:7e:27:41:3d:11:4c:f7:87:a0:f5:d0:6c:03:0c:f6\r\n-----BEGIN CERTIFICATE-----\r\nMIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQsw\r\nCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScw\r\nJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMT\r\nEENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2MDcyNDU0WhcNNDMwMzI2MDcyNDU0\r\nWjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBT\r\nLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAX\r\nBgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATE\r\nKI6rGFtqvm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7Tm\r\nFy8as10CW4kjPMIRBSqniBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68Kj\r\nQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI0GZnQkdjrzife81r1HfS+8\r\nEF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjADVS2m5hjEfO/J\r\nUG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0QoSZ/6vn\r\nnvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority\r\n# Subject: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority\r\n# Label: "Certum Trusted Root CA"\r\n# Serial: 40870380103424195783807378461123655149\r\n# MD5 Fingerprint: 51:e1:c2:e7:fe:4c:84:af:59:0e:2f:f4:54:6f:ea:29\r\n# SHA1 Fingerprint: c8:83:44:c0:18:ae:9f:cc:f1:87:b7:8f:22:d1:c5:d7:45:84:ba:e5\r\n# SHA256 Fingerprint: fe:76:96:57:38:55:77:3e:37:a9:5e:7a:d4:d9:cc:96:c3:01:57:c1:5d:31:76:5b:a9:b1:57:04:e1:ae:78:fd\r\n-----BEGIN CERTIFICATE-----\r\nMIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6\r\nMQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu\r\nMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNV\r\nBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwHhcNMTgwMzE2MTIxMDEzWhcNNDMw\r\nMzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEg\r\nU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRo\r\nb3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqG\r\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZ\r\nn0EGze2jusDbCSzBfN8pfktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/q\r\np1x4EaTByIVcJdPTsuclzxFUl6s1wB52HO8AU5853BSlLCIls3Jy/I2z5T4IHhQq\r\nNwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2fJmItdUDmj0VDT06qKhF\r\n8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGtg/BKEiJ3\r\nHAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGa\r\nmqi4NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi\r\n7VdNIuJGmj8PkTQkfVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSF\r\nytKAQd8FqKPVhJBPC/PgP5sZ0jeJP/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0P\r\nqafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSYnjYJdmZm/Bo/6khUHL4wvYBQ\r\nv3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHKHRzQ+8S1h9E6\r\nTsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1\r\nvALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQAD\r\nggIBAEii1QALLtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4\r\nWxmB82M+w85bj/UvXgF2Ez8sALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvo\r\nzMrnadyHncI013nR03e4qllY/p0m+jiGPp2Kh2RX5Rc64vmNueMzeMGQ2Ljdt4NR\r\n5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8CYyqOhNf6DR5UMEQ\r\nGfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA4kZf\r\n5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq\r\n0Uc9NneoWWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7D\r\nP78v3DSk+yshzWePS/Tj6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTM\r\nqJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmTOPQD8rv7gmsHINFSH5pkAnuYZttcTVoP\r\n0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZckbxJF0WddCajJFdr60qZf\r\nE2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique\r\n# Subject: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique\r\n# Label: "TunTrust Root CA"\r\n# Serial: 108534058042236574382096126452369648152337120275\r\n# MD5 Fingerprint: 85:13:b9:90:5b:36:5c:b6:5e:b8:5a:f8:e0:31:57:b4\r\n# SHA1 Fingerprint: cf:e9:70:84:0f:e0:73:0f:9d:f6:0c:7f:2c:4b:ee:20:46:34:9c:bb\r\n# SHA256 Fingerprint: 2e:44:10:2a:b5:8c:b8:54:19:45:1c:8e:19:d9:ac:f3:66:2c:af:bc:61:4b:6a:53:96:0a:30:f7:d0:e2:eb:41\r\n-----BEGIN CERTIFICATE-----\r\nMIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL\r\nBQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg\r\nQ2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv\r\nb3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQwNDI2MDg1NzU2WjBhMQswCQYDVQQG\r\nEwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBDZXJ0aWZpY2F0aW9u\r\nIEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIwDQYJ\r\nKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZ\r\nn56eY+hz2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd\r\n2JQDoOw05TDENX37Jk0bbjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgF\r\nVwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZ\r\nGoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAdgjH8KcwAWJeRTIAAHDOF\r\nli/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViWVSHbhlnU\r\nr8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2\r\neY8fTpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIb\r\nMlEsPvLfe/ZdeikZjuXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISg\r\njwBUFfyRbVinljvrS5YnzWuioYasDXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB\r\n7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwSVXAkPcvCFDVDXSdOvsC9qnyW\r\n5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI04Y+oXNZtPdE\r\nITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0\r\n90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+z\r\nxiD2BkewhpMl0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYu\r\nQEkHDVneixCwSQXi/5E/S7fdAo74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4\r\nFstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRYYdZ2vyJ/0Adqp2RT8JeNnYA/u8EH\r\n22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJpadbGNjHh/PqAulxP\r\nxOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65xxBzn\r\ndFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5\r\nXc0yGYuPjCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7b\r\nnV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ\r\nCvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH\r\nu/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj\r\nd9qDRIueVSjAi1jTkD5OGwDxFa2DK5o=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA\r\n# Subject: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA\r\n# Label: "HARICA TLS RSA Root CA 2021"\r\n# Serial: 76817823531813593706434026085292783742\r\n# MD5 Fingerprint: 65:47:9b:58:86:dd:2c:f0:fc:a2:84:1f:1e:96:c4:91\r\n# SHA1 Fingerprint: 02:2d:05:82:fa:88:ce:14:0c:06:79:de:7f:14:10:e9:45:d7:a5:6d\r\n# SHA256 Fingerprint: d9:5d:0e:8e:da:79:52:5b:f9:be:b1:1b:14:d2:10:0d:32:94:98:5f:0c:62:d9:fa:bd:9c:d9:99:ec:cb:7b:1d\r\n-----BEGIN CERTIFICATE-----\r\nMIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs\r\nMQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl\r\nc2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg\r\nUm9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUzOFoXDTQ1MDIxMzEwNTUzN1owbDEL\r\nMAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl\r\nYXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNBIFJv\r\nb3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569l\r\nmwVnlskNJLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE\r\n4VGC/6zStGndLuwRo0Xua2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uv\r\na9of08WRiFukiZLRgeaMOVig1mlDqa2YUlhu2wr7a89o+uOkXjpFc5gH6l8Cct4M\r\npbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K5FrZx40d/JiZ+yykgmvw\r\nKh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEvdmn8kN3b\r\nLW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcY\r\nAuUR0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqB\r\nAGMUuTNe3QvboEUHGjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYq\r\nE613TBoYm5EPWNgGVMWX+Ko/IIqmhaZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHr\r\nW2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQCPxrvrNQKlr9qEgYRtaQQJKQ\r\nCoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8GA1UdEwEB/wQF\r\nMAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE\r\nAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAU\r\nX15QvWiWkKQUEapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3\r\nf5Z2EMVGpdAgS1D0NTsY9FVqQRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxaja\r\nH6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxDQpSbIPDRzbLrLFPCU3hKTwSUQZqP\r\nJzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcRj88YxeMn/ibvBZ3P\r\nzzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5vZSt\r\njBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0\r\n/L5H9MG0qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pT\r\nBGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79\r\naPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW\r\nxw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU\r\n63ZTGI0RmLo=\r\n-----END CERTIFICATE-----\r\n\r\n# Issuer: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA\r\n# Subject: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA\r\n# Label: "HARICA TLS ECC Root CA 2021"\r\n# Serial: 137515985548005187474074462014555733966\r\n# MD5 Fingerprint: ae:f7:4c:e5:66:35:d1:b7:9b:8c:22:93:74:d3:4b:b0\r\n# SHA1 Fingerprint: bc:b0:c1:9d:e9:98:92:70:19:38:57:e9:8d:a7:b4:5d:6e:ee:01:48\r\n# SHA256 Fingerprint: 3f:99:cc:47:4a:cf:ce:4d:fe:d5:87:94:66:5e:47:8d:15:47:73:9f:2e:78:0f:1b:b4:ca:9b:13:30:97:d4:01\r\n-----BEGIN CERTIFICATE-----\r\nMIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw\r\nCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh\r\ncmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v\r\ndCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoXDTQ1MDIxMzExMDEwOVowbDELMAkG\r\nA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj\r\naCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJvb3Qg\r\nQ0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7\r\nKKrxcm1lAEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9Y\r\nSTHMmE5gEYd103KUkE+bECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUw\r\nAwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD\r\nAgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw\r\nSaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN\r\nnxS2PFOiTAZpffpskcYqSUXm7LcT4Tps\r\n-----END CERTIFICATE-----' \ No newline at end of file diff --git a/snet/sdk/service_client.py b/snet/sdk/service_client.py index bc6034a..a34ef66 100644 --- a/snet/sdk/service_client.py +++ b/snet/sdk/service_client.py @@ -9,8 +9,8 @@ import web3 from eth_account.messages import defunct_hash_message from rfc3986 import urlparse -from snet.cli.resources.root_certificate import certificate -from snet.cli.utils.utils import RESOURCES_PATH, add_to_path, find_file_by_keyword +from snet.sdk.resources.root_certificate import certificate +from snet.sdk.utils.utils import RESOURCES_PATH, add_to_path, find_file_by_keyword import snet.sdk.generic_client_interceptor as generic_client_interceptor from snet.sdk.mpe.payment_channel_provider import PaymentChannelProvider diff --git a/snet/sdk/training/training.py b/snet/sdk/training/training.py index 67a5e74..a158c8a 100644 --- a/snet/sdk/training/training.py +++ b/snet/sdk/training/training.py @@ -5,8 +5,8 @@ import grpc import web3 -from snet.cli.resources.root_certificate import certificate -from snet.cli.utils.utils import add_to_path, RESOURCES_PATH +from snet.sdk.resources.root_certificate import certificate +from snet.sdk.utils.utils import add_to_path, RESOURCES_PATH # for local debug diff --git a/snet/sdk/utils/__init__.py b/snet/sdk/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/snet/sdk/utils/config.py b/snet/sdk/utils/config.py new file mode 100644 index 0000000..4543ebf --- /dev/null +++ b/snet/sdk/utils/config.py @@ -0,0 +1,62 @@ +from snet.contracts import get_contract_def + + +def get_contract_address(cmd, contract_name, error_message=None): + """ + We try to get config address from the different sources. + The order of priorioty is following: + - command line argument (at) + - command line argument (_at) + - current session configuration (current__at) + - networks/*json + """ + + # try to get from command line argument at or contractname_at + a = "at" + if hasattr(cmd.args, a) and getattr(cmd.args, a): + return cmd.w3.to_checksum_address(getattr(cmd.args, a)) + + # try to get from command line argument contractname_at + a = "%s_at" % contract_name.lower() + if hasattr(cmd.args, a) and getattr(cmd.args, a): + return cmd.w3.to_checksum_address(getattr(cmd.args, a)) + + # try to get from current session configuration + rez = cmd.config.get_session_field("current_%s_at" % (contract_name.lower()), exception_if_not_found=False) + if rez: + return cmd.w3.to_checksum_address(rez) + + error_message = error_message or "Fail to read %s address from \"networks\", you should " \ + "specify address by yourself via --%s_at parameter" % ( + contract_name, contract_name.lower()) + # try to take address from networks + return read_default_contract_address(w3=cmd.w3, contract_name=contract_name) + + +def read_default_contract_address(w3, contract_name): + chain_id = w3.net.version # this will raise exception if endpoint is invalid + contract_def = get_contract_def(contract_name) + networks = contract_def["networks"] + contract_address = networks.get(chain_id, {}).get("address", None) + if not contract_address: + raise Exception() + contract_address = w3.to_checksum_address(contract_address) + return contract_address + + +def get_field_from_args_or_session(config, args, field_name): + """ + We try to get field_name from diffent sources: + The order of priorioty is following: + read_default_contract_address - command line argument (--) + - current session configuration (default_) + """ + rez = getattr(args, field_name, None) + # type(rez) can be int in case of wallet-index, so we cannot make simply if(rez) + if rez is not None: + return rez + rez = config.get_session_field("default_%s" % field_name, exception_if_not_found=False) + if rez: + return rez + raise Exception("Fail to get default_%s from config, should specify %s via --%s parameter" % ( + field_name, field_name, field_name.replace("_", "-"))) diff --git a/snet/sdk/utils/ipfs_utils.py b/snet/sdk/utils/ipfs_utils.py new file mode 100644 index 0000000..8f1e336 --- /dev/null +++ b/snet/sdk/utils/ipfs_utils.py @@ -0,0 +1,122 @@ +""" Utilities related to ipfs """ +import tarfile +import glob +import io +import os + +import base58 +import multihash + + +def publish_file_in_ipfs(ipfs_client, filepath, wrap_with_directory=True): + """ + push a file to ipfs given its path + """ + try: + with open(filepath, 'r+b') as file: + result = ipfs_client.add( + file, pin=True, wrap_with_directory=wrap_with_directory) + if wrap_with_directory: + return result[1]['Hash']+'/'+result[0]['Name'] + return result['Hash'] + except Exception as err: + print("File error ", err) + + +def publish_proto_in_ipfs(ipfs_client, protodir): + """ + make tar from protodir/*proto, and publish this tar in ipfs + return base58 encoded ipfs hash + """ + + if not os.path.isdir(protodir): + raise Exception("Directory %s doesn't exists" % protodir) + + files = glob.glob(os.path.join(protodir, "*.proto")) + + if len(files) == 0: + raise Exception("Cannot find any %s files" % + (os.path.join(protodir, "*.proto"))) + + # We are sorting files before we add them to the .tar since an archive containing the same files in a different + # order will produce a different content hash; + files.sort() + + tarbytes = io.BytesIO() + tar = tarfile.open(fileobj=tarbytes, mode="w") + for f in files: + tar.add(f, os.path.basename(f)) + tar.close() + return ipfs_client.add_bytes(tarbytes.getvalue()) + + +def get_from_ipfs_and_checkhash(ipfs_client, ipfs_hash_base58, validate=True): + """ + Get file from ipfs + We must check the hash becasue we cannot believe that ipfs_client wasn't been compromise + """ + if validate: + from snet.cli.resources.proto.unixfs_pb2 import Data + from snet.cli.resources.proto.merckledag_pb2 import MerkleNode + + # No nice Python library to parse ipfs blocks, so do it ourselves. + block_data = ipfs_client.block.get(ipfs_hash_base58) + mn = MerkleNode() + mn.ParseFromString(block_data) + unixfs_data = Data() + unixfs_data.ParseFromString(mn.Data) + assert unixfs_data.Type == unixfs_data.DataType.Value( + 'File'), "IPFS hash must be a file" + data = unixfs_data.Data + + # multihash has a badly registered base58 codec, overwrite it... + multihash.CodecReg.register( + 'base58', base58.b58encode, base58.b58decode) + # create a multihash object from our ipfs hash + mh = multihash.decode(ipfs_hash_base58.encode('ascii'), 'base58') + + # Convenience method lets us directly use a multihash to verify data + if not mh.verify(block_data): + raise Exception("IPFS hash mismatch with data") + else: + data = ipfs_client.cat(ipfs_hash_base58) + return data + + +def hash_to_bytesuri(s): + """ + Convert in and from bytes uri format used in Registry contract + """ + # TODO: we should pad string with zeros till closest 32 bytes word because of a bug in processReceipt (in snet_cli.contract.process_receipt) + s = "ipfs://" + s + return s.encode("ascii").ljust(32 * (len(s)//32 + 1), b"\0") + + +def bytesuri_to_hash(s): + s = s.rstrip(b"\0").decode('ascii') + if not s.startswith("ipfs://"): + raise Exception("We support only ipfs uri in Registry") + return s[7:] + + +def safe_extract_proto_from_ipfs(ipfs_client, ipfs_hash, protodir): + """ + Tar files might be dangerous (see https://bugs.python.org/issue21109, + and https://docs.python.org/3/library/tarfile.html, TarFile.extractall warning) + we extract only simple files + """ + spec_tar = get_from_ipfs_and_checkhash(ipfs_client, ipfs_hash) + with tarfile.open(fileobj=io.BytesIO(spec_tar)) as f: + for m in f.getmembers(): + if os.path.dirname(m.name) != "": + raise Exception( + "tarball has directories. We do not support it.") + if not m.isfile(): + raise Exception( + "tarball contains %s which is not a files" % m.name) + fullname = os.path.join(protodir, m.name) + if os.path.exists(fullname): + os.remove(fullname) + print("%s removed." % fullname) + # now it is safe to call extractall + f.extractall(protodir) diff --git a/snet/sdk/utils/utils.py b/snet/sdk/utils/utils.py new file mode 100644 index 0000000..2c04daf --- /dev/null +++ b/snet/sdk/utils/utils.py @@ -0,0 +1,310 @@ +import json +import os +import subprocess +import functools +import re +import sys +import importlib.resources +from importlib.metadata import distribution +from urllib.parse import urlparse +from pathlib import Path, PurePath + +import web3 +import grpc +from grpc_tools.protoc import main as protoc + +from snet import sdk +from snet.sdk.resources.root_certificate import certificate + +RESOURCES_PATH = PurePath(os.path.dirname(sdk.__file__)).joinpath("resources") + + +class DefaultAttributeObject(object): + def __init__(self, **kwargs): + for k, v in kwargs.items(): + if v is not None: + setattr(self, k, v) + + def getstring(self, item): + return getattr(self, item) + + def getint(self, item): + if getattr(self, item) is None: + return None + return int(getattr(self, item)) + + def getfloat(self, item): + if getattr(self, item) is None: + return None + return float(getattr(self, item)) + + def getboolean(self, item): + if getattr(self, item) is None: + return None + i = self.getstring(item) + if i in ["yes", "on", "true", "True", "1"]: + return True + return False + + def __getattr__(self, item): + return self.__dict__.get(item, None) + + def __repr__(self): + return self.__dict__.__repr__() + + def __str__(self): + return self.__dict__.__str__() + + +def get_web3(rpc_endpoint): + if rpc_endpoint.startswith("ws:"): + provider = web3.WebsocketProvider(rpc_endpoint) + else: + provider = web3.HTTPProvider(rpc_endpoint) + + return web3.Web3(provider) + + +def serializable(o): + if isinstance(o, bytes): + return o.hex() + else: + return o.__dict__ + + +def safe_address_converter(a): + if not web3.Web3.is_checksum_address(a): + raise Exception("%s is not is not a valid Ethereum checksum address" % a) + return a + + +def type_converter(t): + if t.endswith("[]"): + return lambda x: list(map(type_converter(t.replace("[]", "")), json.loads(x))) + else: + if "int" in t: + return lambda x: web3.Web3.to_int(text=x) + elif "bytes32" in t: + return lambda x: web3.Web3.to_bytes(text=x).ljust(32, b"\0") if not x.startswith( + "0x") else web3.Web3.to_bytes(hexstr=x).ljust(32, b"\0") + elif "byte" in t: + return lambda x: web3.Web3.to_bytes(text=x) if not x.startswith("0x") else web3.Web3.to_bytes(hexstr=x) + elif "address" in t: + return safe_address_converter + else: + return str + + +def bytes32_to_str(b): + return b.rstrip(b"\0").decode("utf-8") + + +def _add_next_paths(path, entry_path, seen_paths, next_paths): + with open(path) as f: + for line in f: + if line.strip().startswith("import"): + import_statement = "".join(line.split('"')[1::2]) + if not import_statement.startswith("google/protobuf"): + import_statement_path = Path(path.parent.joinpath(import_statement)).resolve() + if entry_path.parent in path.parents: + if import_statement_path not in seen_paths: + seen_paths.add(import_statement_path) + next_paths.append(import_statement_path) + else: + raise ValueError("Path must not be a parent of entry path") + + +def walk_imports(entry_path): + seen_paths = set() + next_paths = [] + for file_path in os.listdir(entry_path): + if file_path.endswith(".proto"): + file_path = entry_path.joinpath(file_path) + seen_paths.add(file_path) + next_paths.append(file_path) + while next_paths: + path = next_paths.pop() + if os.path.isfile(path): + _add_next_paths(path, entry_path, seen_paths, next_paths) + else: + raise IOError("Import path must be a valid file: {}".format(path)) + return seen_paths + + +def read_temp_tar(f): + f.flush() + f.seek(0) + return f + + +def get_cli_version(): + return distribution("snet.cli").version + + +def compile_proto(entry_path, codegen_dir, proto_file=None, target_language="python"): + try: + if not os.path.exists(codegen_dir): + os.makedirs(codegen_dir) + proto_include = importlib.resources.files('grpc_tools') / '_proto' + + compiler_args = [ + "-I{}".format(entry_path), + "-I{}".format(proto_include) + ] + + if target_language == "python": + compiler_args.insert(0, "protoc") + compiler_args.append("--python_out={}".format(codegen_dir)) + compiler_args.append("--grpc_python_out={}".format(codegen_dir)) + compiler = protoc + elif target_language == "nodejs": + protoc_node_compiler_path = Path( + RESOURCES_PATH.joinpath("node_modules").joinpath("grpc-tools").joinpath("bin").joinpath( + "protoc.js")).absolute() + grpc_node_plugin_path = Path( + RESOURCES_PATH.joinpath("node_modules").joinpath("grpc-tools").joinpath("bin").joinpath( + "grpc_node_plugin")).resolve() + if not os.path.isfile(protoc_node_compiler_path) or not os.path.isfile(grpc_node_plugin_path): + print("Missing required node.js protoc compiler. Retrieving from npm...") + subprocess.run(["npm", "install"], cwd=RESOURCES_PATH) + compiler_args.append("--js_out=import_style=commonjs,binary:{}".format(codegen_dir)) + compiler_args.append("--grpc_out={}".format(codegen_dir)) + compiler_args.append("--plugin=protoc-gen-grpc={}".format(grpc_node_plugin_path)) + compiler = lambda args: subprocess.run([str(protoc_node_compiler_path)] + args) + + if proto_file: + compiler_args.append(str(proto_file)) + else: + compiler_args.extend([str(p) for p in entry_path.glob("**/*.proto")]) + + if not compiler(compiler_args): + return True + else: + return False + + except Exception as e: + print(e) + return False + + +def abi_get_element_by_name(abi, name): + """ Return element of abi (return None if fails to find) """ + if abi and "abi" in abi: + for a in abi["abi"]: + if "name" in a and a["name"] == name: + return a + return None + + +def abi_decode_struct_to_dict(abi, struct_list): + return {el_abi["name"]: el for el_abi, el in zip(abi["outputs"], struct_list)} + + +def int4bytes_big(b): + return int.from_bytes(b, byteorder='big') + + +def is_valid_endpoint(url): + """ + Just ensures the url has a scheme (http/https), and a net location (IP or domain name). + Can make more advanced or do on-network tests if needed, but this is really just to catch obvious errors. + >>> is_valid_endpoint("https://34.216.72.29:6206") + True + >>> is_valid_endpoint("blahblah") + False + >>> is_valid_endpoint("blah://34.216.72.29") + False + >>> is_valid_endpoint("http://34.216.72.29:%%%") + False + >>> is_valid_endpoint("http://192.168.0.2:9999") + True + """ + try: + result = urlparse(url) + if result.port: + _port = int(result.port) + return ( + all([result.scheme, result.netloc]) and + result.scheme in ['http', 'https'] + ) + except ValueError: + return False + + +def remove_http_https_prefix(endpoint): + """remove http:// or https:// prefix if presented in endpoint""" + endpoint = endpoint.replace("https://", "") + endpoint = endpoint.replace("http://", "") + return endpoint + + +def open_grpc_channel(endpoint): + """ + open grpc channel: + - for http:// we open insecure_channel + - for https:// we open secure_channel (with default credentials) + - without prefix we open insecure_channel + """ + _GB = 1024 ** 3 + options = [('grpc.max_send_message_length', _GB), + ('grpc.max_receive_message_length', _GB)] + if endpoint.startswith("https://"): + return grpc.secure_channel(remove_http_https_prefix(endpoint), grpc.ssl_channel_credentials(root_certificates=certificate)) + return grpc.insecure_channel(remove_http_https_prefix(endpoint)) + + +def rgetattr(obj, attr): + """ + >>> from types import SimpleNamespace + >>> args = SimpleNamespace(a=1, b=SimpleNamespace(c=2, d='e')) + >>> rgetattr(args, "a") + 1 + >>> rgetattr(args, "b.c") + 2 + """ + return functools.reduce(getattr, [obj] + attr.split('.')) + + +def normalize_private_key(private_key): + if private_key.startswith("0x"): + private_key = bytes(bytearray.fromhex(private_key[2:])) + else: + private_key = bytes(bytearray.fromhex(private_key)) + return private_key + + +def get_address_from_private(private_key): + return web3.Account.from_key(private_key).address + + +class add_to_path(): + def __init__(self, path): + self.path = path + + def __enter__(self): + sys.path.insert(0, self.path) + + def __exit__(self, exc_type, exc_value, traceback): + try: + sys.path.remove(self.path) + except ValueError: + pass + + +def find_file_by_keyword(directory, keyword): + for root, dirs, files in os.walk(directory): + for file in files: + if keyword in file: + return file + + +def is_valid_url(url): + regex = re.compile( + r'^(?:http|ftp)s?://' + r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' + r'localhost|' + r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' + r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' + r'(?::\d+)?' + r'(?:/?|[/?]\S+)$', re.IGNORECASE) + return re.match(regex, url) is not None From b6be1039ef4b74263c58f791f65aa6b653f4963a Mon Sep 17 00:00:00 2001 From: Dronny Date: Thu, 25 Jul 2024 15:40:23 +0300 Subject: [PATCH 02/55] Tiny fixes --- snet/sdk/utils/ipfs_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/snet/sdk/utils/ipfs_utils.py b/snet/sdk/utils/ipfs_utils.py index 8f1e336..0cfb9a2 100644 --- a/snet/sdk/utils/ipfs_utils.py +++ b/snet/sdk/utils/ipfs_utils.py @@ -56,8 +56,8 @@ def get_from_ipfs_and_checkhash(ipfs_client, ipfs_hash_base58, validate=True): We must check the hash becasue we cannot believe that ipfs_client wasn't been compromise """ if validate: - from snet.cli.resources.proto.unixfs_pb2 import Data - from snet.cli.resources.proto.merckledag_pb2 import MerkleNode + from snet.sdk.resources.proto.unixfs_pb2 import Data + from snet.sdk.resources.proto.merckledag_pb2 import MerkleNode # No nice Python library to parse ipfs blocks, so do it ourselves. block_data = ipfs_client.block.get(ipfs_hash_base58) From bf22dc5a22e414cfa6776508547343e5ac606247 Mon Sep 17 00:00:00 2001 From: Dronny Date: Tue, 13 Aug 2024 10:36:00 +0400 Subject: [PATCH 03/55] Modified README.md --- README.md | 90 +++++++++++++++++++++++++++++++------- snet/sdk/service_client.py | 4 +- 2 files changed, 76 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 7cd7e1e..7a5ff6d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,20 @@ + + + # snet-sdk-python SingularityNET SDK for Python @@ -45,7 +62,8 @@ snet_sdk = sdk.SnetSDK(config) The `config` parameter is a Python dictionary. See [test_sdk_client.py](https://github.com/singnet/snet-sdk-python/blob/master/testcases/functional_tests/test_sdk_client.py) for a reference. -#### Config options description + +##### Config options description private_key: Your wallet's private key that will be used to pay for calls. Is **required** to make a call; eth_rpc_endpoint: RPC endpoint that is used to access the Ethereum network. Is **required** to make a call; @@ -55,26 +73,36 @@ identity_type: Type of your wallet authentication. Note that snet-sdk currently network: You can set the Ethereum network that will be used to make a call; force_update: If set to False, will reuse the existing gRPC stubs (if any) instead of downloading proto and regenerating them every time. + + + + ##### List organizations and their services You can use the sdk client instance`s methods get_organization_list() to list all organizations and get_services_list("org_id") to list all services of a given organization. ```python print(snet_sdk.get_organization_list()) -print(snet_sdk.get_services_list("26072b8b6a0e448180f8c0e702ab6d2f")) +print(snet_sdk.get_services_list(org_id="26072b8b6a0e448180f8c0e702ab6d2f")) ``` +##### Get service metadata -##### Free call configuration - -If you want to use the free calls you will need to pass these arguments to the create_service_client() method: +The metadata of services is stored in IPFS. To view it, you need to call the `get_service_metadata()` method, passing +the organization id and the service id to it. -``` -"free_call_auth_token-bin":"f2548d27ffd319b9c05918eeac15ebab934e5cfcd68e1ec3db2b92765", -"free-call-token-expiry-block":172800, +```python +service_metadata = snet_sdk.get_service_metadata(org_id="26072b8b6a0e448180f8c0e702ab6d2f", service_id="Exampleservice") +print(*service_metadata.m.items(), sep="\n") +print(*service_metadata.get_tags()) +print(service_metadata.get_payment_address(group_name="default_group")) ``` -You can receive these for a given service from the [Dapp](https://beta.singularitynet.io/) -#### Calling the service -Now, the instance of the sdk can be used to create the service client instances. + +### Calling the service +Now, the instance of the sdk can be used to create the service client instances, using `create_service_client()` method. Continuing from the previous code here is an example using `Exampleservice` from the `26072b8b6a0e448180f8c0e702ab6d2f` organization: ```python @@ -82,6 +110,17 @@ service_client = snet_sdk.create_service_client(org_id="26072b8b6a0e448180f8c0e7 service_id="Exampleservice", group_name="default_group") ``` +##### Free call configuration + +If you want to use the free calls you will need to pass these arguments to the `create_service_client()` method: + +```python +free_call_auth_token-bin = "f2548d27ffd319b9c05918eeac15ebab934e5cfcd68e1ec3db2b92765", +free-call-token-expiry-block = 172800 +``` + +You can receive these for a given service from the [Dapp](https://beta.singularitynet.io/) + Creating a service client with free calls included would look like this: ```python service_client = snet_sdk.create_service_client(org_id="26072b8b6a0e448180f8c0e702ab6d2f", @@ -93,17 +132,36 @@ service_client = snet_sdk.create_service_client(org_id="26072b8b6a0e448180f8c0e7 After executing this code, you should have client libraries created for this service. They are located at the following path: `~/.snet/org_id/service_id/python/` -Note: Currently you can only save files to `~/.snet/`. We will fix this in the future. +Note: Currently you can only save files to `~/.snet/`. We will fix this in the future. + ```python -service_client.deposit_and_open_channel(123456, 33333) +service_client.open_channel(amount=123456, expiration=33333) ``` -`deposit_and_open_channel(amount, expiration)` function deposits the specified amount of AGIX tokens in cogs into an MPE smart contract and opens a payment channel. Expiration is payment channel's TTL in blocks. -The instance of service_client that has been generated can be utilized to invoke the methods that the service offers. You can list these using the get_services_and_messages_info_as_pretty_string() method: +`open_channel(amount, expiration)` opens a payment channel with the specified amount of AGIX tokens in cogs +and expiration time. Expiration is payment channel's TTL in blocks. + +```python +service_client.deposit_and_open_channel(amount=123456, expiration=33333) +``` +`deposit_and_open_channel(amount, expiration)` function does the same as the previous one, but first deposits +the specified amount of AGIX tokens in cogs into an MPE smart contract. + +The instance of service_client that has been generated can be utilized to invoke the methods that the service offers. You can list these using the `get_services_and_messages_info_as_pretty_string()` method: + ```python print(service_client.get_services_and_messages_info_as_pretty_string()) ``` -To invoke the service`s methods, you can use the the call_rpc() method. This method requires the names of the method and data object, along with the data itself, to be passed into it. +But if you need to process lists of services and messages, it is better to use the +`get_services_and_messages_info()` method: + +```python +services, messages = service_client.get_services_and_messages_info() +print(*services.items(), sep="\n") +print(*messages.items(), sep="\n") +``` + +To invoke the service's methods, you can use the `call_rpc()` method. This method requires the names of the method and data object, along with the data itself, to be passed into it. To continue with our example, here’s a call to the *mul* method of the *Exampleservice* from the *26072b8b6a0e448180f8c0e702ab6d2f* organization: ```python diff --git a/snet/sdk/service_client.py b/snet/sdk/service_client.py index bc6034a..9c653f5 100644 --- a/snet/sdk/service_client.py +++ b/snet/sdk/service_client.py @@ -123,8 +123,8 @@ def load_open_channels(self): group_id = base64.b64decode(str(self.group["group_id"])) new_payment_channels = self.payment_channel_provider.get_past_open_channels(self.account, payment_address, group_id, self.last_read_block) - self.payment_channels = self.payment_channels + self._filter_existing_channels_from_new_payment_channels( - new_payment_channels) + self.payment_channels = self.payment_channels + \ + self._filter_existing_channels_from_new_payment_channels(new_payment_channels) self.last_read_block = current_block_number return self.payment_channels From da5c483f4b5c6db837203ee4ecb83e6353b81e64 Mon Sep 17 00:00:00 2001 From: Dronny Date: Tue, 13 Aug 2024 12:10:33 +0400 Subject: [PATCH 04/55] Modified README.md x2 --- README.md | 50 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 7a5ff6d..01ecdba 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,10 @@ TODO: Functions that need to be added to the readme: 2.1. load_open_channels (?) 2.2. get_current_block_number 2.3. update_channel_states (?) -2.4. open_channel +2.4. open_channel (+) 2.5. get_price -2.6. get_services_and_messages_info -2.7. get_concurrency_flag +2.6. get_services_and_messages_info (+) +2.7. get_concurrency_flag (?) --> # snet-sdk-python @@ -65,13 +65,13 @@ See [test_sdk_client.py](https://github.com/singnet/snet-sdk-python/blob/master/ ##### Config options description -private_key: Your wallet's private key that will be used to pay for calls. Is **required** to make a call; -eth_rpc_endpoint: RPC endpoint that is used to access the Ethereum network. Is **required** to make a call; -email: Your email; -identity_name: Name that will be used locally to save your wallet settings. You can check your identities in the `~/.snet/config` file; -identity_type: Type of your wallet authentication. Note that snet-sdk currently supports only "key" identity_type; -network: You can set the Ethereum network that will be used to make a call; -force_update: If set to False, will reuse the existing gRPC stubs (if any) instead of downloading proto and regenerating them every time. +- private_key: Your wallet's private key that will be used to pay for calls. Is **required** to make a call; +- eth_rpc_endpoint: RPC endpoint that is used to access the Ethereum network. Is **required** to make a call; +- email: Your email; +- identity_name: Name that will be used locally to save your wallet settings. You can check your identities in the `~/.snet/config` file; +- identity_type: Type of your wallet authentication. Note that snet-sdk currently supports only "key" identity_type; +- network: You can set the Ethereum network that will be used to make a call; +- force_update: If set to False, will reuse the existing gRPC stubs (if any) instead of downloading proto and regenerating them every time. - - ##### List organizations and their services You can use the sdk client instance`s methods get_organization_list() to list all organizations and get_services_list("org_id") to list all services of a given organization. @@ -102,6 +100,7 @@ print(service_metadata.get_payment_address(group_name="default_group")) ``` ### Calling the service + Now, the instance of the sdk can be used to create the service client instances, using `create_service_client()` method. Continuing from the previous code here is an example using `Exampleservice` from the `26072b8b6a0e448180f8c0e702ab6d2f` organization: @@ -134,6 +133,8 @@ After executing this code, you should have client libraries created for this ser Note: Currently you can only save files to `~/.snet/`. We will fix this in the future. +##### Open channel and list services + ```python service_client.open_channel(amount=123456, expiration=33333) ``` @@ -161,6 +162,8 @@ print(*services.items(), sep="\n") print(*messages.items(), sep="\n") ``` +##### Call + To invoke the service's methods, you can use the `call_rpc()` method. This method requires the names of the method and data object, along with the data itself, to be passed into it. To continue with our example, here’s a call to the *mul* method of the *Exampleservice* from the *26072b8b6a0e448180f8c0e702ab6d2f* organization: @@ -173,6 +176,29 @@ For more information about gRPC and how to use it with Python, please see: - [gRPC Basics - Python](https://grpc.io/docs/tutorials/basic/python.html) - [gRPC Python’s documentation](https://grpc.io/grpc/python/) +##### Other useful features + + + +Service client also provides several useful functions. If you need to find out the number of +the current block in the blockchain, there is a `get_current_block_number()` method for this: + +```python +block_number = service_client.get_current_block_number() +print(f"Current block is {block_number}") +``` + +To find out the price of calling a service function, you need to use the "get_price()" method: + +```python +price = service_client.get_price() +print(f"The price in cogs for calling the service {service_client.service_id} is {price}") +``` + --- ## Development From e0b50069a99abb6f29d53fd83fdb1d496aec082c Mon Sep 17 00:00:00 2001 From: Dronny Date: Tue, 13 Aug 2024 18:21:46 +0400 Subject: [PATCH 05/55] Tiny fixes in README.md. Started work on full documentation. --- README.md | 36 ++----- docs/main/account.md | 0 docs/main/concurrency_manager.md | 0 docs/main/generic_client_interceptor.md | 0 docs/main/init.md | 100 ++++++++++++++++++ docs/main/service_client.md | 0 .../ipfs_metadata_provider.md | 0 docs/metadata_provider/metadata_provider.md | 0 docs/mpe/mpe_contract.md | 0 docs/mpe/payment_channel.md | 0 docs/mpe/payment_channel_provider.md | 0 .../default_payment_strategy.md | 0 .../freecall_payment_strategy.md | 0 .../paidcall_payment_strategy.md | 0 docs/payment_strategies/payment_strategy.md | 0 .../prepaid_payment_strategy.md | 0 docs/snet-sdk-python-documentation.md | 40 +++++++ docs/training/training.md | 0 18 files changed, 148 insertions(+), 28 deletions(-) create mode 100644 docs/main/account.md create mode 100644 docs/main/concurrency_manager.md create mode 100644 docs/main/generic_client_interceptor.md create mode 100644 docs/main/init.md create mode 100644 docs/main/service_client.md create mode 100644 docs/metadata_provider/ipfs_metadata_provider.md create mode 100644 docs/metadata_provider/metadata_provider.md create mode 100644 docs/mpe/mpe_contract.md create mode 100644 docs/mpe/payment_channel.md create mode 100644 docs/mpe/payment_channel_provider.md create mode 100644 docs/payment_strategies/default_payment_strategy.md create mode 100644 docs/payment_strategies/freecall_payment_strategy.md create mode 100644 docs/payment_strategies/paidcall_payment_strategy.md create mode 100644 docs/payment_strategies/payment_strategy.md create mode 100644 docs/payment_strategies/prepaid_payment_strategy.md create mode 100644 docs/snet-sdk-python-documentation.md create mode 100644 docs/training/training.md diff --git a/README.md b/README.md index 01ecdba..3397700 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,14 @@ # snet-sdk-python - + SingularityNET SDK for Python ## Package @@ -73,12 +65,6 @@ See [test_sdk_client.py](https://github.com/singnet/snet-sdk-python/blob/master/ - network: You can set the Ethereum network that will be used to make a call; - force_update: If set to False, will reuse the existing gRPC stubs (if any) instead of downloading proto and regenerating them every time. - - ##### List organizations and their services You can use the sdk client instance`s methods get_organization_list() to list all organizations and get_services_list("org_id") to list all services of a given organization. @@ -178,12 +164,6 @@ For more information about gRPC and how to use it with Python, please see: ##### Other useful features - - Service client also provides several useful functions. If you need to find out the number of the current block in the blockchain, there is a `get_current_block_number()` method for this: @@ -192,7 +172,7 @@ block_number = service_client.get_current_block_number() print(f"Current block is {block_number}") ``` -To find out the price of calling a service function, you need to use the "get_price()" method: +To find out the price of calling a service function, you need to use the `get_price()` method: ```python price = service_client.get_price() @@ -230,4 +210,4 @@ $ pip install -e . ## License This project is licensed under the MIT License - see the -[LICENSE](https://github.com/singnet/snet-sdk-python/blob/master/LICENSE) file for details. \ No newline at end of file +[LICENSE](https://github.com/singnet/snet-sdk-python/blob/master/LICENSE) file for details. diff --git a/docs/main/account.md b/docs/main/account.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/main/concurrency_manager.md b/docs/main/concurrency_manager.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/main/generic_client_interceptor.md b/docs/main/generic_client_interceptor.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/main/init.md b/docs/main/init.md new file mode 100644 index 0000000..df95793 --- /dev/null +++ b/docs/main/init.md @@ -0,0 +1,100 @@ +## module: sdk.\_\_init\_\_.py + +Entities: +1. [Arguments](#class-arguments) + - [\_\_init\_\_](#init) +2. [SnetSDK](#class-snetsdk) + - [\_\_init\_\_](#init-1) + +### class `Arguments` + +extends: - + +is extended by: - + +#### description + +Represents the arguments for the `BlockchainCommand` from `snet.cli`. + +#### attributes + +- `org_id` (str): The organization id. +- `service_id` (str): The service id. +- `language` (str): The language used. Defaults to "python". +- `protodir` (Path): The path to the directory with protobuf files. Defaults to "~/USER_NAME/.snet". + +#### methods + +##### `__init__` + +Initializes a new instance of the class. + +###### args: + +- `org_id` (str): The organization id. Defaults to _None_. +- `service_id` (str): The service id. Defaults to _None_. + +###### returns: + +- _None_ + +### class `SnetSDK` + +extends: - + +is extended by: - + +#### description + +The SnetSDK class is the main entry point for interacting with the SingularityNET platform. +It provides methods for creating service clients, managing identities, and configuring the SDK. + +#### attributes + +- `_sdk_config` (dict): The SDK configuration. +- `_metadata_provider` (MetadataProvider): An instance of the `MetadataProvider` class. Note: There is currently only +one implementation of `MetadataProvider` which is `IPFSMetadataProvider`, so this attribute can only be initialized to +`IPFSMetadataProvider` at this time. +- `web3` (Web3): An instance of the Web3 class for interacting with the Ethereum blockchain. +- `mpe_contract` (MPEContract): An instance of the `MPEContract` class for interacting with the MultiPartyEscrow contract. +- `ipfs_client` (ipfshttpclient.Client): An instance of the `ipfshttpclient.Client` class for interacting with the +InterPlanetary File System. +- `registry_contract` (Contract): An instance of the `Contract` class for interacting with the Registry contract. +- `account` (Account): An instance of the `Account` class for managing the SDK's Ethereum account. + +#### methods + +##### `__init__` + +Initializes a new instance of the `SnetSDK` class. Initializes `web3` with the specified Ethereum RPC endpoint. +Instantiates the MPE contract with the specified contract address if provided, otherwise uses the default MPE contract. +Instantiates the IPFS client with the specified IPFS endpoint if provided, otherwise uses the default IPFS endpoint. +Instantiates the Registry contract with the specified contract address if provided, otherwise uses the default Registry +contract. Instantiates the Account object with the specified Web3 client, SDK configuration, and MPE contract. +Creates an instance of the "Config" class, passing `_sdk_config` to it, and calls the `setup_config` method with this +instance as an argument. + +###### args: + +- `sdk_config` (dict): A dictionary containing the SDK configuration. +- `metadata_provider` (MetadataProvider): A `MetadataProvider` object. Defaults to _None_. + +###### returns: + +- _None_ + +##### `setup_config` + + + +###### args: + +- + +###### returns: + +- _None_ + +###### raises: + +- Exception diff --git a/docs/main/service_client.md b/docs/main/service_client.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/metadata_provider/ipfs_metadata_provider.md b/docs/metadata_provider/ipfs_metadata_provider.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/metadata_provider/metadata_provider.md b/docs/metadata_provider/metadata_provider.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/mpe/mpe_contract.md b/docs/mpe/mpe_contract.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/mpe/payment_channel.md b/docs/mpe/payment_channel.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/mpe/payment_channel_provider.md b/docs/mpe/payment_channel_provider.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/payment_strategies/default_payment_strategy.md b/docs/payment_strategies/default_payment_strategy.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/payment_strategies/freecall_payment_strategy.md b/docs/payment_strategies/freecall_payment_strategy.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/payment_strategies/paidcall_payment_strategy.md b/docs/payment_strategies/paidcall_payment_strategy.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/payment_strategies/payment_strategy.md b/docs/payment_strategies/payment_strategy.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/payment_strategies/prepaid_payment_strategy.md b/docs/payment_strategies/prepaid_payment_strategy.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/snet-sdk-python-documentation.md b/docs/snet-sdk-python-documentation.md new file mode 100644 index 0000000..4cf78a4 --- /dev/null +++ b/docs/snet-sdk-python-documentation.md @@ -0,0 +1,40 @@ +# Welcome to snet-sdk-python's documentation! + +SingularityNET SDK for Python + +### Core concepts + +The SingularityNET SDK allows you to make calls to SingularityNET services programmatically from your application. +To communicate between clients and services, SingularityNET uses [gRPC](https://grpc.io/). +To handle payment of services, SingularityNET uses [Ethereum state channels](https://dev.singularitynet.io/docs/concepts/multi-party-escrow/). +The SingularityNET SDK abstracts and manages state channels with service providers on behalf of the user and handles authentication with the SingularityNET services. + +A getting started guide for the SNET SDK for Python is available [here](https://github.com/singnet/snet-sdk-python/blob/master/README.md). + +### Package Diagram + +![Package Diagram]() + +### Modules + +1. metadata_provider + 1. [metadata_provider](metadata_provider/metadata_provider.md) + 2. [ipfs_metadata_provider](metadata_provider/ipfs_metadata_provider.md) +2. mpe + 1. [mpe_contract](mpe/mpe_contract.md) + 2. [payment_channel](mpe/payment_channel.md) + 3. [payment_channel_provider](mpe/payment_channel_provider.md) +3. payment_strategies + 1. [payment_strategy](payment_strategies/payment_strategy.md) + 2. [default_payment_strategy]() + 3. [freecall_payment_strategy]() + 4. [paidcall_payment_strategy]() + 5. [prepaid_payment_strategy]() +4. training + 1. [training](training/training.md) +5. [init](main/init.md) +6. [account](main/account.md) +7. [service_client](main/service_client.md) +8. [concurrency_manager](main/concurrency_manager.md) +9. [generic_client_interceptor](main/generic_client_interceptor.md) + diff --git a/docs/training/training.md b/docs/training/training.md new file mode 100644 index 0000000..e69de29 From 489be2dde8e7661e460779a9cbe3c32a95cc28a9 Mon Sep 17 00:00:00 2001 From: Dronny Date: Wed, 14 Aug 2024 13:03:46 +0400 Subject: [PATCH 06/55] Finished init.md doc --- docs/main/init.md | 212 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 197 insertions(+), 15 deletions(-) diff --git a/docs/main/init.md b/docs/main/init.md index df95793..053bd4b 100644 --- a/docs/main/init.md +++ b/docs/main/init.md @@ -1,31 +1,43 @@ -## module: sdk.\_\_init\_\_.py +# module: sdk.\_\_init\_\_.py Entities: 1. [Arguments](#class-arguments) - [\_\_init\_\_](#init) 2. [SnetSDK](#class-snetsdk) - [\_\_init\_\_](#init-1) - -### class `Arguments` + - [setup_config](#setupconfig) + - [set_session_identity](#setsessionidentity) + - [create_service_client](#createserviceclient) + - [get_service_stub](#getservicestub) + - [get_path_to_pb_files](#getpathtopbfiles) + - [get_module_by_keyword](#getmodulebykeyword) + - [get_service_metadata](#getservicemetadata) + - [_get_first_group](#getfirstgroup) + - [_get_group_by_group_name](#getgroupbygroupname) + - [_get_service_group_details](#getservicegroupdetails) + - [get_organization_list](#getorganizationlist) + - [get_services_list](#getserviceslist) + +## class `Arguments` extends: - is extended by: - -#### description +### description Represents the arguments for the `BlockchainCommand` from `snet.cli`. -#### attributes +### attributes - `org_id` (str): The organization id. - `service_id` (str): The service id. - `language` (str): The language used. Defaults to "python". - `protodir` (Path): The path to the directory with protobuf files. Defaults to "~/USER_NAME/.snet". -#### methods +### methods -##### `__init__` +### `__init__` Initializes a new instance of the class. @@ -38,18 +50,18 @@ Initializes a new instance of the class. - _None_ -### class `SnetSDK` +## class `SnetSDK` extends: - is extended by: - -#### description +### description The SnetSDK class is the main entry point for interacting with the SingularityNET platform. It provides methods for creating service clients, managing identities, and configuring the SDK. -#### attributes +### attributes - `_sdk_config` (dict): The SDK configuration. - `_metadata_provider` (MetadataProvider): An instance of the `MetadataProvider` class. Note: There is currently only @@ -62,9 +74,9 @@ InterPlanetary File System. - `registry_contract` (Contract): An instance of the `Contract` class for interacting with the Registry contract. - `account` (Account): An instance of the `Account` class for managing the SDK's Ethereum account. -#### methods +### methods -##### `__init__` +### `__init__` Initializes a new instance of the `SnetSDK` class. Initializes `web3` with the specified Ethereum RPC endpoint. Instantiates the MPE contract with the specified contract address if provided, otherwise uses the default MPE contract. @@ -83,18 +95,188 @@ instance as an argument. - _None_ -##### `setup_config` +### `setup_config` + +Sets up the configuration for the SnetSDK instance. This function checks the network and identity_name in +the configuration and sets up the configuration accordingly. If the network is specified, and it is different from +the current session network, it sets the session network. If the identity_name is specified, it sets the session identity. +If there are no identities in the configuration, it checks if the identity is selected and raises an exception if not. + +###### args: +- config (Config): The `snet.cli.config.Congig` instance. +###### returns: + +- _None_ + +###### raises: + +- Exception: If the identity name is not passed or selected. + +### `set_session_identity` + +Sets the session identity in the given config. ###### args: -- +- identity_name (str): The name of the identity to set. +- config (Config): The `snet.cli.config.Congig` object to modify. +- out_f (TextIO): The output to write messages to. ###### returns: - _None_ +### `create_service_client` + +If `force_update` is True or if there are no gRPC stubs for the given service, the proto files are loaded +and compiled using the `generate_client_library()` method of the `SDKCommand` class instance. +It then initializes `payment_channel_management_strategy` to `DefaultPaymentStrategy` if it is not specified. +It also sets the `options` dictionary with some default values. If `self._metadata_provider` is not specified +it is initialized by `IPFSMetadataProvider`. It also gets the service stub using the `self.get_service_stub` +method and the pb2 module using the `self.get_module_by_keyword` method. Finally, it creates a new instance +of the `ServiceClient` class with all the required parameters, which is then returned. + +###### args: + +- org_id (str): The ID of the organization. +- service_id (str): The ID of the service. +- group_name (str): The name of the payment group. Defaults to _None_. +- payment_channel_management_strategy (PaymentStrategy): The payment channel management strategy. Defaults to _None_. +- free_call_auth_token_bin (str): The free call authentication token in binary format. Defaults to _None_. +- free_call_token_expiry_block (int): The block number when the free call token expires. Defaults to _None_. +- options (dict): Additional options for the service client. Defaults to _None_. +- concurrent_calls (int): The number of concurrent calls allowed. Defaults to 1. + +###### returns: + +- The created service client instance. (ServiceClient) + +### `get_service_stub` + +Retrieves the gRPC service stub for the given organization and service ID. + +###### args: + +- org_id (str): The ID of the organization. +- service_id (str): The ID of the service. + +###### returns: + +- The gRPC service stub for the given organization and service ID. (ServiceStub) + +###### raises: + +- Exception: If an error occurs while importing a module. + +### `get_path_to_pb_files` + +Returns the path to the directory containing the protobuf files for a given organization and service. + +###### args: + +- org_id (str): The ID of the organization. +- service_id (str): The ID of the service. + +###### returns: + +- The path to the directory containing the protobuf files. (str) + +### `get_module_by_keyword` + +Retrieves the module name from the given organization ID, service ID, and keyword. + +###### args: + +- org_id (str): The ID of the organization. +- service_id (str): The ID of the service. +- keyword (str): The keyword used to search for the module. + +###### returns: + +- The module name extracted from the file name. (ModuleName) + +### `get_service_metadata` + +Retrieves metadata for a given service in a given organization using Registry first and then IPFS. + +###### args: + +- org_id (str): The ID of the organization. +- service_id (str): The ID of the service. + +###### returns: + +- The metadata for the service. (MPEServiceMetadata) + +###### raises: + +- Exception: If the service is not found in the specified organization. + +### `_get_first_group` + +Returns the first payment group from the given service metadata. + +###### args: + +- service_metadata (MPEServiceMetadata): An instance of `MPEServiceMetadata` class. + +###### returns: + +- The first group from the service metadata. (dict) + +### `_get_group_by_group_name` + +Returns a payment group from the given service metadata based on the group name. + +###### args: + +- service_metadata (MPEServiceMetadata): An instance of `MPEServiceMetadata` class. +- group_name (str): The name of the group to search for. + +###### returns: + +- The group with the matching group name, or an empty dictionary if no match is found. (dict) + +### `_get_service_group_details` + +Returns a payment group from the given service metadata based on the group name or the first payment group if +group name is not specified. + +###### args: + +- service_metadata (MPEServiceMetadata): An instance of `MPEServiceMetadata` class. +- group_name (str): The name of the group to search for. + +###### returns: + +- The group with the matching group name, or the first group if name is not specified. (dict) + +###### raises: + +- Exception: If no groups are found for the given service. + +### `get_organization_list` + +Retrieves a list of organization IDs from the Registry contract. + +###### returns: + +- A list of strings representing the organization IDs. (list) + +### `get_services_list` + +Retrieves a list of service IDs for a given organization from the Registry contract. + +###### args: + +- org_id (str): The ID of the organization. + +###### returns: + +- A list of strings representing the service IDs. (list) + ###### raises: -- Exception +- Exception: If the organization with the given ID does not exist. From 5410722dd1151098bb44827bd0f6d320f291e4ad Mon Sep 17 00:00:00 2001 From: Dronny Date: Wed, 14 Aug 2024 13:11:47 +0400 Subject: [PATCH 07/55] Minor format fixes --- docs/main/init.md | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/main/init.md b/docs/main/init.md index 053bd4b..db119d5 100644 --- a/docs/main/init.md +++ b/docs/main/init.md @@ -1,4 +1,4 @@ -# module: sdk.\_\_init\_\_.py +## module: sdk.\_\_init\_\_.py Entities: 1. [Arguments](#class-arguments) @@ -18,26 +18,26 @@ Entities: - [get_organization_list](#getorganizationlist) - [get_services_list](#getserviceslist) -## class `Arguments` +### class `Arguments` extends: - is extended by: - -### description +#### description Represents the arguments for the `BlockchainCommand` from `snet.cli`. -### attributes +#### attributes - `org_id` (str): The organization id. - `service_id` (str): The service id. - `language` (str): The language used. Defaults to "python". - `protodir` (Path): The path to the directory with protobuf files. Defaults to "~/USER_NAME/.snet". -### methods +#### methods -### `__init__` +#### `__init__` Initializes a new instance of the class. @@ -50,18 +50,18 @@ Initializes a new instance of the class. - _None_ -## class `SnetSDK` +### class `SnetSDK` extends: - is extended by: - -### description +#### description The SnetSDK class is the main entry point for interacting with the SingularityNET platform. It provides methods for creating service clients, managing identities, and configuring the SDK. -### attributes +#### attributes - `_sdk_config` (dict): The SDK configuration. - `_metadata_provider` (MetadataProvider): An instance of the `MetadataProvider` class. Note: There is currently only @@ -74,9 +74,9 @@ InterPlanetary File System. - `registry_contract` (Contract): An instance of the `Contract` class for interacting with the Registry contract. - `account` (Account): An instance of the `Account` class for managing the SDK's Ethereum account. -### methods +#### methods -### `__init__` +#### `__init__` Initializes a new instance of the `SnetSDK` class. Initializes `web3` with the specified Ethereum RPC endpoint. Instantiates the MPE contract with the specified contract address if provided, otherwise uses the default MPE contract. @@ -95,7 +95,7 @@ instance as an argument. - _None_ -### `setup_config` +#### `setup_config` Sets up the configuration for the SnetSDK instance. This function checks the network and identity_name in the configuration and sets up the configuration accordingly. If the network is specified, and it is different from @@ -114,7 +114,7 @@ If there are no identities in the configuration, it checks if the identity is se - Exception: If the identity name is not passed or selected. -### `set_session_identity` +#### `set_session_identity` Sets the session identity in the given config. @@ -128,7 +128,7 @@ Sets the session identity in the given config. - _None_ -### `create_service_client` +#### `create_service_client` If `force_update` is True or if there are no gRPC stubs for the given service, the proto files are loaded and compiled using the `generate_client_library()` method of the `SDKCommand` class instance. @@ -153,7 +153,7 @@ of the `ServiceClient` class with all the required parameters, which is then ret - The created service client instance. (ServiceClient) -### `get_service_stub` +#### `get_service_stub` Retrieves the gRPC service stub for the given organization and service ID. @@ -170,7 +170,7 @@ Retrieves the gRPC service stub for the given organization and service ID. - Exception: If an error occurs while importing a module. -### `get_path_to_pb_files` +#### `get_path_to_pb_files` Returns the path to the directory containing the protobuf files for a given organization and service. @@ -183,7 +183,7 @@ Returns the path to the directory containing the protobuf files for a given orga - The path to the directory containing the protobuf files. (str) -### `get_module_by_keyword` +#### `get_module_by_keyword` Retrieves the module name from the given organization ID, service ID, and keyword. @@ -197,7 +197,7 @@ Retrieves the module name from the given organization ID, service ID, and keywor - The module name extracted from the file name. (ModuleName) -### `get_service_metadata` +#### `get_service_metadata` Retrieves metadata for a given service in a given organization using Registry first and then IPFS. @@ -214,7 +214,7 @@ Retrieves metadata for a given service in a given organization using Registry fi - Exception: If the service is not found in the specified organization. -### `_get_first_group` +#### `_get_first_group` Returns the first payment group from the given service metadata. @@ -226,7 +226,7 @@ Returns the first payment group from the given service metadata. - The first group from the service metadata. (dict) -### `_get_group_by_group_name` +#### `_get_group_by_group_name` Returns a payment group from the given service metadata based on the group name. @@ -239,7 +239,7 @@ Returns a payment group from the given service metadata based on the group name. - The group with the matching group name, or an empty dictionary if no match is found. (dict) -### `_get_service_group_details` +#### `_get_service_group_details` Returns a payment group from the given service metadata based on the group name or the first payment group if group name is not specified. @@ -257,7 +257,7 @@ group name is not specified. - Exception: If no groups are found for the given service. -### `get_organization_list` +#### `get_organization_list` Retrieves a list of organization IDs from the Registry contract. @@ -265,7 +265,7 @@ Retrieves a list of organization IDs from the Registry contract. - A list of strings representing the organization IDs. (list) -### `get_services_list` +#### `get_services_list` Retrieves a list of service IDs for a given organization from the Registry contract. From c495bef6d0e6952f05b99102aa65f37934ac430c Mon Sep 17 00:00:00 2001 From: Dronny Date: Wed, 14 Aug 2024 13:18:49 +0400 Subject: [PATCH 08/55] Minor format fixes (x2) --- docs/main/init.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/main/init.md b/docs/main/init.md index db119d5..c760715 100644 --- a/docs/main/init.md +++ b/docs/main/init.md @@ -5,18 +5,18 @@ Entities: - [\_\_init\_\_](#init) 2. [SnetSDK](#class-snetsdk) - [\_\_init\_\_](#init-1) - - [setup_config](#setupconfig) - - [set_session_identity](#setsessionidentity) - - [create_service_client](#createserviceclient) - - [get_service_stub](#getservicestub) - - [get_path_to_pb_files](#getpathtopbfiles) - - [get_module_by_keyword](#getmodulebykeyword) - - [get_service_metadata](#getservicemetadata) - - [_get_first_group](#getfirstgroup) - - [_get_group_by_group_name](#getgroupbygroupname) - - [_get_service_group_details](#getservicegroupdetails) - - [get_organization_list](#getorganizationlist) - - [get_services_list](#getserviceslist) + - [setup_config](#setup_config) + - [set_session_identity](#set_session_identity) + - [create_service_client](#create_service_client) + - [get_service_stub](#get_service_stub) + - [get_path_to_pb_files](#get_path_to_pb_files) + - [get_module_by_keyword](#get_module_by_keyword) + - [get_service_metadata](#get_service_metadata) + - [_get_first_group](#_get_first_group) + - [_get_group_by_group_name](#_get_group_by_group_name) + - [_get_service_group_details](#_get_service_group_details) + - [get_organization_list](#get_organization_list) + - [get_services_list](#get_services_list) ### class `Arguments` From 8d37597ddb87abfcb93ecc461d029eefe6a0aa68 Mon Sep 17 00:00:00 2001 From: Dronny Date: Wed, 14 Aug 2024 18:36:07 +0400 Subject: [PATCH 09/55] service_client.md is made up to and including "get_price", minor format fixes in init.md. --- docs/main/init.md | 57 +++---- docs/main/service_client.md | 305 ++++++++++++++++++++++++++++++++++++ 2 files changed, 334 insertions(+), 28 deletions(-) diff --git a/docs/main/init.md b/docs/main/init.md index c760715..b277bdc 100644 --- a/docs/main/init.md +++ b/docs/main/init.md @@ -64,7 +64,7 @@ It provides methods for creating service clients, managing identities, and confi #### attributes - `_sdk_config` (dict): The SDK configuration. -- `_metadata_provider` (MetadataProvider): An instance of the `MetadataProvider` class. Note: There is currently only +- `_metadata_provider` (MetadataProvider): An instance of the `MetadataProvider` class. _Note_: There is currently only one implementation of `MetadataProvider` which is `IPFSMetadataProvider`, so this attribute can only be initialized to `IPFSMetadataProvider` at this time. - `web3` (Web3): An instance of the Web3 class for interacting with the Ethereum blockchain. @@ -72,7 +72,8 @@ one implementation of `MetadataProvider` which is `IPFSMetadataProvider`, so thi - `ipfs_client` (ipfshttpclient.Client): An instance of the `ipfshttpclient.Client` class for interacting with the InterPlanetary File System. - `registry_contract` (Contract): An instance of the `Contract` class for interacting with the Registry contract. -- `account` (Account): An instance of the `Account` class for managing the SDK's Ethereum account. +- `account` (Account): An instance of the `Account` class for interacting with the MultiPartyEscrow and +SingularityNetToken contracts. #### methods @@ -120,9 +121,9 @@ Sets the session identity in the given config. ###### args: -- identity_name (str): The name of the identity to set. -- config (Config): The `snet.cli.config.Congig` object to modify. -- out_f (TextIO): The output to write messages to. +- `identity_name` (str): The name of the identity to set. +- `config` (Config): The `snet.cli.config.Congig` object to modify. +- `out_f` (TextIO): The output to write messages to. ###### returns: @@ -140,14 +141,14 @@ of the `ServiceClient` class with all the required parameters, which is then ret ###### args: -- org_id (str): The ID of the organization. -- service_id (str): The ID of the service. -- group_name (str): The name of the payment group. Defaults to _None_. -- payment_channel_management_strategy (PaymentStrategy): The payment channel management strategy. Defaults to _None_. -- free_call_auth_token_bin (str): The free call authentication token in binary format. Defaults to _None_. -- free_call_token_expiry_block (int): The block number when the free call token expires. Defaults to _None_. -- options (dict): Additional options for the service client. Defaults to _None_. -- concurrent_calls (int): The number of concurrent calls allowed. Defaults to 1. +- `org_id` (str): The ID of the organization. +- `service_id` (str): The ID of the service. +- `group_name` (str): The name of the payment group. Defaults to _None_. +- `payment_channel_management_strategy` (PaymentStrategy): The payment channel management strategy. Defaults to _None_. +- `free_call_auth_token_bin` (str): The free call authentication token in binary format. Defaults to _None_. +- `free_call_token_expiry_block` (int): The block number when the free call token expires. Defaults to _None_. +- `options` (dict): Additional options for the service client. Defaults to _None_. +- `concurrent_calls` (int): The number of concurrent calls allowed. Defaults to 1. ###### returns: @@ -159,8 +160,8 @@ Retrieves the gRPC service stub for the given organization and service ID. ###### args: -- org_id (str): The ID of the organization. -- service_id (str): The ID of the service. +- `org_id` (str): The ID of the organization. +- `service_id` (str): The ID of the service. ###### returns: @@ -176,8 +177,8 @@ Returns the path to the directory containing the protobuf files for a given orga ###### args: -- org_id (str): The ID of the organization. -- service_id (str): The ID of the service. +- `org_id` (str): The ID of the organization. +- `service_id` (str): The ID of the service. ###### returns: @@ -189,9 +190,9 @@ Retrieves the module name from the given organization ID, service ID, and keywor ###### args: -- org_id (str): The ID of the organization. -- service_id (str): The ID of the service. -- keyword (str): The keyword used to search for the module. +- `org_id` (str): The ID of the organization. +- `service_id` (str): The ID of the service. +- `keyword` (str): The keyword used to search for the module. ###### returns: @@ -203,8 +204,8 @@ Retrieves metadata for a given service in a given organization using Registry fi ###### args: -- org_id (str): The ID of the organization. -- service_id (str): The ID of the service. +- `org_id` (str): The ID of the organization. +- `service_id` (str): The ID of the service. ###### returns: @@ -220,7 +221,7 @@ Returns the first payment group from the given service metadata. ###### args: -- service_metadata (MPEServiceMetadata): An instance of `MPEServiceMetadata` class. +- `service_metadata` (MPEServiceMetadata): An instance of `MPEServiceMetadata` class. ###### returns: @@ -232,8 +233,8 @@ Returns a payment group from the given service metadata based on the group name. ###### args: -- service_metadata (MPEServiceMetadata): An instance of `MPEServiceMetadata` class. -- group_name (str): The name of the group to search for. +- `service_metadata` (MPEServiceMetadata): An instance of `MPEServiceMetadata` class. +- `group_name` (str): The name of the group to search for. ###### returns: @@ -246,8 +247,8 @@ group name is not specified. ###### args: -- service_metadata (MPEServiceMetadata): An instance of `MPEServiceMetadata` class. -- group_name (str): The name of the group to search for. +- `service_metadata` (MPEServiceMetadata): An instance of `MPEServiceMetadata` class. +- `group_name` (str): The name of the group to search for. ###### returns: @@ -271,7 +272,7 @@ Retrieves a list of service IDs for a given organization from the Registry contr ###### args: -- org_id (str): The ID of the organization. +- `org_id` (str): The ID of the organization. ###### returns: diff --git a/docs/main/service_client.md b/docs/main/service_client.md index e69de29..f7e2d64 100644 --- a/docs/main/service_client.md +++ b/docs/main/service_client.md @@ -0,0 +1,305 @@ +## module: sdk.service_client.py + +Entities: +1. [ServiceClient](#class-serviceclient) + - [\_\_init\_\_](#init) + - [call_rpc](#call_rpc) + - [_generate_grpc_stub](#_generate_grpc_stub) + - [get_grpc_base_channel](#get_grpc_base_channel) + - [_get_grpc_channel](#_get_grpc_channel) + - [_get_service_call_metadata](#_get_service_call_metadata) + - [_intercept_call](#_intercept_call) + - [_filter_existing_channels_from_new_payment_channels](#_filter_existing_channels_from_new_payment_channels) + - [load_open_channels](#load_open_channels) + - [get_current_block_number](#get_current_block_number) + - [update_channel_states](#update_channel_states) + - [default_channel_expiration](#default_channel_expiration) + - [_generate_payment_channel_state_service_client](#_generate_payment_channel_state_service_client) + - [open_channel](#open_channel) + - [deposit_and_open_channel](#deposit_and_open_channel) + - [get_price](#get_price) + - [generate_signature](#generate_signature) + - [generate_training_signature](#generate_training_signature) + - [get_free_call_config](#get_free_call_config) + - [get_service_details]() + - [get_concurrency_flag]() + - [get_concurrency_token_and_channel]() + - [set_concurrency_token_and_channel]() + - [get_path_to_pb_files]() + - [get_services_and_messages_info]() + - [get_services_and_messages_info_as_pretty_string]() + +### class `ServiceClient` + +extends: - + +is extended by: - + +#### description + +This class is responsible for creating a client for interacting with a service. +It initializes various attributes and sets up a gRPC channel for communication with the service. +The class is used to manage the communication and payment channel management for a service in the SDK. + +#### attributes + +- `org_id` (str): The organization id. +- `service_id` (str): The service id. +- `options` (dict): Additional options for the service client. +- `group` (dict): The payment group details. +- `service_metadata` (MPEServiceMetadata): An instance of the `MPEServiceMetadata` class with the metadata of +the specified service. +- `payment_strategy` (PaymentStrategy): The payment strategy. _Note_: In fact, this is an instance of one of +the "PaymentStrategy" inheritor classes. +- `expiry_threshold` (int): The payment expiration threshold (in blocks). +- `__base_grpc_channel` (grpc.Channel): The base gRPC channel. +- `grpc_channel` (grpc.Channel): The gRPC channel with interceptor. +- `payment_channel_provider` (PaymentChannelProvider): An instance of the `PaymentChannelProvider` class for +working with channels and interacting with MPE. +- `service` (Any): The gRPC service stub instance. +- `pb2_module` (ModuleType): The imported protobuf module. +- `payment_channels` (list[PaymentChannel]): The list of payment channels. +- `last_read_block` (int): The last read block number. +- `account` (Account): An instance of the `Account` class for interacting with the MultiPartyEscrow and +SingularityNetToken contracts. +- `sdk_web3` (Web3): The Web3 instance. +- `mpe_address` (str): The MPE contract address. + +#### methods + +#### `__init__` + +Initializes a new instance of the class. + +###### args: + +- `org_id` (str): The ID of the organization. +- `service_id` (str): The ID of the service. +- `service_metadata` (MPEServiceMetadata): The metadata for the service. +- `group` (dict): The payment group from the service metadata. +- `service_stub` (ServiceStub): The gRPC service stub. +- `payment_strategy` (PaymentStrategy): The payment channel management strategy. +- `options` (dict): Additional options for the service client. +- `mpe_contract` (MPEContract): The MPE contract instance. +- `account` (Account): An instance of the `Account` class. +- `sdk_web3` (Web3): The Web3 instance. +- `pb2_module` (Union[str, ModuleType]): The module containing the gRPC message definitions. + +###### returns: + +- _None_ + +#### `call_rpc` + +Calls an RPC method on the service client and returns its result. + +###### args: + +- `rpc_name` (str): The name of the RPC method to call. +- `message_class` (str): The name of the message class to use for the request. +- `**kwargs`: Keyword arguments to pass to the message class constructor, in fact, these are the values +that are passed to the called method as arguments. + +###### returns: + +- The response from the RPC method call. (Any) + +#### `_generate_grpc_stub` + +Generates a gRPC stub instance for the given service stub. + +###### args: + +- `service_stub` (ServiceStub): The gRPC service stub. + +###### returns: + +- stub_instance (object): The generated gRPC stub instance. + +#### `get_grpc_base_channel` + +Returns the base gRPC channel used by the service client. + +###### returns: + +- `self.__base_grpc_channel` (grpc.Channel) + +#### `_get_grpc_channel` + +Returns a gRPC channel based on the provided endpoint. + +Retrieves the endpoint from the options dictionary or from the service metadata. If no endpoint is provided, +it uses the first endpoint from the group specified in the service metadata. The endpoint is parsed using +the `urlparse` function to extract the hostname and port. If a port is specified, it is concatenated with +the hostname to form the channel endpoint. Otherwise, only the hostname is used as the channel endpoint. +The scheme of the endpoint is used to determine the type of channel to be created. If the scheme is "http", +an insecure channel is created using the channel endpoint. If the scheme is "https", a secure channel is +created using the channel endpoint and the root certificates. If the scheme is neither "http" nor "https", +a ValueError is raised with an error message. + +###### returns: + +- The gRPC channel based on the provided endpoint. (grpc.Channel) + +###### raises: + +- ValueError: If the scheme in the service metadata is neither "http" nor "https". + +#### `_get_service_call_metadata` + +Retrieves the metadata required for making a service call using the payment strategy. + +###### returns: + +- Payment metadata. (list[tuple[str, Any]]) + +#### `_intercept_call` + + + +###### args: + +- + +###### returns: + +- + +#### `_filter_existing_channels_from_new_payment_channels` + +Filters the new channel list so that only those that are not yet among the existing ones remain, +and returns them as a list. + +###### args: + +- `new_payment_channels` (list[PaymentChannel]): A list of `PaymentChannel` objects representing the new +payment channels to filter. + +###### returns: + +- A list of the new payment channels that are not already in the `self.payment_channels` list. (list[PaymentChannel]) + +#### `load_open_channels` + +Load open payment channels and update the payment channels list. + +Retrieves open payment channels from the payment channel provider based on the current account, payment address, +group ID, and last read block. It then filters out any existing channels from the new payment channels and +updates the payment channels list with the new channels. Finally, it updates the last read block with the +current block number and returns the updated payment channels list. + +###### returns: + +- The updated payment channels list. (list[PaymentChannel]) + +#### `get_current_block_number` + +Returns the current block number from the Ethereum blockchain using Web3. + +###### returns: + +- The current block number. (int) + +#### `update_channel_states` + +Updates the state of each channel in the `payment_channels` list. + +###### returns: + +- `self.payment_channels` with updated states. (list[PaymentChannel]) + +#### `default_channel_expiration` + +Returns the default expiration time for the payment channel, calculated as the current block number +plus the expiry threshold. + +###### returns: + +- The default expiration time for a payment channel (block number). (int) + +#### `_generate_payment_channel_state_service_client` + +Generates a payment channel state service client. + +Creates a gRPC channel using the base channel and imports the necessary modules for the +state service. It then uses the imported module to create a PaymentChannelStateServiceStub object, which +is the client for the payment channel state service. + +###### returns: + +- Payment channel state service client stub. (Any) + +#### `open_channel` + +Opens a payment channel with the specified amount of AGIX tokens in cogs and expiration time. + +###### args: + +- `amount` (int): The amount of AGIX tokens in cogs to deposit into the channel. +- `expiration` (int): The expiration time of the payment channel in blocks. + +###### returns: + +- Newly opened payment channel. (PaymentChannel) + +#### `deposit_and_open_channel` + +Deposits the specified amount of tokens into the MPE smart contract and opens a payment channel +with its amount of AGIX tokens in cogs and expiration time. + +###### args: + +- `amount` (int): The amount of AGIX tokens in cogs to deposit into the channel. +- `expiration` (int): The expiration time of the payment channel in blocks. + +###### returns: + +- Newly opened payment channel. (PaymentChannel) + +#### `get_price` + + + +###### args: + +- + +###### returns: + +- + +#### `generate_signature` + + + +###### args: + +- + +###### returns: + +- + +#### `generate_training_signature` + + + +###### args: + +- + +###### returns: + +- + +#### `get_free_call_config` + + + +###### args: + +- + +###### returns: + +- From 94721a5114abacc69523036d7821a9355be49c65 Mon Sep 17 00:00:00 2001 From: Dronny Date: Thu, 15 Aug 2024 13:46:40 +0400 Subject: [PATCH 10/55] Finished service_client.md --- docs/main/service_client.md | 121 +++++++++++++++++++++++++++++------- 1 file changed, 99 insertions(+), 22 deletions(-) diff --git a/docs/main/service_client.md b/docs/main/service_client.md index f7e2d64..94db915 100644 --- a/docs/main/service_client.md +++ b/docs/main/service_client.md @@ -21,13 +21,13 @@ Entities: - [generate_signature](#generate_signature) - [generate_training_signature](#generate_training_signature) - [get_free_call_config](#get_free_call_config) - - [get_service_details]() - - [get_concurrency_flag]() - - [get_concurrency_token_and_channel]() - - [set_concurrency_token_and_channel]() - - [get_path_to_pb_files]() - - [get_services_and_messages_info]() - - [get_services_and_messages_info_as_pretty_string]() + - [get_service_details](#get_service_details) + - [get_concurrency_flag](#get_concurrency_flag) + - [get_concurrency_token_and_channel](#get_concurrency_token_and_channel) + - [set_concurrency_token_and_channel](#set_concurrency_token_and_channel) + - [get_path_to_pb_files](#get_path_to_pb_files) + - [get_services_and_messages_info](#get_services_and_messages_info) + - [get_services_and_messages_info_as_pretty_string](#get_services_and_messages_info_as_pretty_string) ### class `ServiceClient` @@ -83,7 +83,7 @@ Initializes a new instance of the class. - `mpe_contract` (MPEContract): The MPE contract instance. - `account` (Account): An instance of the `Account` class. - `sdk_web3` (Web3): The Web3 instance. -- `pb2_module` (Union[str, ModuleType]): The module containing the gRPC message definitions. +- `pb2_module` (str | ModuleType): The module containing the gRPC message definitions. ###### returns: @@ -155,7 +155,7 @@ Retrieves the metadata required for making a service call using the payment stra #### `_intercept_call` - + ###### args: @@ -258,48 +258,125 @@ with its amount of AGIX tokens in cogs and expiration time. #### `get_price` +Returns the price in cogs of the service group's pricing. + +###### returns: +- The price in cogs of the service group's pricing. (int) + +#### `generate_signature` + +Generates a signature for the given message using the account's signer private key. ###### args: -- +- `message` (str | bytes): The message to sign. ###### returns: -- - -#### `generate_signature` +- The generated signature. (bytes) +#### `generate_training_signature` +Generates a training signature by signing a message using the account's signer private key. ###### args: -- +- `text` (str): The text to be included in the message. +- `address` (str): The address to be included in the message. +- `block_number` (int): The block number to be included in the message. ###### returns: -- +- The generated training signature. (str | bytes) -#### `generate_training_signature` +#### `get_free_call_config` +Retrieves the free call configuration from the `self.options` dict. +###### returns: -###### args: +- A tuple containing the email, free call auth token bin, and free call token expiry block. (tuple[str, str, int]) -- +#### `get_service_details` + +Retrieves the details of the service. ###### returns: -- +- A tuple containing the organization ID, service ID, group ID, and the first endpoint for the group. +(tuple[str, str, str, str]) -#### `get_free_call_config` +#### `get_concurrency_flag` +Returns the value of the `concurrency` option from the `self.options` dict. +If the option is not present, it returns `True` by default. + +###### returns: + +- A value indicating whether concurrency is enabled or not. (bool) + +#### `get_concurrency_token_and_channel` + +Retrieves the concurrency token and channel from the payment strategy. + +###### returns: +- The concurrency token and channel obtained from the payment strategy. (tuple[str, PaymentChannel]) + +#### `set_concurrency_token_and_channel` + +Sets the concurrency token and channel for the payment strategy. ###### args: -- +- `token` (str): The concurrency token. +- `channel` (PaymentChannel): The payment channel. ###### returns: -- +- _None_ + +#### `get_path_to_pb_files` + +Returns the path to the directory containing the protobuf files for a given organization and service. + +###### args: + +- `org_id` (str): The ID of the organization. +- `service_id` (str): The ID of the service. + +###### returns: + +- The path to the directory containing the protobuf files. (str) + +#### `get_services_and_messages_info` + +Retrieves information about services and messages defined in a protobuf file. + +This function reads the content of a protobuf file and extracts information about the services and messages defined in it. +It uses regular expressions to match service definitions, RPC methods, message definitions, and fields within messages. + +###### returns: + +- A tuple containing: + - services (dict): A dictionary containing information about the services defined in the protobuf file. + The keys are the names of the services, and the values are lists of tuples representing the RPC methods. + Each tuple contains the method name, input type, and output type. + - messages (dict): A dictionary containing information about the messages defined in the protobuf file. + The keys are the names of the messages, and the values are lists of tuples representing the fields within the messages. + Each tuple contains the field type and field name. + + (tuple[dict[str, list], dict[str, list]]) + +#### `get_services_and_messages_info_as_pretty_string` + +Retrieves information about the services and messages defined in the protobuf file and returns it as a +formatted string. + +_Note_: it first calls the [get_services_and_messages_info](#get_services_and_messages_info) method and then converts +the result into a formatted string. + +###### returns: + +- A formatted string containing information about the services and messages defined in the protobuf file. (str) From bd5f56ba84b7eef6d09df34cc60c9fabd70c2749 Mon Sep 17 00:00:00 2001 From: Dronny Date: Thu, 15 Aug 2024 17:52:26 +0400 Subject: [PATCH 11/55] Tiny fixes --- docs/main/init.md | 2 +- docs/main/service_client.md | 2 +- docs/snet-sdk-python-documentation.md | 29 ++++++++++++++------------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/docs/main/init.md b/docs/main/init.md index b277bdc..ae80cc5 100644 --- a/docs/main/init.md +++ b/docs/main/init.md @@ -1,4 +1,4 @@ -## module: sdk.\_\_init\_\_.py +## module: sdk.\_\_init\_\_ Entities: 1. [Arguments](#class-arguments) diff --git a/docs/main/service_client.md b/docs/main/service_client.md index 94db915..83cb394 100644 --- a/docs/main/service_client.md +++ b/docs/main/service_client.md @@ -1,4 +1,4 @@ -## module: sdk.service_client.py +## module: sdk.service_client Entities: 1. [ServiceClient](#class-serviceclient) diff --git a/docs/snet-sdk-python-documentation.md b/docs/snet-sdk-python-documentation.md index 4cf78a4..197c135 100644 --- a/docs/snet-sdk-python-documentation.md +++ b/docs/snet-sdk-python-documentation.md @@ -9,7 +9,7 @@ To communicate between clients and services, SingularityNET uses [gRPC](https:// To handle payment of services, SingularityNET uses [Ethereum state channels](https://dev.singularitynet.io/docs/concepts/multi-party-escrow/). The SingularityNET SDK abstracts and manages state channels with service providers on behalf of the user and handles authentication with the SingularityNET services. -A getting started guide for the SNET SDK for Python is available [here](https://github.com/singnet/snet-sdk-python/blob/master/README.md). +A getting started guide for the SNET SDK for Python is available [here](https://github.com/Arondondon/snet-sdk-python/blob/master/README.md). ### Package Diagram @@ -17,24 +17,25 @@ A getting started guide for the SNET SDK for Python is available [here](https:// ### Modules -1. metadata_provider +1. [\_\_init\_\_](main/init.md) +2. [account](main/account.md) +3. [service_client](main/service_client.md) +4. [concurrency_manager](main/concurrency_manager.md) +5. [generic_client_interceptor](main/generic_client_interceptor.md) +6. metadata_provider 1. [metadata_provider](metadata_provider/metadata_provider.md) 2. [ipfs_metadata_provider](metadata_provider/ipfs_metadata_provider.md) -2. mpe +7. mpe 1. [mpe_contract](mpe/mpe_contract.md) 2. [payment_channel](mpe/payment_channel.md) 3. [payment_channel_provider](mpe/payment_channel_provider.md) -3. payment_strategies +8. payment_strategies 1. [payment_strategy](payment_strategies/payment_strategy.md) - 2. [default_payment_strategy]() - 3. [freecall_payment_strategy]() - 4. [paidcall_payment_strategy]() - 5. [prepaid_payment_strategy]() -4. training + 2. [default_payment_strategy](payment_strategies/default_payment_strategy.md) + 3. [freecall_payment_strategy](payment_strategies/freecall_payment_strategy.md) + 4. [paidcall_payment_strategy](payment_strategies/paidcall_payment_strategy.md) + 5. [prepaid_payment_strategy](payment_strategies/prepaid_payment_strategy.md) +9. training 1. [training](training/training.md) -5. [init](main/init.md) -6. [account](main/account.md) -7. [service_client](main/service_client.md) -8. [concurrency_manager](main/concurrency_manager.md) -9. [generic_client_interceptor](main/generic_client_interceptor.md) + From 3649be13acbf19d81cbcfb4d8632c7f38180b886 Mon Sep 17 00:00:00 2001 From: Dronny Date: Thu, 15 Aug 2024 17:52:50 +0400 Subject: [PATCH 12/55] Finished account.md --- docs/main/account.md | 206 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) diff --git a/docs/main/account.md b/docs/main/account.md index e69de29..3ba132e 100644 --- a/docs/main/account.md +++ b/docs/main/account.md @@ -0,0 +1,206 @@ +## module: sdk.account + +Entities: +1. [TransactionError](#class-transactionerror) + - [\_\_init\_\_](#init) + - [\_\_str\_\_](#str) +2. [Account](#class-account) + - [\_\_init\_\_](#init-1) + - [_get_nonce](#_get_nonce) + - [_get_gas_price](#_get_gas_price) + - [_send_signed_transaction](#_send_signed_transaction) + - [send_transaction](#send_transaction) + - [_parse_receipt](#_parse_receipt) + - [escrow_balance](#escrow_balance) + - [deposit_to_escrow_account](#deposit_to_escrow_account) + - [approve_transfer](#approve_transfer) + - [allowance](#allowance) + +### class `TransactionError` + +extends: `Exception` + +is extended by: - + +#### description + +`TransactionError` is a custom exception class that is raised when an Ethereum transaction receipt has a status of 0. +This indicates that the transaction failed. Can provide a custom message. Optionally includes receipt + +#### attributes + +- `message` (str): The exception message. +- `receipt` (dict): The transaction receipt. + +#### methods + +#### `__init__` + +Initializes the exception with the provided message and receipt. + +###### args: + +- `message` (str): The exception message. +- `receipt` (dict): The transaction receipt. Defaults to _None_. + +###### returns: + +- _None_ + +#### `__str__` + +Returns a string representation of the `TransactionError` object. + +###### returns: + +- A string containing the `message` attribute of the TransactionError object. (str) + +### class `Account` + +extends: - + +is extended by: - + +#### description + +`Account` is responsible for managing the Ethereum account associated with the SingularityNET platform. +It provides methods for interacting with the MultiPartyEscrow contract, the SingularityNetToken contract, and +the Ethereum blockchain. + +#### attributes + +- `config` (dict): The configuration settings for the account. _Note_: In fact, this is the same config +from `SnetSDK`. +- `web3` (Web3): An instance of the Web3 class for interacting with the Ethereum blockchain. +- `mpe_contract` (MPEContract): An instance of the `MPEContract` class for interacting with +the MultiPartyEscrow contract. +- `token_contract` (Contract): An instance of the `Contract` class from the `web3` library for interacting +with the SingularityNET AGIX Token contract. +- `private_key` (str): The private key associated with the account. +- `signer_private_key` (str): The private key used for signing transactions. +- `address` (str): The Ethereum address associated with the account. +- `signer_address` (str): The Ethereum address used for signing transactions. +- `nonce` (int): The nonce value for the account. + +#### methods + +#### `__init__` + +Initializes a new instance of the `Account` class. + +###### args: + +- `w3` (Web3): An instance of the Web3 class. +- `config` (dict): A dictionary containing the configuration settings. +- `mpe_contract` (MPEContract): An instance of the MPEContract class. + +###### returns: + +- _None_ + +#### `_get_nonce` + +Returns the next nonce for a transaction. + +###### returns: + +- The next nonce for a transaction. (int) + +#### `_get_gas_price` + +Calculates the gas price for a transaction. + +Retrieves the current gas price from the Ethereum network using the web3 and increases it +according to a certain algorithm so that the transaction goes faster. + +###### returns: + +- The calculated gas price. (int) + +#### `_send_signed_transaction` + +Sends a signed transaction to the Ethereum blockchain. + +Builds a transaction using the given contract function and arguments, signs it with the private key of the account, +and sends it to the Ethereum blockchain. + +###### args: + +- `contract_fn`: The contract function to be called. +- `*args`: The arguments to pass to the contract function. + +###### returns: + +- Hash of the sent transaction. (HexStr | str) + +#### `send_transaction` + +Sends a transaction by calling the given contract function with the provided arguments. + +###### args: + +- `contract_fn`: The contract function to be called. +- `*args`: The arguments to pass to the contract function. + +###### returns: + +- The transaction receipt indicating the success or failure of the transaction. (TxReceipt) + +#### `_parse_receipt` + +Parses the receipt of a transaction and returns the result as a JSON string. + +###### args: + +- receipt (TxReceipt): The receipt of the transaction. +- event (Event): The event to process the receipt with. +- encoder (JSONEncoder): The JSON encoder to use. Defaults to json.JSONEncoder. + +###### returns: + +- The result of processing the receipt as a JSON string. (str) + +###### raises: + +- TransactionError: If the transaction status is 0, indicating a failed transaction. + +#### `escrow_balance` + +Retrieves the escrow balance for the current account. + +###### returns: + +- The escrow balance in cogs. (int) + +#### `deposit_to_escrow_account` + +Deposit the specified amount of AGIX tokens in cogs into the MPE account. + +###### args: + +- amount_in_cogs (int): The amount of AGIX tokens in cogs to deposit. + +###### returns: + +- The transaction receipt of the transaction. (TxReceipt) + +#### `approve_transfer` + +Approves a transfer of a specified amount of AGIX tokens in cogs to the MPE contract. + +###### args: + +- amount_in_cogs (int): The amount of AGIX tokens in cogs to approve for transfer. + +###### returns: + +- The transaction receipt of the transaction. (TxReceipt) + +#### `allowance` + +Retrieves the allowance of the current account for the MPE contract. + +###### returns: + +- The allowance in cogs. (int) + From ebac407e9e951e8ea499e3a8a3eb6665e118a322 Mon Sep 17 00:00:00 2001 From: Dronny Date: Thu, 15 Aug 2024 18:46:40 +0400 Subject: [PATCH 13/55] Minor fixes --- docs/main/account.md | 12 +++++++----- docs/main/init.md | 2 ++ docs/main/service_client.md | 2 ++ docs/snet-sdk-python-documentation.md | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/main/account.md b/docs/main/account.md index 3ba132e..3716f79 100644 --- a/docs/main/account.md +++ b/docs/main/account.md @@ -1,5 +1,7 @@ ## module: sdk.account +[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/account.py) to GitHub + Entities: 1. [TransactionError](#class-transactionerror) - [\_\_init\_\_](#init) @@ -152,9 +154,9 @@ Parses the receipt of a transaction and returns the result as a JSON string. ###### args: -- receipt (TxReceipt): The receipt of the transaction. -- event (Event): The event to process the receipt with. -- encoder (JSONEncoder): The JSON encoder to use. Defaults to json.JSONEncoder. +- `receipt` (TxReceipt): The receipt of the transaction. +- `event` (Event): The event to process the receipt with. +- `encoder` (JSONEncoder): The JSON encoder to use. Defaults to json.JSONEncoder. ###### returns: @@ -178,7 +180,7 @@ Deposit the specified amount of AGIX tokens in cogs into the MPE account. ###### args: -- amount_in_cogs (int): The amount of AGIX tokens in cogs to deposit. +- `amount_in_cogs` (int): The amount of AGIX tokens in cogs to deposit. ###### returns: @@ -190,7 +192,7 @@ Approves a transfer of a specified amount of AGIX tokens in cogs to the MPE cont ###### args: -- amount_in_cogs (int): The amount of AGIX tokens in cogs to approve for transfer. +- `amount_in_cogs` (int): The amount of AGIX tokens in cogs to approve for transfer. ###### returns: diff --git a/docs/main/init.md b/docs/main/init.md index ae80cc5..ca306bf 100644 --- a/docs/main/init.md +++ b/docs/main/init.md @@ -1,5 +1,7 @@ ## module: sdk.\_\_init\_\_ +[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/__init__.py) to GitHub + Entities: 1. [Arguments](#class-arguments) - [\_\_init\_\_](#init) diff --git a/docs/main/service_client.md b/docs/main/service_client.md index 83cb394..991ef1d 100644 --- a/docs/main/service_client.md +++ b/docs/main/service_client.md @@ -1,5 +1,7 @@ ## module: sdk.service_client +[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/service_client.py) to GitHub + Entities: 1. [ServiceClient](#class-serviceclient) - [\_\_init\_\_](#init) diff --git a/docs/snet-sdk-python-documentation.md b/docs/snet-sdk-python-documentation.md index 197c135..9a792b2 100644 --- a/docs/snet-sdk-python-documentation.md +++ b/docs/snet-sdk-python-documentation.md @@ -9,7 +9,7 @@ To communicate between clients and services, SingularityNET uses [gRPC](https:// To handle payment of services, SingularityNET uses [Ethereum state channels](https://dev.singularitynet.io/docs/concepts/multi-party-escrow/). The SingularityNET SDK abstracts and manages state channels with service providers on behalf of the user and handles authentication with the SingularityNET services. -A getting started guide for the SNET SDK for Python is available [here](https://github.com/Arondondon/snet-sdk-python/blob/master/README.md). +A getting started guide for the SNET SDK for Python is available [here](https://github.com/singnet/snet-sdk-python/blob/master/README.md). ### Package Diagram From 502d4a2e831ed0896a7c9fc534ef2672ebfb9b77 Mon Sep 17 00:00:00 2001 From: Dronny Date: Thu, 15 Aug 2024 18:47:05 +0400 Subject: [PATCH 14/55] Finished concurrency_manager.md --- docs/main/concurrency_manager.md | 132 +++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/docs/main/concurrency_manager.md b/docs/main/concurrency_manager.md index e69de29..bccab3c 100644 --- a/docs/main/concurrency_manager.md +++ b/docs/main/concurrency_manager.md @@ -0,0 +1,132 @@ +## module: sdk.concurrency_manager + +[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/concurrency_manager.py) to GitHub + +Entities: +1. [ConcurrencyManager](#class-concurrencymanager) + - [\_\_init\_\_](#init) + - [concurrent_calls](#concurrent_calls) + - [get_token](#get_token) + - [__get_token](#__get_token) + - [__get_stub_for_get_token](#__get_stub_for_get_token) + - [__get_token_for_amount](#__get_token_for_amount) + - [record_successful_call](#record_successful_call) + +### class `ConcurrencyManager` + +extends: - + +is extended by: - + +#### description + +`ConcurrencyManager` provides a mechanism for managing the concurrency of service calls in the SDK. +It ensures that only a certain number of concurrent calls are made and handles the retrieval and management +of tokens for making service calls. + +#### attributes + +- `__concurrent_calls` (int): The number of concurrent calls allowed. +- `__token` (str): The token used for concurrent calls. +- `__planned_amount` (int): The planned amount for the payment. +- `__used_amount` (int): The amount used for the payment. + +#### methods + +#### `__init__` + +Initializes a new instance of the class. + +###### args: + +- concurrent_calls (int): The number of concurrent calls allowed. + +###### returns: + +- _None_ + +#### `concurrent_calls` + +decorator: `@property` + +Returns the number of concurrent calls allowed. + +###### returns: + +- The number of concurrent calls allowed. (int) + +#### `get_token` + +Retrieves a token for making service calls. + +###### args: + +- `service_client` (ServiceClient): The service client instance. +- `channel` (PaymentChannel): The payment channel instance. +- `service_call_price` (int): The price of a service call. + +###### returns: + +- The token for making service calls. (str) + +#### `__get_token` + +Retrieves a token for a service call. + +###### args: + +- `service_client` (ServiceClient): The service client instance. +- `channel` (PaymentChannel): The payment channel instance. +- `service_call_price` (int): The price of a service call. +- `new_token` (bool): Whether is needed a new token. Defaults to `False`. + +###### returns: + +- The token for the service call. (str) + +###### raises: + +- grpc.RpcError: If an error occurs while retrieving the token. + +#### `__get_stub_for_get_token` + +Retrieves the gRPC service stub for the TokenServiceStub. + +###### args: + +- `service_client` (ServiceClient): The service client instance. + +###### returns: + +- The gRPC service stub for the TokenServiceStub. (ServiceStub) + +#### `__get_token_for_amount` + +Retrieves a token for a given amount from the token service. + +This function retrieves a token for a given amount from the token service. It first retrieves the nonce +from the channel state, then it creates a stub for the token service using the `get_stub_for_get_token` +method. It then imports the `token_service_pb2` module and retrieves the current block number from the +service client's SDK web3 instance. It generates a message using the `solidity_keccak` method from the +`web3.Web3` class and generates signatures using the `generate_signature` method from the service client. +It creates a `TokenRequest` object with the necessary parameters and sends it to the token service using +the `GetToken` method of the stub. Finally, it returns the token reply object containing the token. + +###### args: + +- service_client (ServiceClient): The service client instance. +channel (PaymentChannel): The payment channel instance. +amount (int): The amount for which the token is requested. + +###### returns: + +- The token reply object containing the token. (Any) + +#### `record_successful_call` + +Increments the `__used_amount` attribute by 1 to record a successful call. + +###### returns: + +- _None_ + From e3f946adee11b51da3e018abfd02cf589a5d8b0a Mon Sep 17 00:00:00 2001 From: Dronny Date: Fri, 16 Aug 2024 12:56:21 +0400 Subject: [PATCH 15/55] Some fixes in concurrency_manager.md --- docs/main/concurrency_manager.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/main/concurrency_manager.md b/docs/main/concurrency_manager.md index bccab3c..3058b2a 100644 --- a/docs/main/concurrency_manager.md +++ b/docs/main/concurrency_manager.md @@ -114,9 +114,9 @@ the `GetToken` method of the stub. Finally, it returns the token reply object co ###### args: -- service_client (ServiceClient): The service client instance. -channel (PaymentChannel): The payment channel instance. -amount (int): The amount for which the token is requested. +- `service_client` (ServiceClient): The service client instance. +- `channel` (PaymentChannel): The payment channel instance. +- `amount` (int): The amount for which the token is requested. ###### returns: @@ -130,3 +130,4 @@ Increments the `__used_amount` attribute by 1 to record a successful call. - _None_ + \ No newline at end of file From 9ba9c7996d13c667fa42f3dcf7f382d86a40b147 Mon Sep 17 00:00:00 2001 From: Dronny Date: Fri, 16 Aug 2024 12:56:49 +0400 Subject: [PATCH 16/55] Finished ipfs_metadata_provider.md and metadata_provider.md --- .../ipfs_metadata_provider.md | 59 +++++++++++++++++++ docs/metadata_provider/metadata_provider.md | 59 +++++++++++++++++++ 2 files changed, 118 insertions(+) diff --git a/docs/metadata_provider/ipfs_metadata_provider.md b/docs/metadata_provider/ipfs_metadata_provider.md index e69de29..536e23f 100644 --- a/docs/metadata_provider/ipfs_metadata_provider.md +++ b/docs/metadata_provider/ipfs_metadata_provider.md @@ -0,0 +1,59 @@ +## module: sdk.metadata_provider.ipfs_metadata_provider + +[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/metadata_provider/metadata_provider.py) to GitHub + +Entities: +1. [IPFSMetadataProvider](#class-transactionerror) + - [fetch_org_metadata](#fetch_org_metadata) + - [fetch_service_metadata](#fetch_service_metadata) + - [enhance_service_metadata](#enhance_service_metadata) + +### class `IPFSMetadataProvider` + +extends: `object` + +is extended by: - + +#### description + +Class for extracting metadata from Interplanetary Filesystem (IPFS). + +#### methods + +#### `fetch_org_metadata` + +Retrieves metadata for the specified organization ID from IPFS. + +###### args: + +- org_id (str): The ID of the organization. + +###### returns: + +- Metadata of a specified organization. (dict | OrganizationMetadata (from `snet.cli`)) + +#### `fetch_service_metadata` + +Retrieves metadata for the specified service from the metadata provider. + +###### args: + +- org_id (str): The ID of the organization. +- service_id (str): The ID of the service. + +###### returns: + +- Metadata of a specified service. (dict | MPEServiceMetadata (from `snet.cli`)) + +#### `enhance_service_metadata` + +Enhances the service group details by merging them with the organization group details. + +###### args: + +- service_metadata (dict): The metadata of the service. +- org_metadata (dict): The metadata of the organization. + +###### returns: + +- Metadata of a specified service. (dict | MPEServiceMetadata (from `snet.cli`)) diff --git a/docs/metadata_provider/metadata_provider.md b/docs/metadata_provider/metadata_provider.md index e69de29..f3d8347 100644 --- a/docs/metadata_provider/metadata_provider.md +++ b/docs/metadata_provider/metadata_provider.md @@ -0,0 +1,59 @@ +## module: sdk.metadata_provider.metadata_provider + +[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/metadata_provider/metadata_provider.py) to GitHub + +Entities: +1. [MetadataProvider](#class-transactionerror) + - [fetch_org_metadata](#fetch_org_metadata) + - [fetch_service_metadata](#fetch_service_metadata) + - [enhance_service_group_details](#enhance_service_group_details) + +### abstract class `MetadataProvider` + +extends: `object` + +is extended by: - + +#### description + +Abstract class for retrieving metadata from a metadata provider. + +#### methods + +#### abstract `fetch_org_metadata` + +Retrieves metadata for the specified organization ID from the metadata provider. + +###### args: + +- org_id (str): The ID of the organization. + +###### returns: + +- Metadata of a specified organization. (dict | OrganizationMetadata (from `snet.cli`)) + +#### abstract `fetch_service_metadata` + +Retrieves metadata for the specified service from the metadata provider. + +###### args: + +- org_id (str): The ID of the organization. +- service_id (str): The ID of the service. + +###### returns: + +- Metadata of a specified service. (dict | MPEServiceMetadata (from `snet.cli`)) + +#### abstract `enhance_service_group_details` + +Enhances the service group details by merging them with the organization group details. + +###### args: + +- service_metadata (dict): The metadata of the service. +- org_metadata (dict): The metadata of the organization. + +###### returns: + +- Metadata of a specified service. (dict | MPEServiceMetadata (from `snet.cli`)) From cefebc0bf686b2276d6775a47a56e124a61e1714 Mon Sep 17 00:00:00 2001 From: Dronny Date: Fri, 16 Aug 2024 14:10:19 +0400 Subject: [PATCH 17/55] Finished mpe_contract.md --- docs/mpe/mpe_contract.md | 171 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) diff --git a/docs/mpe/mpe_contract.md b/docs/mpe/mpe_contract.md index e69de29..6281223 100644 --- a/docs/mpe/mpe_contract.md +++ b/docs/mpe/mpe_contract.md @@ -0,0 +1,171 @@ +## module: sdk.mpe.mpe_contract + +[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/mpe/mpe_contract.py) to GitHub + +Entities: +1. [MPEContract](#class-mpecontract) + - [\_\_init\_\_](#init) + - [balance](#balance) + - [deposit](#deposit) + - [open_channel](#open_channel) + - [deposit_and_open_channel](#deposit_and_open_channel) + - [channel_add_funds](#channel_add_funds) + - [channel_extend](#channel_extend) + - [channel_extend_and_add_funds](#channel_extend_and_add_funds) + - [_fund_escrow_account](#_fund_escrow_account) + +### class `MPEContract` + +extends: - + +is extended by: - + +#### description + +The `MPEContract` class is responsible for interacting with the MultiPartyEscrow +contract on the Ethereum blockchain. It provides methods for retrieving the balance of an address, depositing +funds into the contract, opening a channel, adding funds to a channel, extending the expiration of a channel, and more. +The class is initialized with a Web3 object and an optional contract address. If no contract address is provided, +it uses the default MultiPartyEscrow contract. + +#### attributes + +- `web3` (Web3): An instance of the Web3 class for interacting with the Ethereum blockchain. +- `contract` (Contract): An instance of the `Contract` class from the `web3` library for interacting +with the MultiPartyEscrow contract. +- `event_topics` (list): A list of event topics for the MultiPartyEscrow contract. +- `deployment_block` (int | BlockNumber): The block number at which the MultiPartyEscrow contract was deployed. + +#### methods + +#### `__init__` + +Initializes a new instance of the class. The class is initialized with a Web3 object and an optional contract address. If no contract address is provided, +it uses the default MultiPartyEscrow contract. + +###### args: + +- `w3` (Web3): An instance of the Web3 class. +- `address` (str): The address of the MultiPartyEscrow contract. Defaults to None. + +###### returns: + +- _None_ + +#### `balance` + +Returns the balance of the given address in the MPE contract in cogs. + +###### args: + +- `address` (str): The address to retrieve the balance for. + +###### returns: + +- The balance in cogs. (int) + +#### `deposit` + +Deposit the specified amount of AGIX tokens in cogs into the MultiPartyEscrow contract. + +###### args: + +- `account` (Account): The account instance used to send the transaction. +- `amount_in_cogs` (int): The amount of AGIX tokens in cogs to deposit. + + +###### returns: + +- The transaction receipt of the deposit transaction. (TxReceipt) + +#### `open_channel` + +Opens a payment channel with the specified amount of AGIX tokens in cogs (taken from MPE) and expiration time. + +###### args: + +- `account` (Account): The account object used to send the transaction. +- `payment_address` (str): The address of the payment recipient. +- `group_id` (str): The ID of the payment group. +- `amount` (int): The amount of AGIX tokens in cogs to deposit into the channel. +- `expiration` (int): The expiration time of the payment channel in blocks. + +###### returns: + +- The transaction receipt of the transaction. (TxReceipt) + +#### `deposit_and_open_channel` + +Opens a payment channel with the specified amount of AGIX tokens in cogs (which are previously deposited on MPE) +and expiration time. The account must have sufficient allowance to perform the deposit, otherwise the account +first approves the transfer. + +###### args: + +- `account` (Account): The account object used to send the transaction. +- `payment_address` (str): The address of the payment recipient. +- `group_id` (str): The ID of the payment group. +- `amount` (int): The amount of AGIX tokens in cogs first for deposit on MPE, then for deposit on the channel. +- `expiration` (int): The expiration time of the payment channel in blocks. + +###### returns: + +- The transaction receipt of the transaction. (TxReceipt) + +#### `channel_add_funds` + +Adds funds to an existing payment channel. + +###### args: + +- `account` (Account): The account object used to sign the transaction. +- `channel_id` (int): The ID of the payment channel. +- `amount` (int): The amount of funds to add to the channel. + +###### returns: + +- The transaction receipt of the transaction. (TxReceipt) + +#### `channel_extend` + +Extends the expiration time of a payment channel. + +###### args: + +- `account` (Account): The account object used to send the transaction. +- `channel_id` (int): The ID of the payment channel. +- `expiration` (int): The new expiration time of the payment channel in blocks. + +###### returns: + +- The transaction receipt of the transaction. (TxReceipt) + +#### `channel_extend_and_add_funds` + +Extends the expiration time of a payment channel and adds funds to it. + +###### args: + +- `account` (Account): The account object used to send the transaction. +- `channel_id` (int): The ID of the payment channel. +- `expiration` (int): The new expiration time of the payment channel in blocks. +- `amount` (int): The amount of funds to add to the channel. + +###### returns: + +- The transaction receipt of the transaction. (TxReceipt) + +#### `_fund_escrow_account` + +Funds the escrow account for the given account with the specified amount. + +###### args: + +- `account` (Account): The account object used to send the transaction and for which +the escrow account needs to be funded. +- `amount` (int): The amount to be funded into the escrow account. + +###### returns: + +- The transaction receipt of the transaction. (TxReceipt | _None_) + From b7c5bf2d9ff4745b20d100a4e23de49b250af43a Mon Sep 17 00:00:00 2001 From: Dronny Date: Fri, 16 Aug 2024 15:12:17 +0400 Subject: [PATCH 18/55] Tiny fixes...again --- docs/main/account.md | 6 +++--- docs/main/init.md | 4 ++-- docs/main/service_client.md | 6 +++--- docs/mpe/mpe_contract.md | 10 ++++------ 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/docs/main/account.md b/docs/main/account.md index 3716f79..4795db8 100644 --- a/docs/main/account.md +++ b/docs/main/account.md @@ -73,7 +73,7 @@ the Ethereum blockchain. - `config` (dict): The configuration settings for the account. _Note_: In fact, this is the same config from `SnetSDK`. -- `web3` (Web3): An instance of the Web3 class for interacting with the Ethereum blockchain. +- `web3` (Web3): An instance of the `Web3` class for interacting with the Ethereum blockchain. - `mpe_contract` (MPEContract): An instance of the `MPEContract` class for interacting with the MultiPartyEscrow contract. - `token_contract` (Contract): An instance of the `Contract` class from the `web3` library for interacting @@ -92,9 +92,9 @@ Initializes a new instance of the `Account` class. ###### args: -- `w3` (Web3): An instance of the Web3 class. +- `w3` (Web3): An instance of the `Web3` class. - `config` (dict): A dictionary containing the configuration settings. -- `mpe_contract` (MPEContract): An instance of the MPEContract class. +- `mpe_contract` (MPEContract): An instance of the `MPEContract` class. ###### returns: diff --git a/docs/main/init.md b/docs/main/init.md index ca306bf..4846218 100644 --- a/docs/main/init.md +++ b/docs/main/init.md @@ -69,11 +69,11 @@ It provides methods for creating service clients, managing identities, and confi - `_metadata_provider` (MetadataProvider): An instance of the `MetadataProvider` class. _Note_: There is currently only one implementation of `MetadataProvider` which is `IPFSMetadataProvider`, so this attribute can only be initialized to `IPFSMetadataProvider` at this time. -- `web3` (Web3): An instance of the Web3 class for interacting with the Ethereum blockchain. +- `web3` (Web3): An instance of the `Web3` class for interacting with the Ethereum blockchain. - `mpe_contract` (MPEContract): An instance of the `MPEContract` class for interacting with the MultiPartyEscrow contract. - `ipfs_client` (ipfshttpclient.Client): An instance of the `ipfshttpclient.Client` class for interacting with the InterPlanetary File System. -- `registry_contract` (Contract): An instance of the `Contract` class for interacting with the Registry contract. +- `registry_contract` (Contract): An instance of the `Contract` class (from `snet.cli`) for interacting with the Registry contract. - `account` (Account): An instance of the `Account` class for interacting with the MultiPartyEscrow and SingularityNetToken contracts. diff --git a/docs/main/service_client.md b/docs/main/service_client.md index 991ef1d..a569d2c 100644 --- a/docs/main/service_client.md +++ b/docs/main/service_client.md @@ -52,7 +52,7 @@ The class is used to manage the communication and payment channel management for - `service_metadata` (MPEServiceMetadata): An instance of the `MPEServiceMetadata` class with the metadata of the specified service. - `payment_strategy` (PaymentStrategy): The payment strategy. _Note_: In fact, this is an instance of one of -the "PaymentStrategy" inheritor classes. +the `PaymentStrategy` inheritor classes. - `expiry_threshold` (int): The payment expiration threshold (in blocks). - `__base_grpc_channel` (grpc.Channel): The base gRPC channel. - `grpc_channel` (grpc.Channel): The gRPC channel with interceptor. @@ -64,7 +64,7 @@ working with channels and interacting with MPE. - `last_read_block` (int): The last read block number. - `account` (Account): An instance of the `Account` class for interacting with the MultiPartyEscrow and SingularityNetToken contracts. -- `sdk_web3` (Web3): The Web3 instance. +- `sdk_web3` (Web3): The `Web3` instance. - `mpe_address` (str): The MPE contract address. #### methods @@ -84,7 +84,7 @@ Initializes a new instance of the class. - `options` (dict): Additional options for the service client. - `mpe_contract` (MPEContract): The MPE contract instance. - `account` (Account): An instance of the `Account` class. -- `sdk_web3` (Web3): The Web3 instance. +- `sdk_web3` (Web3): The `Web3` instance. - `pb2_module` (str | ModuleType): The module containing the gRPC message definitions. ###### returns: diff --git a/docs/mpe/mpe_contract.md b/docs/mpe/mpe_contract.md index 6281223..804eb0c 100644 --- a/docs/mpe/mpe_contract.md +++ b/docs/mpe/mpe_contract.md @@ -24,9 +24,7 @@ is extended by: - The `MPEContract` class is responsible for interacting with the MultiPartyEscrow contract on the Ethereum blockchain. It provides methods for retrieving the balance of an address, depositing -funds into the contract, opening a channel, adding funds to a channel, extending the expiration of a channel, and more. -The class is initialized with a Web3 object and an optional contract address. If no contract address is provided, -it uses the default MultiPartyEscrow contract. +funds into the contract, opening a channel, adding funds to a channel, extending the expiration of a channel, and more. #### attributes @@ -40,12 +38,12 @@ with the MultiPartyEscrow contract. #### `__init__` -Initializes a new instance of the class. The class is initialized with a Web3 object and an optional contract address. If no contract address is provided, -it uses the default MultiPartyEscrow contract. +Initializes a new instance of the class. The class is initialized with a Web3 object and an optional contract address. +If no contract address is provided, it uses the default MultiPartyEscrow contract. ###### args: -- `w3` (Web3): An instance of the Web3 class. +- `w3` (Web3): An instance of the `Web3` class. - `address` (str): The address of the MultiPartyEscrow contract. Defaults to None. ###### returns: From 02dfdc14ced43729a6b2f1fbd710390b5acbc6b2 Mon Sep 17 00:00:00 2001 From: Dronny Date: Fri, 16 Aug 2024 15:12:42 +0400 Subject: [PATCH 19/55] Finished payment_channel.md --- docs/mpe/payment_channel.md | 105 ++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/docs/mpe/payment_channel.md b/docs/mpe/payment_channel.md index e69de29..1e2c3f2 100644 --- a/docs/mpe/payment_channel.md +++ b/docs/mpe/payment_channel.md @@ -0,0 +1,105 @@ +## module: sdk.mpe.payment_channel + +[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/mpe/payment_channel.py) to GitHub + +Entities: +1. [PaymentChannel](#class-mpecontract) + - [\_\_init\_\_](#init) + - [add_funds](#add_funds) + - [extend_expiration](#extend_expiration) + - [extend_and_add_funds](#extend_and_add_funds) + - [sync_state](#sync_state) + - [_get_current_channel_state](#_get_current_channel_state) + +### class `PaymentChannel` + +extends: - + +is extended by: - + +#### description + +The PaymentChannel (payment_channel.py:8-65:135) class is responsible for managing a payment channel +in the SingularityNET platform. + +#### attributes + +- `channel_id` (int): The ID of the payment channel. +- `web3` (Web3): An instance of the `Web3` class for interacting with the Ethereum blockchain. +- `account` (Account): An instance of the `Account` class for interacting with the MultiPartyEscrow and SingularityNetToken contracts. +- `mpe_contract` (MPEContract): An instance of the `MPEContract` class for interacting with the MultiPartyEscrow contract. +- `payment_channel_state_service_client` (ServiceStub): A stub for interacting with PaymentChannelStateService via gRPC. +- `state` (dict): The current state of the payment channel. It contains the following keys: + - `nonce` (int): The current nonce of the payment channel. + - `last_signed_amount` (int): The last signed amount of the payment channel. +#### methods + +#### `__init__` + +Initializes a new instance of the class. + +###### args: + +- `channel_id` (str): The ID of the payment channel. +- `w3` (Web3): An instance of the `Web3` class for interacting with the Ethereum blockchain. +- `account` (Account): An instance of the `Account` class for managing Ethereum accounts. +- `payment_channel_state_service_client` (ServiceStub): A stub for interacting with PaymentChannelStateService via gRPC. +- `mpe_contract` (MPEContract): An instance of the `MPEContract` class for interacting with the MultiPartyEscrow contract. + +###### returns: + +- _None_ + +#### `add_funds` + +Adds funds to the payment channel. + +###### args: + +- `amount` (int): The amount of funds to add to the payment channel. + +###### returns: + +- The transaction receipt of the transaction. (TxReceipt) + +#### `extend_expiration` + +Extends the expiration time of the payment channel. + +###### args: + +- `expiration` (int): The new expiration time of the payment channel in blocks. + +###### returns: + +- The transaction receipt of the transaction. (TxReceipt) + +#### `extend_and_add_funds` + +Extends the expiration time of a payment channel and adds funds to it. + +###### args: + +- `expiration` (int): The new expiration time of the payment channel in blocks. +- `amount` (int): The amount of funds to add to the channel. + +###### returns: + +- The transaction receipt of the transaction. (TxReceipt) + +#### `sync_state` + +This method gets the channel state data from the MPE and the daemon and updates all values of the state field. + +###### returns: + +- _None_ + +#### `_get_current_channel_state` + +Receives channel state data from the daemon via gRPC using PaymentChannelStateService and returns it. + +###### returns: + +- A tuple containing the current nonce and the current signed amount of funds. (tuple[int, int]) + From e6e6c9ed6ed2d0464601f55f182be05bedad31e4 Mon Sep 17 00:00:00 2001 From: Dronny Date: Fri, 16 Aug 2024 18:11:24 +0400 Subject: [PATCH 20/55] One fix --- docs/mpe/payment_channel.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/mpe/payment_channel.md b/docs/mpe/payment_channel.md index 1e2c3f2..be27163 100644 --- a/docs/mpe/payment_channel.md +++ b/docs/mpe/payment_channel.md @@ -3,7 +3,7 @@ [link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/mpe/payment_channel.py) to GitHub Entities: -1. [PaymentChannel](#class-mpecontract) +1. [PaymentChannel](#class-paymentchannel) - [\_\_init\_\_](#init) - [add_funds](#add_funds) - [extend_expiration](#extend_expiration) @@ -32,6 +32,7 @@ in the SingularityNET platform. - `state` (dict): The current state of the payment channel. It contains the following keys: - `nonce` (int): The current nonce of the payment channel. - `last_signed_amount` (int): The last signed amount of the payment channel. + #### methods #### `__init__` From fc43e69acf9b4f1348f7137c6bec6e4e235192b5 Mon Sep 17 00:00:00 2001 From: Dronny Date: Fri, 16 Aug 2024 18:11:44 +0400 Subject: [PATCH 21/55] Finished payment_channel_provider.md --- docs/mpe/payment_channel_provider.md | 117 +++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/docs/mpe/payment_channel_provider.md b/docs/mpe/payment_channel_provider.md index e69de29..af8c612 100644 --- a/docs/mpe/payment_channel_provider.md +++ b/docs/mpe/payment_channel_provider.md @@ -0,0 +1,117 @@ +## module: sdk.mpe.payment_channel_provider + +[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/mpe/payment_channel_provider.py) to GitHub + +Entities: +1. [PaymentChannelProvider](#class-paymentchannelprovider) + - [\_\_init\_\_](#init) + - [get_past_open_channels](#get_past_open_channels) + - [open_channel](#open_channel) + - [deposit_and_open_channel](#deposit_and_open_channel) + - [_get_newly_opened_channel](#_get_newly_opened_channel) + +### class `PaymentChannelProvider` + +extends: `object` + +is extended by: - + +#### description + +A class for managing payment channels. + +#### attributes + +- `web3` (Web3): An instance of the `Web3` class for interacting with the Ethereum blockchain. +- `mpe_contract` (MPEContract): An instance of the `MPEContract` class for interacting with the MultiPartyEscrow contract. +with the MultiPartyEscrow contract. +- `event_topics` (list): A list of event topics for the MultiPartyEscrow contract. +- `deployment_block` (int | BlockNumber): The block number at which the MultiPartyEscrow contract was deployed. +- `payment_channel_state_service_client` (ServiceStub): A stub for interacting with PaymentChannelStateService via gRPC. + +#### methods + +#### `__init__` + +Initializes a new instance of the class. + +###### args: + +- `w3` (Web3): An instance of the `Web3` class for interacting with the Ethereum blockchain. +- payment_channel_state_service_client` (ServiceStub): A stub for interacting with PaymentChannelStateService via gRPC. +- `mpe_contract` (MPEContract): An instance of the `MPEContract` class for interacting with the MultiPartyEscrow contract. + +###### returns: + +- _None_ + +#### `get_past_open_channels` + +Extracts a list of all past open payment channels from the blockchain, filters it by account and payment group, +and returns it. + +###### args: + +- `account` (Account): The account object to filter the channels by its address and signer address. +- `payment_address` (str): The payment address to filter the channels by. +- `group_id` (str): The group ID to filter the channels by. +- `starting_block_number` (int): The starting block number of the block range. Defaults to 0. +- `to_block_number` (int): The ending block number of the block range. Defauls to _None_. + +###### returns: + +- A list of payment channels. (list[PaymentChannel]) + +#### `open_channel` + +Opens a payment channel with the specified amount of AGIX tokens in cogs (taken from MPE) and expiration time. +And then returns it. + +###### args: + +- `account` (Account): The account object used to send the transaction. +- `amount` (int): The amount of AGIX tokens in cogs to deposit into the channel. +- `expiration` (int): The expiration time of the payment channel in blocks. +- `payment_address` (str): The address of the payment recipient. +- `group_id` (str): The ID of the payment group. + +###### returns: + +- The newly opened payment channel. (PaymentChannel) + +#### `deposit_and_open_channel` + +Opens a payment channel with the specified amount of AGIX tokens in cogs (which are previously deposited on MPE) +and expiration time. And then returns it. + +###### args: + +- `account` (Account): The account object used to send the transaction. +- `amount` (int): The amount of AGIX tokens in cogs to deposit into the channel. +- `expiration` (int): The expiration time of the payment channel in blocks. +- `payment_address` (str): The address of the payment recipient. +- `group_id` (str): The ID of the payment group. + +###### returns: + +- The newly opened payment channel. (PaymentChannel) + +#### `_get_newly_opened_channel` + +Retrieves the newly opened payment channel from blockchain based on the given data. + +###### args: + +- `receipt` (dict): The receipt of the transaction that opened the payment channel. +- `account` (Account): The account object associated with the payment channel. +- `payment_address` (str): The payment address of the payment channel. +- `group_id` (str): The ID of the payment group. + +###### returns: + +- The newly opened payment channel. (PaymentChannel) + +###### raises: + +- Exception: If no payment channels are found for the given data. + From 6abbbc03d8cc8cd2ce5ef09b4100a6f6183fb90f Mon Sep 17 00:00:00 2001 From: Dronny Date: Mon, 19 Aug 2024 12:16:04 +0400 Subject: [PATCH 22/55] Finished payment_strategy.md --- docs/payment_strategies/payment_strategy.md | 47 +++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/docs/payment_strategies/payment_strategy.md b/docs/payment_strategies/payment_strategy.md index e69de29..87527e1 100644 --- a/docs/payment_strategies/payment_strategy.md +++ b/docs/payment_strategies/payment_strategy.md @@ -0,0 +1,47 @@ +## module: sdk.payment_strategies.payment_staregy + + + +[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/payment_strategies/payment_staregy.py) to GitHub + +Entities: +1. [PaymentStrategy](#class-paymentstrategy) + - [get_payment_metadata](#get_payment_metadata) + - [get_price](#get_price) + +### abstract class `PaymentStrategy` + +extends: `object` + +is extended by: `DefaultPaymentStrategy`, `DefaultPaymentStrategy`, `PaidCallPaymentStrategy`, `PrePaidPaymentStrategy` + +#### description + +Abstract base class for payment strategies. Defines the interface for organizing payment strategy. + +#### methods + +#### abstract `get_payment_metadata` + +Determines and returns payment metadata for a specified service client. + +###### args: + +- `service_client` (ServiceClient): The service client object. + +###### returns: + +- Payment metadata. (list[tuple[str, Any]]) + +#### abstract `get_price` + +Returns the price for calling a service using the provided client service. + +###### args: + +- `service_client` (ServiceClient): The service client object. + +###### returns: + +- Price of calling service in cogs. (int) + From 46f799dbc5c186f10b014e640592af01672d04b8 Mon Sep 17 00:00:00 2001 From: Dronny Date: Mon, 19 Aug 2024 14:00:20 +0400 Subject: [PATCH 23/55] Some additions and corrections --- .../ipfs_metadata_provider.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/metadata_provider/ipfs_metadata_provider.md b/docs/metadata_provider/ipfs_metadata_provider.md index 536e23f..d2753e0 100644 --- a/docs/metadata_provider/ipfs_metadata_provider.md +++ b/docs/metadata_provider/ipfs_metadata_provider.md @@ -18,8 +18,27 @@ is extended by: - Class for extracting metadata from Interplanetary Filesystem (IPFS). +#### attributes + +- `registry_contract` (Contract): An instance of the `Contract` class (from `snet.cli`) for interacting with the Registry contract. +- `ipfs_client` (ipfshttpclient.Client): An instance of the `ipfshttpclient.Client` class for interacting with the +InterPlanetary File System. + #### methods +#### `__init__` + +Initializes a new instance of the class. + +###### args: + +- `ipfs_client` (ipfshttpclient.Client): The IPFS client to use for fetching metadata. +- `registry_contract` (Contract): The contract instance of the registry. + +###### returns: + +- _None_ + #### `fetch_org_metadata` Retrieves metadata for the specified organization ID from IPFS. From 091de6e6bea320a28d7c51bf5c5694be21b2d71a Mon Sep 17 00:00:00 2001 From: Dronny Date: Mon, 19 Aug 2024 14:00:45 +0400 Subject: [PATCH 24/55] Finished default_payment_strategy.md --- .../default_payment_strategy.md | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/docs/payment_strategies/default_payment_strategy.md b/docs/payment_strategies/default_payment_strategy.md index e69de29..7998758 100644 --- a/docs/payment_strategies/default_payment_strategy.md +++ b/docs/payment_strategies/default_payment_strategy.md @@ -0,0 +1,96 @@ +## module: sdk.payment_strategies.default_payment_strategy + +[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/payment_strategies/default_payment_strategy.py) to GitHub + +Entities: +1. [DefaultPaymentStrategy](#class-paymentstrategy) + - [\_\_init\_\_](#init) + - [set_concurrency_token](#set_concurrency_token) + - [set_channel](#set_channel) + - [get_payment_metadata](#get_payment_metadata) + - [get_concurrency_token_and_channel](#get_concurrency_token_and_channel) + +### class `DefaultPaymentStrategy` + +extends: `PaymentStrategy` + +is extended by: + +#### description + +The `DefaultPaymentStrategy` class is an implementation of the `PaymentStrategy`. It defines and returns +payment metadata for a given service client, taking into account free calls, concurrency, and various +payment strategies. In fact, it is not an implementation of a payment strategy on its own, this class is used +by default and selects a payment strategy from `FreeCallPaymentStrategy`, `PaidCallPaymentStrategy` +and `PrePaidPaymentStrategy`. + +#### attributes + +- `concurrent_calls` (int): The number of concurrent calls allowed. +- `concurrencyManager` (ConcurrencyManager): An instance of the `ConcurrencyManager` class for managing concurrency. +- `channel` (PaymentChannel): The payment channel used for a specific service call. + +#### methods + +#### `__init__` + +Initializes a new instance of the class. + +###### args: + +- `concurrent_calls` (int): The number of concurrent calls allowed. Defaults to 1. + +###### returns: + +- _None_ + +#### `set_concurrency_token` + +Sets the concurrency token for the concurrency manager. + +###### args: + +- `token` (str): The token to be set. + +###### returns: + +- _None_ + +#### `set_channel` + +Sets a new channel object. + +###### args: + +- `channel` (PaymentChannel): The channel to set for the `DefaultPaymentStrategy` object. + +###### returns: + +- _None_ + +#### `get_payment_metadata` + +Retrieves payment metadata for the specified service client. Depending on several conditions, creates +an instance of one of the `FreeCallPaymentStrategy`, `PaidCallPaymentStrategy` and `PrePaidPaymentStrategy` +classes and calls the method of the same name in it. + +###### args: + +- `service_client` (ServiceClient): The service client object. + +###### returns: + +- The payment metadata. (list[tuple[str, Any]]) + +#### `get_concurrency_token_and_channel` + +Retrieves the concurrency token and channel for a given service client. + +###### args: + +- `service_client` (ServiceClient): The service client instance. + +###### returns: + +- The concurrency token and channel. (tuple[str, PaymentChannel]) + From 38fa04f477998101b567a784a3ec76bec22d8d61 Mon Sep 17 00:00:00 2001 From: Dronny Date: Mon, 19 Aug 2024 15:24:10 +0400 Subject: [PATCH 25/55] Tiny fixes --- docs/payment_strategies/default_payment_strategy.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/payment_strategies/default_payment_strategy.md b/docs/payment_strategies/default_payment_strategy.md index 7998758..b5c31f5 100644 --- a/docs/payment_strategies/default_payment_strategy.md +++ b/docs/payment_strategies/default_payment_strategy.md @@ -3,8 +3,8 @@ [link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/payment_strategies/default_payment_strategy.py) to GitHub Entities: -1. [DefaultPaymentStrategy](#class-paymentstrategy) - - [\_\_init\_\_](#init) +1. [DefaultPaymentStrategy](#class-defaultpaymentstrategy) + - [\_\_init\_\_](#__init__) - [set_concurrency_token](#set_concurrency_token) - [set_channel](#set_channel) - [get_payment_metadata](#get_payment_metadata) @@ -14,7 +14,7 @@ Entities: extends: `PaymentStrategy` -is extended by: +is extended by: - #### description From c2de1850b6654c09d60166728458f9ca9aa919fc Mon Sep 17 00:00:00 2001 From: Dronny Date: Mon, 19 Aug 2024 15:24:41 +0400 Subject: [PATCH 26/55] Finished freecall_payment_strategy.md --- .../freecall_payment_strategy.md | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/docs/payment_strategies/freecall_payment_strategy.md b/docs/payment_strategies/freecall_payment_strategy.md index e69de29..d67b24c 100644 --- a/docs/payment_strategies/freecall_payment_strategy.md +++ b/docs/payment_strategies/freecall_payment_strategy.md @@ -0,0 +1,68 @@ +## module: sdk.payment_strategies.freecall_payment_strategy + +[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/payment_strategies/freecall_payment_strategy.py) to GitHub + +Entities: +1. [FreeCallPaymentStrategy](#class-freecallpaymentstrategy) + - [is_free_call_available](#is_free_call_available) + - [get_payment_metadata](#get_payment_metadata) + - [generate_signature](#generate_signature) + +### class `FreeCallPaymentStrategy` + +extends: `PaymentStrategy` + +is extended by: - + +#### description + +The `FreeCallPaymentStrategy` class is a concrete implementation of the `PaymentStrategy` interface. +It allows you to use free calls (which can be received from the [Dapp](https://beta.singularitynet.io/)) to call services. + +#### methods + +#### `is_free_call_available` + +Checks if a free call is available for a given service client. + +###### args: + +- `service_client` (ServiceClient): The service client instance. + +###### returns: + +- True if a free call is available, False otherwise. (bool) + +###### raises: + +- Exception: If an error occurs while checking the free call availability. + +_Note_: If any exception occurs during the process, it returns False. + +#### `get_payment_metadata` + +Retrieves the payment metadata for a service client using the provided free call configuration. + +###### args: + +- `service_client` (ServiceClient): The service client instance. + +###### returns: + +- The payment metadata. (list[tuple[str, Any]]) + +#### `generate_signature` + +Generates a signature for the given service client using the provided free call configuration. + +###### args: + +- `service_client` (ServiceClient): The service client instance. + +###### returns: + +- A tuple containing the generated signature and the current block number. (tuple[bytes, int]) + +###### raises: + +- Exception: If any of the required parameters for the free call strategy are missing. From 030e003864aaddb94d792cba9e293f10f1f46451 Mon Sep 17 00:00:00 2001 From: Dronny Date: Thu, 22 Aug 2024 11:00:02 +0400 Subject: [PATCH 27/55] Tiny change --- docs/payment_strategies/freecall_payment_strategy.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/payment_strategies/freecall_payment_strategy.md b/docs/payment_strategies/freecall_payment_strategy.md index d67b24c..2338e8f 100644 --- a/docs/payment_strategies/freecall_payment_strategy.md +++ b/docs/payment_strategies/freecall_payment_strategy.md @@ -17,7 +17,8 @@ is extended by: - #### description The `FreeCallPaymentStrategy` class is a concrete implementation of the `PaymentStrategy` interface. -It allows you to use free calls (which can be received from the [Dapp](https://beta.singularitynet.io/)) to call services. +It allows you to use free calls (which can be received from the [Dapp](https://beta.singularitynet.io/)) to +call services. #### methods @@ -41,7 +42,8 @@ _Note_: If any exception occurs during the process, it returns False. #### `get_payment_metadata` -Retrieves the payment metadata for a service client using the provided free call configuration. +Retrieves the payment metadata for a service client with the field `snet-paument-type` equals to `free-call` +using the provided free call configuration. ###### args: From 3ac2e02aec4c4f93dca5346523e21941bc41193c Mon Sep 17 00:00:00 2001 From: Dronny Date: Thu, 22 Aug 2024 11:00:30 +0400 Subject: [PATCH 28/55] Finished paidcall_payment_strategy.md --- .../paidcall_payment_strategy.md | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/docs/payment_strategies/paidcall_payment_strategy.md b/docs/payment_strategies/paidcall_payment_strategy.md index e69de29..1207f47 100644 --- a/docs/payment_strategies/paidcall_payment_strategy.md +++ b/docs/payment_strategies/paidcall_payment_strategy.md @@ -0,0 +1,109 @@ +## module: sdk.payment_strategies.paidcall_payment_strategy + +[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/payment_strategies/paidcall_payment_strategy.py) to GitHub + +Entities: +1. [PaidCallPaymentStrategy](#class-paidcallpaymentstrategy) + - [\_\_init\_\_](#__init__) + - [get_price](#get_price) + - [get_payment_metadata](#get_payment_metadata) + - [select_channel](#select_channel) + - [_has_sufficient_funds](#_has_sufficient_funds) + - [_is_valid](#_is_valid) + + +### class `PaidCallPaymentStrategy` + +extends: `PaymentStrategy` + +is extended by: - + +#### description + +The `PaidCallPaymentStrategy` class is a concrete implementation of the `PaymentStrategy` interface. +This is the simplest payment strategy among those presented. In it availability of channel, funds and +expiration are checked before each call and the payment itself is made each call. + +#### attributes + +- `block_offset` (int): Current block number. +- `call_allowance` (int): The amount of allowed calls. + +#### methods + +#### `__init__` + +Initializes a new instance of the class. + +###### args: + +- `block_offset` (int): Current block number. Defaults to 240. +- `call_allowance` (int): The amount of allowed calls. Defaults to 1. + +###### returns: + +- _None_ + +#### `get_price` + +Returns the price of the service call using service client. + +###### args: + +- `service_client` (ServiceClient): The service client object. + +###### returns: + +- The price of the service call. (int) + +#### `get_payment_metadata` + +Creates and returns the payment metadata for a service client with the field `snet-paument-type` equals to `escrow`. + +###### args: + +- `service_client` (ServiceClient): The service client object. + +###### returns: + +- The payment metadata. (list[tuple[str, Any]]) + +#### `select_channel` + +Retrieves the suitable payment channel from the MPE. Opens the channel, extends expiration +and adds funds id it is necessary. + +###### args: + +- `service_client` (ServiceClient): The service client object. + +###### returns: + +- The payment channel for the service calling. (PaymentChannel) + +#### static `_has_sufficient_funds` + +Checks whether the payment channel has the required amount of funds. + +###### args: + +- `channel` (PaymentChannel): The payment channel to check. +- `amount` (int): The amount of funds in cogs to check. + +###### returns: + +- True if the channel has enough funds, False otherwise. (bool) + +#### static `_is_valid` + +Checks whether the payment channel expiration ends later than it is necessary. + +###### args: + +- `channel` (PaymentChannel): The payment channel to check. +- `expiry` (int): The expiration time in blocks to check. + +###### returns: + +- True if the channel has enough expiration, False otherwise. (bool) + From 1bf106e38f6d3bc30d42ee7002d698a48f1fa427 Mon Sep 17 00:00:00 2001 From: Dronny Date: Thu, 22 Aug 2024 11:00:57 +0400 Subject: [PATCH 29/55] Finished prepaid_payment_strategy.md --- .../prepaid_payment_strategy.md | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/docs/payment_strategies/prepaid_payment_strategy.md b/docs/payment_strategies/prepaid_payment_strategy.md index e69de29..a7721d3 100644 --- a/docs/payment_strategies/prepaid_payment_strategy.md +++ b/docs/payment_strategies/prepaid_payment_strategy.md @@ -0,0 +1,126 @@ +## module: sdk.payment_strategies.prepaid_payment_strategy + +[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/payment_strategies/prepaid_payment_strategy.py) to GitHub + +Entities: +1. [PrePaidPaymentStrategy](#class-prepaidpaymentstrategy) + - [\_\_init\_\_](#__init__) + - [get_price](#get_price) + - [get_payment_metadata](#get_payment_metadata) + - [get_concurrency_token_and_channel](#get_concurrency_token_and_channel) + - [select_channel](#select_channel) + - [_has_sufficient_funds](#_has_sufficient_funds) + - [_is_valid](#_is_valid) + + +### class `PrePaidPaymentStrategy` + +extends: `PaymentStrategy` + +is extended by: - + +#### description + +The `PrePaidPaymentStrategy` class is a concrete implementation of the `PaymentStrategy` interface. +The prepaid payment strategy is similar to the paid call strategy, but allows you to pay for several calls at once +and then make them. + +#### attributes + +- `concurrency_manager`: The `ConcurrencyManager` instance. +- `block_offset` (int): Current block number. +- `call_allowance` (int): The amount of allowed calls. + +#### methods + +#### `__init__` + +Initializes a new instance of the class. + +###### args: + +- `concurrency_manager`: The `ConcurrencyManager` instance. +- `block_offset` (int): Current block number. Defaults to 240. +- `call_allowance` (int): The amount of allowed calls. Defaults to 1. + +###### returns: + +- _None_ + +#### `get_price` + +Returns the price of the service calls using service client. + +###### args: + +- `service_client` (ServiceClient): The service client object. + +###### returns: + +- The price of the service call. (int) + +#### `get_payment_metadata` + +Creates and returns the payment metadata for a service client with the field `snet-paument-type` equals +to `prepaid-call`. + +###### args: + +- `service_client` (ServiceClient): The service client object. +- `channel` (PaymentChannel): The payment channel to make service call. + +###### returns: + +- The payment metadata. (list[tuple[str, Any]]) + +#### `get_concurrency_token_and_channel` + +Retrieves the concurrency token and channel from the payment strategy. + +###### args: + +- `service_client` (ServiceClient): The service client object. + +###### returns: + +- The concurrency token and channel. (tuple[str, PaymentChannel]) + +#### `select_channel` + +Retrieves the suitable payment channel from the MPE. Opens the channel, extends expiration +and adds funds if it is necessary. + +###### args: + +- `service_client` (ServiceClient): The service client object. + +###### returns: + +- The payment channel for the service calling. (PaymentChannel) + +#### static `_has_sufficient_funds` + +Checks whether the payment channel has the required amount of funds. + +###### args: + +- `channel` (PaymentChannel): The payment channel to check. +- `amount` (int): The amount of funds in cogs to check. + +###### returns: + +- True if the channel has enough funds, False otherwise. (bool) + +#### static `_is_valid` + +Checks whether the payment channel expiration ends later than it is necessary. + +###### args: + +- `channel` (PaymentChannel): The payment channel to check. +- `expiry` (int): The expiration time in blocks to check. + +###### returns: + +- True if the channel has enough expiration, False otherwise. (bool) + From 7b6afdc767bd3cee9806caee50d8d7ff8d2bb214 Mon Sep 17 00:00:00 2001 From: Dronny Date: Thu, 22 Aug 2024 11:26:03 +0400 Subject: [PATCH 30/55] Minor fixes --- docs/main/account.md | 2 +- docs/main/concurrency_manager.md | 2 +- docs/main/init.md | 4 ++-- docs/main/service_client.md | 2 +- docs/metadata_provider/ipfs_metadata_provider.md | 1 + docs/mpe/mpe_contract.md | 2 +- docs/mpe/payment_channel.md | 2 +- docs/mpe/payment_channel_provider.md | 2 +- docs/payment_strategies/paidcall_payment_strategy.md | 2 +- docs/payment_strategies/prepaid_payment_strategy.md | 2 +- 10 files changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/main/account.md b/docs/main/account.md index 4795db8..d3d8974 100644 --- a/docs/main/account.md +++ b/docs/main/account.md @@ -4,7 +4,7 @@ Entities: 1. [TransactionError](#class-transactionerror) - - [\_\_init\_\_](#init) + - [\_\_init\_\_](#__init__) - [\_\_str\_\_](#str) 2. [Account](#class-account) - [\_\_init\_\_](#init-1) diff --git a/docs/main/concurrency_manager.md b/docs/main/concurrency_manager.md index 3058b2a..978efe4 100644 --- a/docs/main/concurrency_manager.md +++ b/docs/main/concurrency_manager.md @@ -4,7 +4,7 @@ Entities: 1. [ConcurrencyManager](#class-concurrencymanager) - - [\_\_init\_\_](#init) + - [\_\_init\_\_](#__init__) - [concurrent_calls](#concurrent_calls) - [get_token](#get_token) - [__get_token](#__get_token) diff --git a/docs/main/init.md b/docs/main/init.md index 4846218..d89af00 100644 --- a/docs/main/init.md +++ b/docs/main/init.md @@ -4,9 +4,9 @@ Entities: 1. [Arguments](#class-arguments) - - [\_\_init\_\_](#init) + - [\_\_init\_\_](#__init__) 2. [SnetSDK](#class-snetsdk) - - [\_\_init\_\_](#init-1) + - [\_\_init\_\_](#__init__-1) - [setup_config](#setup_config) - [set_session_identity](#set_session_identity) - [create_service_client](#create_service_client) diff --git a/docs/main/service_client.md b/docs/main/service_client.md index a569d2c..f3d9976 100644 --- a/docs/main/service_client.md +++ b/docs/main/service_client.md @@ -4,7 +4,7 @@ Entities: 1. [ServiceClient](#class-serviceclient) - - [\_\_init\_\_](#init) + - [\_\_init\_\_](#__init__) - [call_rpc](#call_rpc) - [_generate_grpc_stub](#_generate_grpc_stub) - [get_grpc_base_channel](#get_grpc_base_channel) diff --git a/docs/metadata_provider/ipfs_metadata_provider.md b/docs/metadata_provider/ipfs_metadata_provider.md index d2753e0..9edde79 100644 --- a/docs/metadata_provider/ipfs_metadata_provider.md +++ b/docs/metadata_provider/ipfs_metadata_provider.md @@ -4,6 +4,7 @@ Entities: 1. [IPFSMetadataProvider](#class-transactionerror) + - [\_\_init\_\_](#__init__) - [fetch_org_metadata](#fetch_org_metadata) - [fetch_service_metadata](#fetch_service_metadata) - [enhance_service_metadata](#enhance_service_metadata) diff --git a/docs/mpe/mpe_contract.md b/docs/mpe/mpe_contract.md index 804eb0c..957fe39 100644 --- a/docs/mpe/mpe_contract.md +++ b/docs/mpe/mpe_contract.md @@ -4,7 +4,7 @@ Entities: 1. [MPEContract](#class-mpecontract) - - [\_\_init\_\_](#init) + - [\_\_init\_\_](#__init__) - [balance](#balance) - [deposit](#deposit) - [open_channel](#open_channel) diff --git a/docs/mpe/payment_channel.md b/docs/mpe/payment_channel.md index be27163..648ada7 100644 --- a/docs/mpe/payment_channel.md +++ b/docs/mpe/payment_channel.md @@ -4,7 +4,7 @@ Entities: 1. [PaymentChannel](#class-paymentchannel) - - [\_\_init\_\_](#init) + - [\_\_init\_\_](#__init__) - [add_funds](#add_funds) - [extend_expiration](#extend_expiration) - [extend_and_add_funds](#extend_and_add_funds) diff --git a/docs/mpe/payment_channel_provider.md b/docs/mpe/payment_channel_provider.md index af8c612..99a339d 100644 --- a/docs/mpe/payment_channel_provider.md +++ b/docs/mpe/payment_channel_provider.md @@ -4,7 +4,7 @@ Entities: 1. [PaymentChannelProvider](#class-paymentchannelprovider) - - [\_\_init\_\_](#init) + - [\_\_init\_\_](#__init__) - [get_past_open_channels](#get_past_open_channels) - [open_channel](#open_channel) - [deposit_and_open_channel](#deposit_and_open_channel) diff --git a/docs/payment_strategies/paidcall_payment_strategy.md b/docs/payment_strategies/paidcall_payment_strategy.md index 1207f47..2cd2c99 100644 --- a/docs/payment_strategies/paidcall_payment_strategy.md +++ b/docs/payment_strategies/paidcall_payment_strategy.md @@ -96,7 +96,7 @@ Checks whether the payment channel has the required amount of funds. #### static `_is_valid` -Checks whether the payment channel expiration ends later than it is necessary. +Checks if the payment channel expires later than it should. ###### args: diff --git a/docs/payment_strategies/prepaid_payment_strategy.md b/docs/payment_strategies/prepaid_payment_strategy.md index a7721d3..c5e28a7 100644 --- a/docs/payment_strategies/prepaid_payment_strategy.md +++ b/docs/payment_strategies/prepaid_payment_strategy.md @@ -113,7 +113,7 @@ Checks whether the payment channel has the required amount of funds. #### static `_is_valid` -Checks whether the payment channel expiration ends later than it is necessary. +Checks if the payment channel expires later than it should. ###### args: From ca1900684ecfa68ef214f33d281687ea4b0e8cd9 Mon Sep 17 00:00:00 2001 From: Dronny Date: Thu, 22 Aug 2024 11:54:00 +0400 Subject: [PATCH 31/55] Initial markup for training.md --- docs/training/training.md | 131 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/docs/training/training.md b/docs/training/training.md index e69de29..f27efd6 100644 --- a/docs/training/training.md +++ b/docs/training/training.md @@ -0,0 +1,131 @@ +## module: sdk.training.training + +[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/training/training.py) to GitHub + +Entities: +1. [ModelMethodMessage](#enum-modelmethodmessage) +2. [TrainingModel](#class-trainingmodel) + - [\_\_init\_\_](#__init__) + - [_invoke_model](#_invoke_model) + - [create_model](#create_model) + - [get_model_status](#get_model_status) + - [delete_model](#delete_model) + - [update_model_access](#_update_model_access) + - [get_all_models](#_get_all_models) + +### enum `ModelMethodMessage` + +#### description + +An enumeration containing method messages for the model from daemon code. + +_Note_: This is a class that inherits from "Enum" + +#### elements + +- `CreateModel` +- `GetModelStatus` +- `UpdateModelAccess` +- `GetAllModels` +- `DeleteModel` + +### class `TrainingModel` + +extends: - + +is extended by: - + +#### description + + + +#### attributes + +- + +#### methods + +#### `__init__` + +Initializes a new instance of the class. + +###### args: + +- + +###### returns: + +- _None_ + +#### `_invoke_model` + + + +###### args: + +- + +###### returns: + +- + +#### `create_model` + + + +###### args: + +- + +###### returns: + +- + +#### `get_model_status` + + + +###### args: + +- + +###### returns: + +- + +#### `delete_model` + + + +###### args: + +- + +###### returns: + +- + +#### `update_model_access` + + + +###### args: + +- + +###### returns: + +- + +#### `get_all_models` + + + +###### args: + +- + +###### returns: + +- + From 89acb8aa0fffe8ca0cca382ef57c22aa4d526fdf Mon Sep 17 00:00:00 2001 From: Dronny Date: Mon, 26 Aug 2024 17:35:50 +0300 Subject: [PATCH 32/55] The structure of the README.md has been reworked and some new information has been added to it. --- README.md | 219 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 162 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 3397700..fa69b63 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,4 @@ - - # snet-sdk-python SingularityNET SDK for Python @@ -19,10 +11,6 @@ The package is published in PyPI at the following link: |----------------------------------------------|---------------------------------------------------------------------| |[snet.sdk](https://pypi.org/project/snet.sdk/)|Integrate SingularityNET services seamlessly into Python applications| -## Getting Started - -These instructions are for the development and use of the SingularityNET SDK for Python. - ### Core concepts The SingularityNET SDK allows you to make calls to SingularityNET services programmatically from your application. @@ -30,6 +18,10 @@ To communicate between clients and services, SingularityNET uses [gRPC](https:// To handle payment of services, SingularityNET uses [Ethereum state channels](https://dev.singularitynet.io/docs/concepts/multi-party-escrow/). The SingularityNET SDK abstracts and manages state channels with service providers on behalf of the user and handles authentication with the SingularityNET services. +## Getting Started + +These instructions are for the development and use of the SingularityNET SDK for Python. + ### Usage To call a SingularityNET service, the user must be able to deposit funds (AGIX tokens) to the [Multi-Party Escrow](https://dev.singularitynet.io/docs/concepts/multi-party-escrow/) Smart Contract. @@ -65,24 +57,25 @@ See [test_sdk_client.py](https://github.com/singnet/snet-sdk-python/blob/master/ - network: You can set the Ethereum network that will be used to make a call; - force_update: If set to False, will reuse the existing gRPC stubs (if any) instead of downloading proto and regenerating them every time. -##### List organizations and their services +#### List organizations and their services You can use the sdk client instance`s methods get_organization_list() to list all organizations and get_services_list("org_id") to list all services of a given organization. ```python -print(snet_sdk.get_organization_list()) -print(snet_sdk.get_services_list(org_id="26072b8b6a0e448180f8c0e702ab6d2f")) +orgs_list = snet_sdk.get_organization_list() +print(*orgs_list, sep="\n") +# ... +# GoogleOrg3 +# 26072b8b6a0e448180f8c0e702ab6d2f +# 43416d873fcb454589900189474b2eaa +# ... ``` -##### Get service metadata - -The metadata of services is stored in IPFS. To view it, you need to call the `get_service_metadata()` method, passing -the organization id and the service id to it. ```python -service_metadata = snet_sdk.get_service_metadata(org_id="26072b8b6a0e448180f8c0e702ab6d2f", service_id="Exampleservice") -print(*service_metadata.m.items(), sep="\n") -print(*service_metadata.get_tags()) -print(service_metadata.get_payment_address(group_name="default_group")) +org_id = "26072b8b6a0e448180f8c0e702ab6d2f" +services_list = snet_sdk.get_services_list(org_id=org_id) +print(*services_list, sep="\n") +# Exampleservice ``` ### Calling the service @@ -95,13 +88,87 @@ service_client = snet_sdk.create_service_client(org_id="26072b8b6a0e448180f8c0e7 service_id="Exampleservice", group_name="default_group") ``` -##### Free call configuration + +After executing this code, you should have client libraries created for this service. They are located at the following path: `~/.snet/org_id/service_id/python/` + +_Note_: Currently you can only save files to `~/.snet/`. We will fix this in the future. + +The instance of service_client that has been generated can be utilized to invoke the methods that the service offers. You can list these using the `get_services_and_messages_info_as_pretty_string()` method: + +```python +print(service_client.get_services_and_messages_info_as_pretty_string()) +# Service: Calculator +# Method: add, Input: Numbers, Output: Result +# Method: sub, Input: Numbers, Output: Result +# Method: mul, Input: Numbers, Output: Result +# Method: div, Input: Numbers, Output: Result +# Message: Numbers +# Field: float a +# Field: float b +# Message: Result +# Field: float value + +``` + +To invoke the service's methods, you can use the `call_rpc()` method. This method requires the names of the method and data object, along with the data itself, to be passed into it. +To continue with our example, here’s a call to the *mul* method of the *Exampleservice* from the *26072b8b6a0e448180f8c0e702ab6d2f* organization: + +```python +result = service_client.call_rpc("mul", "Numbers", a=20, b=3) +print(f"Calculating 20 * 3: {result}") +# Calculating 20 * 3: 60.0 +``` + +For more information about gRPC and how to use it with Python, please see: +- [gRPC Basics - Python](https://grpc.io/docs/tutorials/basic/python.html) +- [gRPC Python’s documentation](https://grpc.io/grpc/python/) + +### The complete code of the example + +```python +from snet import sdk +config = { + "private_key": 'YOUR_PRIVATE_WALLET_KEY', + "eth_rpc_endpoint": f"https://sepolia.infura.io/v3/YOUR_INFURA_KEY", + "email": "your@email.com", + "concurrency": False, + "identity_name": "local_name_for_that_identity", + "identity_type": "key", + "network": "sepolia", + "force_update": False + } + +snet_sdk = sdk.SnetSDK(config) + +orgs_list = snet_sdk.get_organization_list() +print(*orgs_list, sep="\n") + +org_id = "26072b8b6a0e448180f8c0e702ab6d2f" +services_list = snet_sdk.get_services_list(org_id=org_id) +print(*services_list, sep="\n") + +service_client = snet_sdk.create_service_client(org_id="26072b8b6a0e448180f8c0e702ab6d2f", + service_id="Exampleservice", + group_name="default_group") + +result = service_client.call_rpc("mul", "Numbers", a=20, b=3) +print(f"Calculating 20 * 3: {result}") +``` + +_Note_: In this example, the user doesn't deposit funds to MPE, doesn't open a channel, and doesn't +perform other actions related to payment. In this case, the choice of payment strategy, as well as, if necessary, +opening a channel and depositing funds into MPE occurs automatically. For more information on payment, please +visit the [Payment](#payment) section. + +## Payment + +### Free call If you want to use the free calls you will need to pass these arguments to the `create_service_client()` method: ```python -free_call_auth_token-bin = "f2548d27ffd319b9c05918eeac15ebab934e5cfcd68e1ec3db2b92765", -free-call-token-expiry-block = 172800 +free_call_auth_token_bin = "f2548d27ffd319b9c05918eeac15ebab934e5cfcd68e1ec3db2b92765", +free_call_token_expiry_block = 172800 ``` You can receive these for a given service from the [Dapp](https://beta.singularitynet.io/) @@ -115,54 +182,35 @@ service_client = snet_sdk.create_service_client(org_id="26072b8b6a0e448180f8c0e7 free_call_token_expiry_block=172800) ``` -After executing this code, you should have client libraries created for this service. They are located at the following path: `~/.snet/org_id/service_id/python/` +### Paid call -Note: Currently you can only save files to `~/.snet/`. We will fix this in the future. - -##### Open channel and list services - -```python -service_client.open_channel(amount=123456, expiration=33333) -``` `open_channel(amount, expiration)` opens a payment channel with the specified amount of AGIX tokens in cogs and expiration time. Expiration is payment channel's TTL in blocks. ```python -service_client.deposit_and_open_channel(amount=123456, expiration=33333) +service_client.open_channel(amount=123456, expiration=33333) ``` -`deposit_and_open_channel(amount, expiration)` function does the same as the previous one, but first deposits -the specified amount of AGIX tokens in cogs into an MPE smart contract. -The instance of service_client that has been generated can be utilized to invoke the methods that the service offers. You can list these using the `get_services_and_messages_info_as_pretty_string()` method: +`deposit_and_open_channel(amount, expiration)` function does the same as the previous one, but first deposits +the specified amount of AGIX tokens in cogs into an MPE. ```python -print(service_client.get_services_and_messages_info_as_pretty_string()) +service_client.deposit_and_open_channel(amount=123456, expiration=33333) ``` -But if you need to process lists of services and messages, it is better to use the -`get_services_and_messages_info()` method: +`open_channel()` as well as `deposit_and_open_channel()` returns the payment channel. You can use it to add funds to it +and extend its expiration. ```python -services, messages = service_client.get_services_and_messages_info() -print(*services.items(), sep="\n") -print(*messages.items(), sep="\n") -``` - -##### Call +payment_channel = service_client.open_channel(amount=123456, expiration=33333) -To invoke the service's methods, you can use the `call_rpc()` method. This method requires the names of the method and data object, along with the data itself, to be passed into it. -To continue with our example, here’s a call to the *mul* method of the *Exampleservice* from the *26072b8b6a0e448180f8c0e702ab6d2f* organization: +payment_channel.add_funds(amount=123456) +payment_channel.extend_expiration(expiration=33333) -```python -result = service_client.call_rpc("mul", "Numbers", a=20, b=3) -print(f"Calculating 20 * 3: {result}") # Calculating 20 * 3: 60.0 +payment_channel.extend_and_add_funds(amount=123456, expiration=33333) ``` -For more information about gRPC and how to use it with Python, please see: -- [gRPC Basics - Python](https://grpc.io/docs/tutorials/basic/python.html) -- [gRPC Python’s documentation](https://grpc.io/grpc/python/) - -##### Other useful features +## Other useful features Service client also provides several useful functions. If you need to find out the number of the current block in the blockchain, there is a `get_current_block_number()` method for this: @@ -170,6 +218,7 @@ the current block in the blockchain, there is a `get_current_block_number()` met ```python block_number = service_client.get_current_block_number() print(f"Current block is {block_number}") +# Current block is 6574322 ``` To find out the price of calling a service function, you need to use the `get_price()` method: @@ -177,6 +226,62 @@ To find out the price of calling a service function, you need to use the `get_pr ```python price = service_client.get_price() print(f"The price in cogs for calling the service {service_client.service_id} is {price}") +# The price in cogs for calling the service Exampleservice is 1 +``` + +The metadata of services is stored in IPFS. To view it, you need to call the `get_service_metadata()` method, passing +the organization id and the service id to it. + +```python +service_metadata = snet_sdk.get_service_metadata(org_id="26072b8b6a0e448180f8c0e702ab6d2f", service_id="Exampleservice") +print(*service_metadata.m.items(), sep="\n", end="\n\n") +print(*service_metadata.get_tags(), sep=",", end="\n\n") +print(*service_metadata.get_all_endpoints_for_group(group_name="default_group"), sep=",", end="\n\n") + +# ('version', 1) +# ('display_name', 'Example service') +# ('encoding', 'proto') +# ('service_type', 'grpc') +# ('model_ipfs_hash', 'QmeyrQkEyba8dd4rc3jrLd5pEwsxHutfH2RvsSaeSMqTtQ') +# ('mpe_address', '0x7E0aF8988DF45B824b2E0e0A87c6196897744970') +# ('groups', [{'free_calls': 0, 'free_call_signer_address': '0x7DF35C98f41F3Af0df1dc4c7F7D4C19a71Dd059F', 'daemon_addresses': ['0x0709e9b78756b740ab0c64427f43f8305fd6d1a7'], 'pricing': [{'default': True, 'price_model': 'fixed_price', 'price_in_cogs': 1}], 'endpoints': ['http://node1.naint.tech:62400'], 'group_id': '/mb90Qs8VktxGQmU0uRu0bSlGgqeDlYrKrs+WbsOvOQ=', 'group_name': 'default_group'}]) +# ('service_description', {'url': 'https://ropsten-v2-publisher.singularitynet.io/org', 'short_description': 'Example service', 'description': 'Example service'}) +# ('media', [{'order': 1, 'url': 'https://ropsten-marketplace-service-assets.s3.us-east-1.amazonaws.com/26072b8b6a0e448180f8c0e702ab6d2f/services/d05c62bf9aa84843a195457d98417f4e/assets/20240327124952_asset.jpeg', 'file_type': 'image', 'asset_type': 'hero_image', 'alt_text': ''}]) +# ('contributors', [{'name': 'test', 'email_id': ''}]) +# ('tags', ['exampleservice']) +# +# exampleservice +# +# http://node1.naint.tech:62400 +``` + +In the section [Calling the service](#calling-the-service) we already talked about the function +`get_services_and_messages_info_as_pretty_string()`, with which you can get information about the methods and +messages of a service. But if you need to process lists of services and messages, it is better to use the +`get_services_and_messages_info()` method. + +```python +services, messages = service_client.get_services_and_messages_info() + +for service in services.items(): + print(service[0], *service[1], sep="\n") + +print() + +for message in messages.items(): + print(message[0], *message[1], sep="\n") + +# Calculator +# ('add', 'Numbers', 'Result') +# ('sub', 'Numbers', 'Result') +# ('mul', 'Numbers', 'Result') +# ('div', 'Numbers', 'Result') +# +# Numbers +# ('float', 'a') +# ('float', 'b') +# Result +# ('float', 'value') ``` --- From 030bcc89fdb852da2ddf36df98afe840480c2447 Mon Sep 17 00:00:00 2001 From: Dronny Date: Tue, 27 Aug 2024 11:42:35 +0300 Subject: [PATCH 33/55] Several changes in README.md --- README.md | 120 ++++++++++++++++++++++++------------------------------ 1 file changed, 53 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index fa69b63..5f2fefc 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,10 @@ The package is published in PyPI at the following link: The SingularityNET SDK allows you to make calls to SingularityNET services programmatically from your application. To communicate between clients and services, SingularityNET uses [gRPC](https://grpc.io/). -To handle payment of services, SingularityNET uses [Ethereum state channels](https://dev.singularitynet.io/docs/concepts/multi-party-escrow/). -The SingularityNET SDK abstracts and manages state channels with service providers on behalf of the user and handles authentication with the SingularityNET services. +To handle payment of services, SingularityNET uses +[Ethereum state channels](https://dev.singularitynet.io/docs/concepts/multi-party-escrow/). +The SingularityNET SDK abstracts and manages state channels with service providers on behalf of the user and +handles authentication with the SingularityNET services. ## Getting Started @@ -24,10 +26,12 @@ These instructions are for the development and use of the SingularityNET SDK for ### Usage -To call a SingularityNET service, the user must be able to deposit funds (AGIX tokens) to the [Multi-Party Escrow](https://dev.singularitynet.io/docs/concepts/multi-party-escrow/) Smart Contract. -To deposit these tokens or do any other transaction on the Ethereum blockchain, the user must possess an Ethereum identity with available Ether. +To call a service on a SingularityNET platform, the user must be able to deposit funds (AGIX tokens) to the +[Multi-Party Escrow](https://dev.singularitynet.io/docs/concepts/multi-party-escrow/) Smart Contract. +To deposit these tokens or do any other transaction on the Ethereum blockchain. -Once you have installed snet-sdk in your current environment, you can import it into your Python script and create an instance of the base sdk class: +Once you have installed snet-sdk in your current environment, you can import it into your Python script and create an +instance of the base sdk class: ```python from snet import sdk config = { @@ -45,7 +49,8 @@ snet_sdk = sdk.SnetSDK(config) ``` The `config` parameter is a Python dictionary. -See [test_sdk_client.py](https://github.com/singnet/snet-sdk-python/blob/master/testcases/functional_tests/test_sdk_client.py) for a reference. +See [test_sdk_client.py](https://github.com/singnet/snet-sdk-python/blob/master/testcases/functional_tests/test_sdk_client.py) +for a reference. ##### Config options description @@ -81,7 +86,8 @@ print(*services_list, sep="\n") ### Calling the service Now, the instance of the sdk can be used to create the service client instances, using `create_service_client()` method. -Continuing from the previous code here is an example using `Exampleservice` from the `26072b8b6a0e448180f8c0e702ab6d2f` organization: +Continuing from the previous code here is an example using `Exampleservice` from the `26072b8b6a0e448180f8c0e702ab6d2f` +organization: ```python service_client = snet_sdk.create_service_client(org_id="26072b8b6a0e448180f8c0e702ab6d2f", @@ -89,11 +95,13 @@ service_client = snet_sdk.create_service_client(org_id="26072b8b6a0e448180f8c0e7 group_name="default_group") ``` -After executing this code, you should have client libraries created for this service. They are located at the following path: `~/.snet/org_id/service_id/python/` +After executing this code, you should have client libraries created for this service. They are located at the following +path: `~/.snet/org_id/service_id/python/` -_Note_: Currently you can only save files to `~/.snet/`. We will fix this in the future. +_Note_: Currently you can only save files to `~/.snet/`. -The instance of service_client that has been generated can be utilized to invoke the methods that the service offers. You can list these using the `get_services_and_messages_info_as_pretty_string()` method: +The instance of service_client that has been generated can be utilized to invoke the methods that the service offers. +You can list these using the `get_services_and_messages_info_as_pretty_string()` method: ```python print(service_client.get_services_and_messages_info_as_pretty_string()) @@ -110,8 +118,10 @@ print(service_client.get_services_and_messages_info_as_pretty_string()) ``` -To invoke the service's methods, you can use the `call_rpc()` method. This method requires the names of the method and data object, along with the data itself, to be passed into it. -To continue with our example, here’s a call to the *mul* method of the *Exampleservice* from the *26072b8b6a0e448180f8c0e702ab6d2f* organization: +To invoke the service's methods, you can use the `call_rpc()` method. This method requires the names of the method and +data object, along with the data itself, to be passed into it. +To continue with our example, here’s a call to the *mul* method of the *Exampleservice* from the +*26072b8b6a0e448180f8c0e702ab6d2f* organization: ```python result = service_client.call_rpc("mul", "Numbers", a=20, b=3) @@ -123,38 +133,6 @@ For more information about gRPC and how to use it with Python, please see: - [gRPC Basics - Python](https://grpc.io/docs/tutorials/basic/python.html) - [gRPC Python’s documentation](https://grpc.io/grpc/python/) -### The complete code of the example - -```python -from snet import sdk -config = { - "private_key": 'YOUR_PRIVATE_WALLET_KEY', - "eth_rpc_endpoint": f"https://sepolia.infura.io/v3/YOUR_INFURA_KEY", - "email": "your@email.com", - "concurrency": False, - "identity_name": "local_name_for_that_identity", - "identity_type": "key", - "network": "sepolia", - "force_update": False - } - -snet_sdk = sdk.SnetSDK(config) - -orgs_list = snet_sdk.get_organization_list() -print(*orgs_list, sep="\n") - -org_id = "26072b8b6a0e448180f8c0e702ab6d2f" -services_list = snet_sdk.get_services_list(org_id=org_id) -print(*services_list, sep="\n") - -service_client = snet_sdk.create_service_client(org_id="26072b8b6a0e448180f8c0e702ab6d2f", - service_id="Exampleservice", - group_name="default_group") - -result = service_client.call_rpc("mul", "Numbers", a=20, b=3) -print(f"Calculating 20 * 3: {result}") -``` - _Note_: In this example, the user doesn't deposit funds to MPE, doesn't open a channel, and doesn't perform other actions related to payment. In this case, the choice of payment strategy, as well as, if necessary, opening a channel and depositing funds into MPE occurs automatically. For more information on payment, please @@ -184,22 +162,33 @@ service_client = snet_sdk.create_service_client(org_id="26072b8b6a0e448180f8c0e7 ### Paid call -`open_channel(amount, expiration)` opens a payment channel with the specified amount of AGIX tokens in cogs -and expiration time. Expiration is payment channel's TTL in blocks. +#### Open channel with the specified amount of funds and expiration + +`open_channel()`[[1]](#1-this-method-uses-a-call-to-a-paid-smart-contract-function) opens a payment channel with the specified amount of AGIX tokens in cogs and expiration time. +Expiration is payment channel's TTL in blocks. When opening a channel, funds are taken from MPE. So they must be +pre-deposited on it. For this, you can use the `deposit_to_escrow_account()`[[1]](#1-this-method-uses-a-call-to-a-paid-smart-contract-function) +method. ```python +snet_sdk.account.deposit_to_escrow_account(123456) service_client.open_channel(amount=123456, expiration=33333) ``` -`deposit_and_open_channel(amount, expiration)` function does the same as the previous one, but first deposits -the specified amount of AGIX tokens in cogs into an MPE. +You can also use the `deposit_and_open_channel()`[[1]](#1-this-method-uses-a-call-to-a-paid-smart-contract-function) +method instead. It does the same as the previous one, but first deposits the specified amount of AGIX tokens in cogs +into an MPE. ```python service_client.deposit_and_open_channel(amount=123456, expiration=33333) ``` +#### Extend expiration and add funds + `open_channel()` as well as `deposit_and_open_channel()` returns the payment channel. You can use it to add funds to it -and extend its expiration. +and extend its expiration using the following methods: +`add_funds()`[[1]](#1-this-method-uses-a-call-to-a-paid-smart-contract-function), +`extend_expiration`[[1]](#1-this-method-uses-a-call-to-a-paid-smart-contract-function) +and `extend_and_add_funds()`[[1]](#1-this-method-uses-a-call-to-a-paid-smart-contract-function). ```python payment_channel = service_client.open_channel(amount=123456, expiration=33333) @@ -212,6 +201,8 @@ payment_channel.extend_and_add_funds(amount=123456, expiration=33333) ## Other useful features +#### Get the current block number + Service client also provides several useful functions. If you need to find out the number of the current block in the blockchain, there is a `get_current_block_number()` method for this: @@ -221,6 +212,8 @@ print(f"Current block is {block_number}") # Current block is 6574322 ``` +#### Get the service call price + To find out the price of calling a service function, you need to use the `get_price()` method: ```python @@ -229,6 +222,8 @@ print(f"The price in cogs for calling the service {service_client.service_id} is # The price in cogs for calling the service Exampleservice is 1 ``` +#### Get the metadata of the service + The metadata of services is stored in IPFS. To view it, you need to call the `get_service_metadata()` method, passing the organization id and the service id to it. @@ -255,6 +250,8 @@ print(*service_metadata.get_all_endpoints_for_group(group_name="default_group"), # http://node1.naint.tech:62400 ``` +#### Get raw services and messages info + In the section [Calling the service](#calling-the-service) we already talked about the function `get_services_and_messages_info_as_pretty_string()`, with which you can get information about the methods and messages of a service. But if you need to process lists of services and messages, it is better to use the @@ -262,27 +259,16 @@ messages of a service. But if you need to process lists of services and messages ```python services, messages = service_client.get_services_and_messages_info() +print(services) +print(messages) -for service in services.items(): - print(service[0], *service[1], sep="\n") - -print() +# {'Calculator': [('add', 'Numbers', 'Result'), ('sub', 'Numbers', 'Result'), ('mul', 'Numbers', 'Result'), ('div', 'Numbers', 'Result')]} +# {'Numbers': [('float', 'a'), ('float', 'b')], 'Result': [('float', 'value')]} +``` -for message in messages.items(): - print(message[0], *message[1], sep="\n") +--- -# Calculator -# ('add', 'Numbers', 'Result') -# ('sub', 'Numbers', 'Result') -# ('mul', 'Numbers', 'Result') -# ('div', 'Numbers', 'Result') -# -# Numbers -# ('float', 'a') -# ('float', 'b') -# Result -# ('float', 'value') -``` +###### 1 This method uses a call to a paid smart contract function. --- From 7b2d5cab50561ad4c891a881100b5d6422dd5615 Mon Sep 17 00:00:00 2001 From: Dronny Date: Tue, 27 Aug 2024 18:55:37 +0300 Subject: [PATCH 34/55] Added two examples. calculator.py is finished, console_app.py is almost finished --- examples/calculator.py | 61 ++++++++++++++++ examples/console_app.py | 149 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 210 insertions(+) create mode 100644 examples/calculator.py create mode 100644 examples/console_app.py diff --git a/examples/calculator.py b/examples/calculator.py new file mode 100644 index 0000000..2d6c3b2 --- /dev/null +++ b/examples/calculator.py @@ -0,0 +1,61 @@ +from snet.sdk import SnetSDK + +config = { + "private_key": '668cfafb7d20829e9db5e6970247cf06edb0e7787f7ea8ae8f8320c2995b88d9', + "eth_rpc_endpoint": f"https://sepolia.infura.io/v3/87335b880b2645ebb9e1a433e05c0ed9", + "concurrency": False, + "identity_name": "andrey", + "identity_type": "key", + "network": "sepolia", + "force_update": False +} + +operators = { + "+": "add", + "-": "sub", + "*": "mul", + "/": "div" +} + +snet_sdk = SnetSDK(config) +calc_client = snet_sdk.create_service_client(org_id="26072b8b6a0e448180f8c0e702ab6d2f", + service_id="Exampleservice", group_name="default_group") + + +def parse_expression(expression): + elements = list(expression.split()) + if len(elements) != 3: + raise Exception(f"Invalid expression '{expression}'. Three items required.") + + a = int(elements[0]) + b = int(elements[2]) + if elements[1] not in ["+", "-", "*", "/"]: + raise Exception(f"Invalid expression '{expression}'. Operation must be '+' or '-' or '*' or '/'") + elif not isinstance(a, (float, int)) or not isinstance(b, (float, int)): + raise Exception(f"Invalid expression '{expression}'. Operands must be integers or floating point numbers.") + op = elements[1] + + return a, b, op + + +def main(): + print(""" +Welcome to the calculator powered by SingularityNET platform! +Please type the expression you want to calculate, e.g. 2 + 3. +Type 'exit' to exit the program.""") + while True: + expression = input("Calculator> ") + if expression == "exit": + break + try: + a, b, op = parse_expression(expression) + print(f"Calculating {a} {op} {b}...") + result = calc_client.call_rpc(operators[op], "Numbers", a=a, b=b) + print(f"{a} {op} {b} = {result}") + except Exception as e: + print(e) + + +if __name__ == "__main__": + main() + diff --git a/examples/console_app.py b/examples/console_app.py new file mode 100644 index 0000000..b00c19e --- /dev/null +++ b/examples/console_app.py @@ -0,0 +1,149 @@ +from snet.sdk import SnetSDK +from snet.sdk.service_client import ServiceClient + + +def initialize_by_default(): + org_id = "26072b8b6a0e448180f8c0e702ab6d2f" + service_id = "Exampleservice" + group_name = "default_group" + service = snet_sdk.create_service_client(org_id=org_id, service_id=service_id, group_name=group_name) + services[(service.org_id, service.service_id)] = service + global active_service + active_service = service + + +def get_group_name(service_client): + service_client + + +def list_organizations(): + print("Organizations:") + print(*map(lambda x: '\t' + x, snet_sdk.get_organization_list()), sep="\n") + + +def list_services_for_org(): + org_id = input("Enter organization id: ") + print("Services:") + print(*map(lambda x: '\t' + x, snet_sdk.get_services_list(org_id=org_id)), sep="\n") + + +def create_service_client(): + org_id = input("Enter organization id: ") + service_id = input("Enter service id: ") + group_name = input("Enter payment group name: ") + service = snet_sdk.create_service_client(org_id=org_id, service_id=service_id, group_name=group_name) + services[(service.org_id, service.service_id)] = service + + +def commands_help(): + print("Available commands:") + print(*map(lambda x: '\t' + x, commands.keys()), sep="\n") + + +def change_config(): + pass + + +def list_initialized_services(): + print("Initialized services:") + print("Organization_id Service_id Group_name") + print(*map(lambda service: f"{service.org_id} {service.service_id} {service.group['group_name']}", services.values()), sep="\n") + + +def switch_service(): + list_initialized_services() + org_id, service_id = input("Enter organization id and service id (by space): ").split() + if (org_id, service_id) in services.keys(): + global active_service + active_service = services[(org_id, service_id)] + else: + print(f"Service with {org_id} organization id and {service_id} service id is not initialized") + + +def call(): + if active_service is None: + print("No active service. Please enter 'switch-service' to switch service") + return None + + method_name = input("Enter method name: ") + + servs, msgs = active_service.get_services_and_messages_info() + is_found = False + for service in servs.items(): + for method in service[1]: + print(method[0], method_name) + if method[0] == method_name: + input_type = method[1] + output_type = method[2] + is_found = True + break + if is_found: + break + + if not is_found: + print(f"Method '{method_name}' is not found in service") + return None + + inputs = {var[1]: float(input(f"{var[1]}: ")) for var in msgs[input_type]} + + print("Service calling...") + + result = active_service.call_rpc(method_name, input_type, **inputs) + outputs = {var[1]: getattr(result, var[1]) for var in msgs[output_type]} + + print("Result:", *map(lambda x: f"{x[0]}: {x[1]}", outputs.items()), sep="\n") + + +def print_service_info(): + if active_service is None: + print("No active service") + return None + + print(active_service.get_services_and_messages_info_as_pretty_string()) + + +config = { + "private_key": '668cfafb7d20829e9db5e6970247cf06edb0e7787f7ea8ae8f8320c2995b88d9', + "eth_rpc_endpoint": f"https://sepolia.infura.io/v3/87335b880b2645ebb9e1a433e05c0ed9", + "concurrency": False, + "identity_name": "andrey", + "identity_type": "key", + "network": "sepolia", + "force_update": False +} + +snet_sdk = SnetSDK(config) +services = {} +active_service: ServiceClient = None +initialize_by_default() + +commands = { + "list-orgs": list_organizations, + "list-services-for-org": list_services_for_org, + "add-service": create_service_client, + "help": commands_help, + "exit": lambda: exit(0), + "change-config": change_config, + "list-initialized-services": list_initialized_services, + "switch-service": switch_service, + "call": call, + "print-service-info": print_service_info, + +} + + +def main(): + print(""" +Hello, welcome to the Snet SDK console application! +To use the application, type the name of the command you want to execute. +To print the list of available commands, type 'help'.""") + while True: + command = input(">>> ") + if command in commands: + commands[command]() + else: + print(f"Command '{command}' is not found. Please try again.") + + +if __name__ == "__main__": + main() From 1e0b5ccbdec3087519b875a8f2f2f039d7c5ac16 Mon Sep 17 00:00:00 2001 From: Dronny Date: Tue, 27 Aug 2024 19:02:03 +0300 Subject: [PATCH 35/55] Tiny fixes --- examples/calculator.py | 6 +++--- examples/console_app.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/calculator.py b/examples/calculator.py index 2d6c3b2..f17bfcf 100644 --- a/examples/calculator.py +++ b/examples/calculator.py @@ -1,10 +1,10 @@ from snet.sdk import SnetSDK config = { - "private_key": '668cfafb7d20829e9db5e6970247cf06edb0e7787f7ea8ae8f8320c2995b88d9', - "eth_rpc_endpoint": f"https://sepolia.infura.io/v3/87335b880b2645ebb9e1a433e05c0ed9", + "private_key": 'APP_PROVIDER_PRIVATE_KEY', + "eth_rpc_endpoint": f"https://sepolia.infura.io/v3/INFURE_API_KEY", "concurrency": False, - "identity_name": "andrey", + "identity_name": "NAME", "identity_type": "key", "network": "sepolia", "force_update": False diff --git a/examples/console_app.py b/examples/console_app.py index b00c19e..6e9aee6 100644 --- a/examples/console_app.py +++ b/examples/console_app.py @@ -103,10 +103,10 @@ def print_service_info(): config = { - "private_key": '668cfafb7d20829e9db5e6970247cf06edb0e7787f7ea8ae8f8320c2995b88d9', - "eth_rpc_endpoint": f"https://sepolia.infura.io/v3/87335b880b2645ebb9e1a433e05c0ed9", + "private_key": 'APP_PROVIDER_PRIVATE_KEY', + "eth_rpc_endpoint": f"https://sepolia.infura.io/v3/INFURE_API_KEY", "concurrency": False, - "identity_name": "andrey", + "identity_name": "NAME", "identity_type": "key", "network": "sepolia", "force_update": False From 7a7df143b415a4b750aa3877f95ed7adddac1180 Mon Sep 17 00:00:00 2001 From: Dronny Date: Thu, 29 Aug 2024 12:39:11 +0300 Subject: [PATCH 36/55] Reworked commands. Added several command implementations. --- examples/console_app.py | 197 ++++++++++++++++++++++++++++++++-------- 1 file changed, 158 insertions(+), 39 deletions(-) diff --git a/examples/console_app.py b/examples/console_app.py index 6e9aee6..c983832 100644 --- a/examples/console_app.py +++ b/examples/console_app.py @@ -7,15 +7,11 @@ def initialize_by_default(): service_id = "Exampleservice" group_name = "default_group" service = snet_sdk.create_service_client(org_id=org_id, service_id=service_id, group_name=group_name) - services[(service.org_id, service.service_id)] = service + initialized_services.append(service) global active_service active_service = service -def get_group_name(service_client): - service_client - - def list_organizations(): print("Organizations:") print(*map(lambda x: '\t' + x, snet_sdk.get_organization_list()), sep="\n") @@ -31,13 +27,29 @@ def create_service_client(): org_id = input("Enter organization id: ") service_id = input("Enter service id: ") group_name = input("Enter payment group name: ") - service = snet_sdk.create_service_client(org_id=org_id, service_id=service_id, group_name=group_name) - services[(service.org_id, service.service_id)] = service + + have_free_calls = input("Use free calls? (y/n): ") == 'y' + free_call_token = None + free_call_token_expiry_block = None + if have_free_calls: + free_call_token = input("Enter free call auth token: ") + free_call_token_expiry_block = input("Enter free call token expiry block: ") + + service = snet_sdk.create_service_client(org_id=org_id, service_id=service_id, group_name=group_name, + free_call_auth_token_bin=free_call_token, + free_call_token_expiry_block=free_call_token_expiry_block) + initialized_services.append(service) + + global active_service + if active_service is None: + active_service = service def commands_help(): + global active_commands print("Available commands:") - print(*map(lambda x: '\t' + x, commands.keys()), sep="\n") + for command in active_commands.items(): + print(f'\t{command[0]} - {command[1][1]}') def change_config(): @@ -45,31 +57,34 @@ def change_config(): def list_initialized_services(): - print("Initialized services:") - print("Organization_id Service_id Group_name") - print(*map(lambda service: f"{service.org_id} {service.service_id} {service.group['group_name']}", services.values()), sep="\n") + print("INDEX ORGANIZATION_ID SERVICE_ID GROUP_NAME") + for index, service in enumerate(initialized_services): + print(f"{index} {service.org_id} {service.service_id} {service.group['group_name']}") def switch_service(): list_initialized_services() - org_id, service_id = input("Enter organization id and service id (by space): ").split() - if (org_id, service_id) in services.keys(): + index = int(input("Enter the index of one of the initialized services: ")) + if index < len(initialized_services): global active_service - active_service = services[(org_id, service_id)] + active_service = initialized_services[index] + print(f"Switched to service with index {index}") else: - print(f"Service with {org_id} organization id and {service_id} service id is not initialized") + print(f"Service with index {index} is not initialized!") def call(): + global active_service if active_service is None: - print("No active service. Please enter 'switch-service' to switch service") + print("No initialized services!\n" + "Please enter 'service' to go to the service menu and then enter 'add' to add a service.") return None method_name = input("Enter method name: ") - servs, msgs = active_service.get_services_and_messages_info() + services, msgs = active_service.get_services_and_messages_info() is_found = False - for service in servs.items(): + for service in services.items(): for method in service[1]: print(method[0], method_name) if method[0] == method_name: @@ -95,16 +110,81 @@ def call(): def print_service_info(): + global active_service if active_service is None: - print("No active service") + print("No initialized services!\n" + "Please enter 'service' to go to the service menu and then enter 'add' to add a service.") return None - print(active_service.get_services_and_messages_info_as_pretty_string()) +def deposit(): + amount = int(input("Enter amount of AGIX tokens in cogs to deposit into an MPE account: ")) + snet_sdk.account.deposit_to_escrow_account(amount) + + +def open_channel(): + global active_service + additions = False + if active_service is None: + additions = input("No initialized services! This entails entering additional parameters. Continue? (y/n): ") == 'y' + if not additions: + return None + else: + is_continue = input("The new channel will be opened for the active service. Continue? (y/n): ") == 'y' + if not is_continue: + return None + + amount = int(input("Enter amount of AGIX tokens in cogs to put into the channel: ")) + + balance = snet_sdk.account.escrow_balance() + is_deposit = False + if balance < amount: + print(f"Insufficient balance!\n\tCurrent MPE balance: {balance}\n\tAmount to put: {amount}") + is_deposit = input("Would you like to deposit needed amount of AGIX tokens in advance? (y/n): ") == 'y' + if not is_deposit: + print("Channel is not opened!") + return None + + expiration = int(input("Enter expiration time in blocks: ")) + + if additions: + payment_address = input("Enter payment address: ") + group_id = input("Enter payment group id: ") + if is_deposit: + snet_sdk.mpe_contract.deposit_and_open_channel(account=snet_sdk.account, + payment_address=payment_address, + group_id=group_id, + amount=amount, + expiration=expiration) + else: + snet_sdk.mpe_contract.open_channel(account=snet_sdk.account, + payment_address=payment_address, + group_id=group_id, + amount=amount, + expiration=expiration) + else: + if is_deposit: + active_service.open_channel(amount=amount, expiration=expiration) + else: + active_service.deposit_and_open_channel(amount=amount, expiration=expiration) + + +def list_channels(): + pass + + +def add_funds(): + pass + + +def extend_expiration(): + pass + + config = { "private_key": 'APP_PROVIDER_PRIVATE_KEY', - "eth_rpc_endpoint": f"https://sepolia.infura.io/v3/INFURE_API_KEY", + "eth_rpc_endpoint": f"https://sepolia.infura.io/v3/APP_PROVIDER_INFURA_KEY", "concurrency": False, "identity_name": "NAME", "identity_type": "key", @@ -113,36 +193,75 @@ def print_service_info(): } snet_sdk = SnetSDK(config) -services = {} -active_service: ServiceClient = None +initialized_services = [] +active_service: ServiceClient +channels = {} initialize_by_default() commands = { - "list-orgs": list_organizations, - "list-services-for-org": list_services_for_org, - "add-service": create_service_client, - "help": commands_help, - "exit": lambda: exit(0), - "change-config": change_config, - "list-initialized-services": list_initialized_services, - "switch-service": switch_service, - "call": call, - "print-service-info": print_service_info, - + "main": { + "organizations": (list_organizations, "print a list of organization ids from Registry"), + "services": (list_services_for_org, "print a list of service ids for an organization from Registry"), + # "change-config": change_config, + "deposit": (deposit, "deposit AGIX tokens into MPE"), + "service": (lambda: None, "go to the services menu"), + "channel": (lambda: None, "go to the channels menu"), + "help": (commands_help, "print a list of available commands in the main menu"), + "exit": (lambda: exit(0), "exit the application") + }, + + "service": { + "add": (create_service_client, + "create a new service client. If it the first time, the new service becomes active"), + "use": (switch_service, "switch the active service"), + "call": (call, "call the active service method"), + "info": (print_service_info, "output services, methods and messages in a service"), + "list": (list_initialized_services, "print a list of initialized services"), + "help": (commands_help, "print a list of available commands in the services menu"), + "back": (lambda: None, "return to the main menu"), + "exit": (lambda: exit(0), "exit the application") + }, + + "channel": { + "list": (list_channels, "print a list of initialized payment channels"), + "open": (open_channel, "open a new payment channel"), + "add-funds": (add_funds, "add funds to a channel"), + "extend-expiration": (extend_expiration, "extend expiration of a channel"), + "help": (commands_help, "print a list of available commands in the channels menu"), + "back": (lambda: None, "return to the main menu"), + "exit": (lambda: exit(0), "exit the application") + } } +active_commands: dict = commands["main"] + def main(): + global active_commands + print(""" Hello, welcome to the Snet SDK console application! -To use the application, type the name of the command you want to execute. -To print the list of available commands, type 'help'.""") +To use the application, type the name of the command you want to execute.""") + commands_help() + print("To print a list of available commands, type 'help'") + + prefix = ">>> " while True: - command = input(">>> ") - if command in commands: - commands[command]() + command = input(prefix) + if command in active_commands: + active_commands[command][0]() else: print(f"Command '{command}' is not found. Please try again.") + continue + + if command in ["back", "service", "channel"]: + if command == "back": + command = "main" + prefix = ">>> " + else: + prefix = command + " >>> " + active_commands = commands[command] + commands_help() if __name__ == "__main__": From a9a8e317849a21eafcc1812425b73bda0109bee0 Mon Sep 17 00:00:00 2001 From: Dronny Date: Thu, 29 Aug 2024 14:01:36 +0300 Subject: [PATCH 37/55] Finished console_app.py. Implemented updating channels. --- examples/console_app.py | 143 +++++++++++++++++++++++++--------------- 1 file changed, 90 insertions(+), 53 deletions(-) diff --git a/examples/console_app.py b/examples/console_app.py index c983832..1cb1c6a 100644 --- a/examples/console_app.py +++ b/examples/console_app.py @@ -1,39 +1,28 @@ from snet.sdk import SnetSDK from snet.sdk.service_client import ServiceClient - -def initialize_by_default(): - org_id = "26072b8b6a0e448180f8c0e702ab6d2f" - service_id = "Exampleservice" - group_name = "default_group" - service = snet_sdk.create_service_client(org_id=org_id, service_id=service_id, group_name=group_name) - initialized_services.append(service) - global active_service - active_service = service - - def list_organizations(): print("Organizations:") print(*map(lambda x: '\t' + x, snet_sdk.get_organization_list()), sep="\n") def list_services_for_org(): - org_id = input("Enter organization id: ") + org_id = input("Enter organization id: ").strip() print("Services:") print(*map(lambda x: '\t' + x, snet_sdk.get_services_list(org_id=org_id)), sep="\n") def create_service_client(): - org_id = input("Enter organization id: ") - service_id = input("Enter service id: ") - group_name = input("Enter payment group name: ") + org_id = input("Enter organization id: ").strip() + service_id = input("Enter service id: ").strip() + group_name = input("Enter payment group name: ").strip() - have_free_calls = input("Use free calls? (y/n): ") == 'y' + have_free_calls = input("Use free calls? (y/n): ").strip() == 'y' free_call_token = None free_call_token_expiry_block = None if have_free_calls: - free_call_token = input("Enter free call auth token: ") - free_call_token_expiry_block = input("Enter free call token expiry block: ") + free_call_token = input("Enter free call auth token: ").strip() + free_call_token_expiry_block = input("Enter free call token expiry block: ").strip() service = snet_sdk.create_service_client(org_id=org_id, service_id=service_id, group_name=group_name, free_call_auth_token_bin=free_call_token, @@ -64,7 +53,7 @@ def list_initialized_services(): def switch_service(): list_initialized_services() - index = int(input("Enter the index of one of the initialized services: ")) + index = int(input("Enter the index of one of the initialized services: ").strip()) if index < len(initialized_services): global active_service active_service = initialized_services[index] @@ -123,68 +112,115 @@ def deposit(): snet_sdk.account.deposit_to_escrow_account(amount) +def block_number(): + if active_service is None: + print("No initialized services!\n" + "Please enter 'service' to go to the service menu and then enter 'add' to add a service.") + return None + print("Current block number: ", active_service.get_current_block_number()) + + +def update_channels(): + if active_service is None: + print("No initialized services!\n" + "Please enter 'service' to go to the service menu and then enter 'add' to add a service.") + return None + + is_continue = input("""Updating the channel list makes sense if the channel data has changed through other entry points. +This procedure may take several minutes. +Continue? (y/n): """).strip() == 'y' + if not is_continue: + return None + + print("Updating the channel list...") + global channels + channels.clear() + + for service in initialized_services: + load_channels = service.load_open_channels() + for channel in load_channels: + channels.append((channel, service.org_id, service.service_id, service.group['group_name'])) + + print("Channels updated! Enter 'list' to print the updated list.") + + def open_channel(): global active_service + global channels additions = False if active_service is None: - additions = input("No initialized services! This entails entering additional parameters. Continue? (y/n): ") == 'y' - if not additions: - return None + print("No initialized services! The channel can only be opened for the service!\n" + "Please enter 'service' to go to the service menu and then enter 'add' to add a service.") + return None else: - is_continue = input("The new channel will be opened for the active service. Continue? (y/n): ") == 'y' + is_continue = input("The new channel will be opened for the active service. Continue? (y/n): ").strip() == 'y' if not is_continue: return None - amount = int(input("Enter amount of AGIX tokens in cogs to put into the channel: ")) + amount = int(input("Enter amount of AGIX tokens in cogs to put into the channel: ").strip()) balance = snet_sdk.account.escrow_balance() is_deposit = False if balance < amount: print(f"Insufficient balance!\n\tCurrent MPE balance: {balance}\n\tAmount to put: {amount}") - is_deposit = input("Would you like to deposit needed amount of AGIX tokens in advance? (y/n): ") == 'y' + is_deposit = input("Would you like to deposit needed amount of AGIX tokens in advance? (y/n): ").strip() == 'y' if not is_deposit: print("Channel is not opened!") return None - expiration = int(input("Enter expiration time in blocks: ")) - - if additions: - payment_address = input("Enter payment address: ") - group_id = input("Enter payment group id: ") - if is_deposit: - snet_sdk.mpe_contract.deposit_and_open_channel(account=snet_sdk.account, - payment_address=payment_address, - group_id=group_id, - amount=amount, - expiration=expiration) - else: - snet_sdk.mpe_contract.open_channel(account=snet_sdk.account, - payment_address=payment_address, - group_id=group_id, - amount=amount, - expiration=expiration) + expiration = int(input("Enter expiration time in blocks: ").strip()) + + if is_deposit: + channel = active_service.open_channel(amount=amount, expiration=expiration) else: - if is_deposit: - active_service.open_channel(amount=amount, expiration=expiration) - else: - active_service.deposit_and_open_channel(amount=amount, expiration=expiration) + channel = active_service.deposit_and_open_channel(amount=amount, expiration=expiration) + channels.append((channel, active_service.org_id, active_service.service_id, active_service.group['group_name'])) def list_channels(): - pass + global channels + print("ORGANIZATION_ID SERVICE_ID GROUP_NAME CHANNEL_ID AMOUNT EXPIRATION") + for channel in channels: + channel[0].sync_state() + print(channel[1], + channel[2], + channel[3], + channel[0].channel_id, + channel[0].state['available_amount'], + channel[0].state['expiration']) def add_funds(): - pass + channel_id = int(input("Enter channel id: ").strip()) + exists = False + for channel in channels: + if channel[0].channel_id == channel_id: + amount = int(input("Enter amount of AGIX tokens in cogs to add to the channel: ").strip()) + channel[0].add_funds(amount) + exists = True + if not exists: + print(f"Channel with id {channel_id} is not found!") def extend_expiration(): - pass + channel_id = int(input("Enter channel id: ").strip()) + exists = False + for channel in channels: + if channel[0].channel_id == channel_id: + expiration = int(input("Enter new expiration time in blocks: ").strip()) + current_block = active_service.get_current_block_number() + if expiration < current_block: + print(f"Expiration time can't be less than current block ({current_block})!") + return None + channel[0].extend_expiration(expiration) + exists = True + if not exists: + print(f"Channel with id {channel_id} is not found!") config = { "private_key": 'APP_PROVIDER_PRIVATE_KEY', - "eth_rpc_endpoint": f"https://sepolia.infura.io/v3/APP_PROVIDER_INFURA_KEY", + "eth_rpc_endpoint": f"https://sepolia.infura.io/v3/APP_PROVIDER_INFURA_API_KEY", "concurrency": False, "identity_name": "NAME", "identity_type": "key", @@ -195,8 +231,7 @@ def extend_expiration(): snet_sdk = SnetSDK(config) initialized_services = [] active_service: ServiceClient -channels = {} -initialize_by_default() +channels = [] commands = { "main": { @@ -204,6 +239,7 @@ def extend_expiration(): "services": (list_services_for_org, "print a list of service ids for an organization from Registry"), # "change-config": change_config, "deposit": (deposit, "deposit AGIX tokens into MPE"), + "block": (block_number, "print the current block number"), "service": (lambda: None, "go to the services menu"), "channel": (lambda: None, "go to the channels menu"), "help": (commands_help, "print a list of available commands in the main menu"), @@ -223,6 +259,7 @@ def extend_expiration(): }, "channel": { + "update": (update_channels, "update a list of initialized payment channels"), "list": (list_channels, "print a list of initialized payment channels"), "open": (open_channel, "open a new payment channel"), "add-funds": (add_funds, "add funds to a channel"), @@ -247,7 +284,7 @@ def main(): prefix = ">>> " while True: - command = input(prefix) + command = input(prefix).strip() if command in active_commands: active_commands[command][0]() else: From 32f76ddacb1d8382e2421ef6c66ba793f027e6a0 Mon Sep 17 00:00:00 2001 From: Dronny Date: Thu, 29 Aug 2024 18:52:18 +0300 Subject: [PATCH 38/55] Added description (comments) to console_app.py. Added "balance" function. --- examples/console_app.py | 140 +++++++++++++++++++++++++++++++++------- 1 file changed, 117 insertions(+), 23 deletions(-) diff --git a/examples/console_app.py b/examples/console_app.py index 1cb1c6a..b58d881 100644 --- a/examples/console_app.py +++ b/examples/console_app.py @@ -1,32 +1,48 @@ +""" +An example of how to use the SnetSDK to create a console application. +All the functionality of this application is based on the SnetSDK. + +It is assumed that there is an application provider (developer), who pays for all the +transactions and service calls. So, to run the application, you will need to change the values in 'config'. +""" + + from snet.sdk import SnetSDK from snet.sdk.service_client import ServiceClient + def list_organizations(): + """ + The function, which is called when the user enters the command 'organizations' in the main menu. + Prints the list of organization IDs related to the network specified in 'config'. + The list is got from the MPE contract using 'get_organization_list'. + """ print("Organizations:") print(*map(lambda x: '\t' + x, snet_sdk.get_organization_list()), sep="\n") def list_services_for_org(): + """ + The function, which is called when the user enters the command 'services' in the main menu. + Prints the list of services IDs, related to the organization specified by the user. + The list is got from the MPE contract using 'get_organization_list'. + """ org_id = input("Enter organization id: ").strip() print("Services:") print(*map(lambda x: '\t' + x, snet_sdk.get_services_list(org_id=org_id)), sep="\n") def create_service_client(): + """ + The function, which is called when the user enters the command 'add' in the service menu. + Creates a service client, related to the service specified by the user, and adds it to the + list of initialized services. The creation occurs using 'create_service_client'. + """ org_id = input("Enter organization id: ").strip() service_id = input("Enter service id: ").strip() group_name = input("Enter payment group name: ").strip() - have_free_calls = input("Use free calls? (y/n): ").strip() == 'y' - free_call_token = None - free_call_token_expiry_block = None - if have_free_calls: - free_call_token = input("Enter free call auth token: ").strip() - free_call_token_expiry_block = input("Enter free call token expiry block: ").strip() - - service = snet_sdk.create_service_client(org_id=org_id, service_id=service_id, group_name=group_name, - free_call_auth_token_bin=free_call_token, - free_call_token_expiry_block=free_call_token_expiry_block) + service = snet_sdk.create_service_client(org_id=org_id, service_id=service_id, group_name=group_name) initialized_services.append(service) global active_service @@ -35,23 +51,31 @@ def create_service_client(): def commands_help(): + """ + The function, which is called when the user enters the command 'help' in any menu. + Prints the list of available commands with descriptions depending on the active menu. + """ global active_commands print("Available commands:") for command in active_commands.items(): print(f'\t{command[0]} - {command[1][1]}') -def change_config(): - pass - - def list_initialized_services(): + """ + The function, which is called when the user enters the command 'list' in the service menu. + Prints the list of initialized services including their organization ID, service ID and group name. + """ print("INDEX ORGANIZATION_ID SERVICE_ID GROUP_NAME") for index, service in enumerate(initialized_services): print(f"{index} {service.org_id} {service.service_id} {service.group['group_name']}") def switch_service(): + """ + The function, which is called when the user enters the command 'use' in the service menu. + Changes the active service to the one whose index the user specified. + """ list_initialized_services() index = int(input("Enter the index of one of the initialized services: ").strip()) if index < len(initialized_services): @@ -63,6 +87,12 @@ def switch_service(): def call(): + """ + The function, which is called when the user enters the command 'call' in the service menu. + Calls the method specified by the user of the active service. It gets data about the service using the + 'get_services_and_messages_info' and parses the resulting dict to display the correct names of the input + and output values to the user. + """ global active_service if active_service is None: print("No initialized services!\n" @@ -71,7 +101,7 @@ def call(): method_name = input("Enter method name: ") - services, msgs = active_service.get_services_and_messages_info() + services, messages = active_service.get_services_and_messages_info() is_found = False for service in services.items(): for method in service[1]: @@ -88,17 +118,22 @@ def call(): print(f"Method '{method_name}' is not found in service") return None - inputs = {var[1]: float(input(f"{var[1]}: ")) for var in msgs[input_type]} + inputs = {var[1]: float(input(f"{var[1]}: ")) for var in messages[input_type]} print("Service calling...") result = active_service.call_rpc(method_name, input_type, **inputs) - outputs = {var[1]: getattr(result, var[1]) for var in msgs[output_type]} + outputs = {var[1]: getattr(result, var[1]) for var in messages[output_type]} print("Result:", *map(lambda x: f"{x[0]}: {x[1]}", outputs.items()), sep="\n") def print_service_info(): + """ + The function, which is called when the user enters the command 'info' in the service menu. + Prints data (services, methods and services) of the active service. It gets data about the service using + 'get_services_and_messages_info_as_pretty_string'. + """ global active_service if active_service is None: print("No initialized services!\n" @@ -107,12 +142,32 @@ def print_service_info(): print(active_service.get_services_and_messages_info_as_pretty_string()) +def balance(): + """ + The function, which is called when the user enters the command 'balance' in the main menu. + Prints the balances of AGIX and MPE. It gets the balances using 'balance_of' and 'escrow_balance'. + """ + account_balance = snet_sdk.account.token_contract.functions.balanceOf(snet_sdk.account.address).call() + escrow_balance = snet_sdk.account.escrow_balance() + + print(f"AGIX balance: {account_balance}") + print(f"MPE balance: {escrow_balance}") + + def deposit(): + """ + The function, which is called when the user enters the command 'deposit' in the main menu. + Deposits the user-specified amount of AGIX tokens in cogs into MPE contract using 'deposit_to_escrow_account'. + """ amount = int(input("Enter amount of AGIX tokens in cogs to deposit into an MPE account: ")) snet_sdk.account.deposit_to_escrow_account(amount) def block_number(): + """ + The function, which is called when the user enters the command 'block' in the main menu. + Prints the current block number. It gets the block number using 'get_current_block_number'. + """ if active_service is None: print("No initialized services!\n" "Please enter 'service' to go to the service menu and then enter 'add' to add a service.") @@ -121,6 +176,13 @@ def block_number(): def update_channels(): + """ + The function, which is called when the user enters the command 'update' in the channel menu. + Updates the list of open channels stored in 'channels'. Gets the list of open channels using + 'load_open_channels' for each initialized service. + The specified method searches for channels through the blockchain, which is why it takes quite a long time + to work, so there is a warning for the user about this at the beginning. + """ if active_service is None: print("No initialized services!\n" "Please enter 'service' to go to the service menu and then enter 'add' to add a service.") @@ -145,6 +207,12 @@ def update_channels(): def open_channel(): + """ + The function, which is called when the user enters the command 'open' in the channel menu. + Opens a new channel for the active service. Checks the balance of the MPE contract and asks the user + if they want to deposit AGIX tokens into it if there isn't enough funds. Opens the channel using 'open_channel' + or 'deposit_and_open_channel' with the user-specified amount of AGIX tokens in cogs and expiration time. + """ global active_service global channels additions = False @@ -178,6 +246,10 @@ def open_channel(): def list_channels(): + """ + The function, which is called when the user enters the command 'list' in the channel menu. + Prints the list of open channels stored in 'channels'. + """ global channels print("ORGANIZATION_ID SERVICE_ID GROUP_NAME CHANNEL_ID AMOUNT EXPIRATION") for channel in channels: @@ -191,6 +263,11 @@ def list_channels(): def add_funds(): + """ + The function, which is called when the user enters the command 'add-funds' in the channel menu. + Adds funds to the channel. Finds the channel by its id specified by the user and adds the user-specified amount + of AGIX tokens in cogs to it using 'add_funds'. + """ channel_id = int(input("Enter channel id: ").strip()) exists = False for channel in channels: @@ -203,6 +280,12 @@ def add_funds(): def extend_expiration(): + """ + The function, which is called when the user enters the command 'extend-expiration' in the channel menu. + Extends expiration time of the channel. Finds the channel by its id specified by the user and Extends its + expiration time to a user-specified block using 'extend_expiration', after comparing it with the current + block number. + """ channel_id = int(input("Enter channel id: ").strip()) exists = False for channel in channels: @@ -218,6 +301,10 @@ def extend_expiration(): print(f"Channel with id {channel_id} is not found!") +""" +SDK configuration that is configured by the application provider. +To run the application you need to change the 'private_key', 'eth_rpc_endpoint' and 'identity_name' values. +""" config = { "private_key": 'APP_PROVIDER_PRIVATE_KEY', "eth_rpc_endpoint": f"https://sepolia.infura.io/v3/APP_PROVIDER_INFURA_API_KEY", @@ -228,16 +315,19 @@ def extend_expiration(): "force_update": False } -snet_sdk = SnetSDK(config) -initialized_services = [] -active_service: ServiceClient -channels = [] +snet_sdk = SnetSDK(config) # the 'SnetSDK' instance +initialized_services = [] # the list of initialized services +active_service: ServiceClient # the currently active service +channels = [] # the list of open channels +""" +Commands available in the application with their descriptions and functions to call. +""" commands = { "main": { "organizations": (list_organizations, "print a list of organization ids from Registry"), "services": (list_services_for_org, "print a list of service ids for an organization from Registry"), - # "change-config": change_config, + "balance": (balance, "print the account balance and the escrow balance"), "deposit": (deposit, "deposit AGIX tokens into MPE"), "block": (block_number, "print the current block number"), "service": (lambda: None, "go to the services menu"), @@ -270,10 +360,14 @@ def extend_expiration(): } } -active_commands: dict = commands["main"] +active_commands: dict = commands["main"] # the list of available commands in the active menu def main(): + """ + The function, which is called when the application is started. + Manages global variables and calls the appropriate functions. + """ global active_commands print(""" From e6da5d907f28c47369fa5887a6cfe6b50f1dd9b5 Mon Sep 17 00:00:00 2001 From: Arondondon Date: Thu, 5 Sep 2024 13:24:17 +0300 Subject: [PATCH 39/55] Added calculator.py file with the guide and description. --- examples/calculator.py | 4 +- examples/examples_docs/calculator.md | 139 +++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 examples/examples_docs/calculator.md diff --git a/examples/calculator.py b/examples/calculator.py index f17bfcf..18d0924 100644 --- a/examples/calculator.py +++ b/examples/calculator.py @@ -1,4 +1,4 @@ -from snet.sdk import SnetSDK +from snet import sdk config = { "private_key": 'APP_PROVIDER_PRIVATE_KEY', @@ -17,7 +17,7 @@ "/": "div" } -snet_sdk = SnetSDK(config) +snet_sdk = sdk.SnetSDK(config) calc_client = snet_sdk.create_service_client(org_id="26072b8b6a0e448180f8c0e702ab6d2f", service_id="Exampleservice", group_name="default_group") diff --git a/examples/examples_docs/calculator.md b/examples/examples_docs/calculator.md new file mode 100644 index 0000000..e529a9d --- /dev/null +++ b/examples/examples_docs/calculator.md @@ -0,0 +1,139 @@ +## Tutorial on developing a console calculator + +This is an example of how to use the SnetSDK to create a console сфдсгдфещк that works using a service +on the SingularityNET platform. + +### Description + +It is assumed that there is an application provider (developer), who pays for all the +transactions and service calls. + +So the application must have the next console interface: + +```commandline +Welcome to the calculator powered by SingularityNET platform! +Please type the expression you want to calculate, e.g. 2 + 3. +Type 'exit' to exit the program. +Calculator> 34 * 4 +Calculating 34 * 4... +34 * 4 = 134 +Calculator> 103 - 82 +Calculating 103 - 82... +103 - 82 = 21 +Calculator> exit +``` + +### Development + +#### Install package + +Before the beginning we need to install `snet.sdk` package: + +```commandline +pip install snet.sdk +``` + +#### Configuration + +Firstly, we need to configure sdk and service client. So we create a config dict and then an sdk instance with +that config. + +_Note:_ don't forget to import `snet.sdk` package. + +```python +from snet import sdk + +config = { + "private_key": 'APP_PROVIDER_PRIVATE_KEY', + "eth_rpc_endpoint": f"https://sepolia.infura.io/v3/INFURE_API_KEY", + "concurrency": False, + "identity_name": "NAME", + "identity_type": "key", + "network": "sepolia", + "force_update": False +} + +snet_sdk = sdk.SnetSDK(config) +``` + +Here you need to set private values: `private_key`, `eth_rpc_endpoint`, `identity_name` and possibly change some others. + +Calculator service is deployed on the sepolia network. To create a client of this service we need to pass `org_id`, +`service_id` and `group_name` to `create_service_client` method: + +```python +calc_client = snet_sdk.create_service_client(org_id="26072b8b6a0e448180f8c0e702ab6d2f", + service_id="Exampleservice", group_name="default_group") +``` + +#### User input parsing + +Secondly, we need to write a function that will process and parse user input. + +```python +def parse_expression(expression): + elements = list(expression.split()) + if len(elements) != 3: + raise Exception(f"Invalid expression '{expression}'. Three items required.") + + a = int(elements[0]) + b = int(elements[2]) + if elements[1] not in ["+", "-", "*", "/"]: + raise Exception(f"Invalid expression '{expression}'. Operation must be '+' or '-' or '*' or '/'") + elif not isinstance(a, (float, int)) or not isinstance(b, (float, int)): + raise Exception(f"Invalid expression '{expression}'. Operands must be integers or floating point numbers.") + op = elements[1] + + return a, b, op +``` + +This function splits the passed expression entered by the user into separate elements and checks their correctness. +In case of invalid input, exceptions are thrown, otherwise three elements of the required types are returned. + +#### Main cycle + +The calculator service accepts the name of the method on which the arithmetic operation depends. Therefore, we will +add a dictionary to match the operation symbol and the method name in the service. + +```python +operators = { + "+": "add", + "-": "sub", + "*": "mul", + "/": "div" +} +``` + +Now we can write the main function: + +```python +def main(): + print(""" +Welcome to the calculator powered by SingularityNET platform! +Please type the expression you want to calculate, e.g. 2 + 3. +Type 'exit' to exit the program.""") + while True: + expression = input("Calculator> ") + if expression == "exit": + break + try: + a, b, op = parse_expression(expression) + print(f"Calculating {a} {op} {b}...") + result = calc_client.call_rpc(operators[op], "Numbers", a=a, b=b) + print(f"{a} {op} {b} = {result}") + except Exception as e: + print(e) + + +if __name__ == "__main__": + main() +``` + +In an "infinite" loop, user input is read. If `exit` is entered, the program terminates. Otherwise, the expression +is parsed using the `parse_expression` function and, if no errors occur, its result is calculated using the previously +created instance of the ServiceClient class - `calc_client`, using `call_rpc` method. The result is then displayed +on the screen. + +The entire application code can be viewed at the +[link](https://github.com/Arondondon/snet-sdk-python/blob/depelopment-app-example/examples/calculator.py) to GitHub. + From c1ed00684136698fb849803102a9039764149f76 Mon Sep 17 00:00:00 2001 From: Arondondon Date: Thu, 5 Sep 2024 13:29:52 +0300 Subject: [PATCH 40/55] Tiny fix. --- examples/examples_docs/calculator.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/examples_docs/calculator.md b/examples/examples_docs/calculator.md index e529a9d..7c079a8 100644 --- a/examples/examples_docs/calculator.md +++ b/examples/examples_docs/calculator.md @@ -1,6 +1,6 @@ ## Tutorial on developing a console calculator -This is an example of how to use the SnetSDK to create a console сфдсгдфещк that works using a service +This is an example of how to use the SnetSDK to create a console calculator that works using a service on the SingularityNET platform. ### Description From 131c6e8d4d93a1da8050eef6856daae5a8d51056 Mon Sep 17 00:00:00 2001 From: Arondondon Date: Fri, 6 Sep 2024 12:48:51 +0300 Subject: [PATCH 41/55] Added console_app.md as a description to console_app.py --- examples/console_app.py | 13 +- examples/examples_docs/calculator.md | 4 +- examples/examples_docs/console_app.md | 385 ++++++++++++++++++++++++++ 3 files changed, 393 insertions(+), 9 deletions(-) create mode 100644 examples/examples_docs/console_app.md diff --git a/examples/console_app.py b/examples/console_app.py index b58d881..9015c5f 100644 --- a/examples/console_app.py +++ b/examples/console_app.py @@ -7,8 +7,7 @@ """ -from snet.sdk import SnetSDK -from snet.sdk.service_client import ServiceClient +from snet import sdk def list_organizations(): @@ -89,8 +88,8 @@ def switch_service(): def call(): """ The function, which is called when the user enters the command 'call' in the service menu. - Calls the method specified by the user of the active service. It gets data about the service using the - 'get_services_and_messages_info' and parses the resulting dict to display the correct names of the input + Calls the method specified by the user of the active service using `call_rpc` method. It gets data about the service + using the 'get_services_and_messages_info' and parses the resulting dict to display the correct names of the input and output values to the user. """ global active_service @@ -315,9 +314,9 @@ def extend_expiration(): "force_update": False } -snet_sdk = SnetSDK(config) # the 'SnetSDK' instance -initialized_services = [] # the list of initialized services -active_service: ServiceClient # the currently active service +snet_sdk = sdk.SnetSDK(config) # the 'SnetSDK' instance +initialized_services = [] # the list of initialized service clients +active_service: sdk.service_client.ServiceClient # the currently active service channels = [] # the list of open channels """ diff --git a/examples/examples_docs/calculator.md b/examples/examples_docs/calculator.md index 7c079a8..937baaa 100644 --- a/examples/examples_docs/calculator.md +++ b/examples/examples_docs/calculator.md @@ -1,6 +1,6 @@ ## Tutorial on developing a console calculator -This is an example of how to use the SnetSDK to create a console calculator that works using a service +This is an example of how to use SingularityNET Python SDK to create a console calculator that works using a service on the SingularityNET platform. ### Description @@ -8,7 +8,7 @@ on the SingularityNET platform. It is assumed that there is an application provider (developer), who pays for all the transactions and service calls. -So the application must have the next console interface: +So, the application must have the next console interface: ```commandline Welcome to the calculator powered by SingularityNET platform! diff --git a/examples/examples_docs/console_app.md b/examples/examples_docs/console_app.md new file mode 100644 index 0000000..6ba28de --- /dev/null +++ b/examples/examples_docs/console_app.md @@ -0,0 +1,385 @@ +## Tutorial on developing a console application + +This is an example of how to use SingularityNET Python SDK to create a console application that uses all the +core functionality of the SDK. + +### Description + +It is assumed that there is an application provider (developer), who pays for all the +transactions and service calls. + +The application should have a main menu and a submenu where the user enters the command name. +The application should request additional parameters for a specific command after entering the command itself. + +So, the application must have the next console interface: + +```commandline +Hello, welcome to the Snet SDK console application! +To use the application, type the name of the command you want to execute. +Available commands: + organizations - print a list of organization ids from Registry + services - print a list of service ids for an organization from Registry + balance - print the account balance and the escrow balance + deposit - deposit AGIX tokens into MPE + block - print the current block number + service - go to the services menu + channel - go to the channels menu + help - print a list of available commands in the main menu + exit - exit the application +To print a list of available commands, type 'help' +>>> channel +Available commands: + update - update a list of initialized payment channels + list - print a list of initialized payment channels + open - open a new payment channel + add-funds - add funds to a channel + extend-expiration - extend expiration of a channel + help - print a list of available commands in the channels menu + back - return to the main menu + exit - exit the application +channel >>> back +Available commands: + organizations - print a list of organization ids from Registry + services - print a list of service ids for an organization from Registry + balance - print the account balance and the escrow balance + deposit - deposit AGIX tokens into MPE + block - print the current block number + service - go to the services menu + channel - go to the channels menu + help - print a list of available commands in the main menu + exit - exit the application +>>> services +Enter organization id: 26072b8b6a0e448180f8c0e702ab6d2f +Services: + Exampleservice +>>> exit +``` + +### Development + +#### Install package + +Before the beginning we need to install `snet.sdk` package: + +```commandline +pip install snet.sdk +``` + +#### Configuration + +Firstly, we need to configure sdk and service client. So we create a config dict and then an sdk instance with +that config. + +_Note:_ don't forget to import `snet.sdk` package. + +```python +from snet import sdk + +""" +SDK configuration that is configured by the application provider. +To run the application you need to change the 'private_key', 'eth_rpc_endpoint' and 'identity_name' values. +""" +config = { + "private_key": 'APP_PROVIDER_PRIVATE_KEY', + "eth_rpc_endpoint": f"https://sepolia.infura.io/v3/APP_PROVIDER_INFURA_API_KEY", + "concurrency": False, + "identity_name": "NAME", + "identity_type": "key", + "network": "sepolia", + "force_update": False +} + +snet_sdk = sdk.SnetSDK(config) # the 'SnetSDK' instance +``` + +Here you need to set private values: `private_key`, `eth_rpc_endpoint`, `identity_name` and possibly change some others. + +#### Global variables + +The application will locally store created service clients as well as open payment channels for these services. +In addition, there must be an active service - the one for which methods are called, channels are opened, etc. + +```python +initialized_services = [] # the list of initialized service clients +active_service: sdk.service_client.ServiceClient # the currently active service +channels = [] # the list of open channels +``` + +The concept of the application is that when the user enters any command, the corresponding function should be called. +To implement this concept, we need a dict that will associate command names with the corresponding functions. + +```python +""" +Commands available in the application with their descriptions and functions to call. +""" +commands = { + "main": { + "organizations": (list_organizations, "print a list of organization ids from Registry"), + "services": (list_services_for_org, "print a list of service ids for an organization from Registry"), + "balance": (balance, "print the account balance and the escrow balance"), + "deposit": (deposit, "deposit AGIX tokens into MPE"), + "block": (block_number, "print the current block number"), + "service": (lambda: None, "go to the services menu"), + "channel": (lambda: None, "go to the channels menu"), + "help": (commands_help, "print a list of available commands in the main menu"), + "exit": (lambda: exit(0), "exit the application") + }, + + "service": { + "add": (create_service_client, + "create a new service client. If it the first time, the new service becomes active"), + "use": (switch_service, "switch the active service"), + "call": (call, "call the active service method"), + "info": (print_service_info, "output services, methods and messages in a service"), + "list": (list_initialized_services, "print a list of initialized services"), + "help": (commands_help, "print a list of available commands in the services menu"), + "back": (lambda: None, "return to the main menu"), + "exit": (lambda: exit(0), "exit the application") + }, + + "channel": { + "update": (update_channels, "update a list of initialized payment channels"), + "list": (list_channels, "print a list of initialized payment channels"), + "open": (open_channel, "open a new payment channel"), + "add-funds": (add_funds, "add funds to a channel"), + "extend-expiration": (extend_expiration, "extend expiration of a channel"), + "help": (commands_help, "print a list of available commands in the channels menu"), + "back": (lambda: None, "return to the main menu"), + "exit": (lambda: exit(0), "exit the application") + } +} +``` + +This dict is divided into three parts: the `main` part is responsible for the main menu, the `channel` part is +responsible for the channel submenu, and `service` is responsible for the service submenu. `commands` performs +several functions at once. It associates commands with their corresponding functions, stores descriptions for the +commands needed for the `help` command, and also defines the list of commands available in +the current menu. So one of the dict parts should be active to perform two last functions. + +```python +active_commands: dict = commands["main"] # the list of available commands in the active menu +``` + +`active_commands` is changed when changing menu. + +#### Main function + +```python +def main(): + """ + The function, which is called when the application is started. + Manages global variables and calls the appropriate functions. + """ + global active_commands + + print(""" +Hello, welcome to the Snet SDK console application! +To use the application, type the name of the command you want to execute.""") + commands_help() + print("To print a list of available commands, type 'help'") + + prefix = ">>> " + while True: + command = input(prefix).strip() + if command in active_commands: + active_commands[command][0]() + else: + print(f"Command '{command}' is not found. Please try again.") + continue + + if command in ["back", "service", "channel"]: + if command == "back": + command = "main" + prefix = ">>> " + else: + prefix = command + " >>> " + active_commands = commands[command] + commands_help() + + +if __name__ == "__main__": + main() + +``` + +The "main" function processes user input, calls the necessary functions, and also switches the menu (when the +corresponding command is entered) by changing the values of the "active_commands" and "prefix" variables. Of course, +all this happens in an "infinite" loop (until the "exit" command is called). + +#### Functions for commands + +There are lots of functions to implement, that can be viewed in `commands` dict. For example, `list_services_for_org`, +`deposit`, `create_service_client`, `call`, `open_channel`, etc. All these functions use SDK functionality and +global variables, and prompt the user for additional input if needed. Also some of them do some checks on user input +if necessary and possible. + +Not all functions will be described here, but the full program code with comments can be viewed at the +[link](https://github.com/Arondondon/snet-sdk-python/blob/depelopment-app-example/examples/console_app.py) to GitHub + +###### list_services_for_org + +The function, which is called when the user enters the command 'services' in the main menu. +Prints the list of services IDs, related to the organization specified by the user. +The list is got from the MPE contract using 'get_organization_list'. + +```python +def list_services_for_org(): + org_id = input("Enter organization id: ").strip() + print("Services:") + print(*map(lambda x: '\t' + x, snet_sdk.get_services_list(org_id=org_id)), sep="\n") +``` + +###### create_service_client + +The function, which is called when the user enters the command 'add' in the service menu. +Creates a service client, related to the service specified by the user, and adds it to the +list of initialized services. The creation occurs using 'create_service_client'. + +```python +def create_service_client(): + org_id = input("Enter organization id: ").strip() + service_id = input("Enter service id: ").strip() + group_name = input("Enter payment group name: ").strip() + + service = snet_sdk.create_service_client(org_id=org_id, service_id=service_id, group_name=group_name) + initialized_services.append(service) + + global active_service + if active_service is None: + active_service = service +``` + +###### commands_help + +The function, which is called when the user enters the command 'help' in any menu. +Prints the list of available commands with descriptions depending on the active menu. + +```python +def commands_help(): + global active_commands + print("Available commands:") + for command in active_commands.items(): + print(f'\t{command[0]} - {command[1][1]}') +``` + +###### call + +The function, which is called when the user enters the command 'call' in the service menu. +Calls the method specified by the user of the active service using `call_rpc` method. It gets data about the service +using the 'get_services_and_messages_info' and parses the resulting dict to display the correct names of the input +and output values to the user. + +```python +def call(): + global active_service + if active_service is None: + print("No initialized services!\n" + "Please enter 'service' to go to the service menu and then enter 'add' to add a service.") + return None + + method_name = input("Enter method name: ") + + services, messages = active_service.get_services_and_messages_info() + is_found = False + for service in services.items(): + for method in service[1]: + print(method[0], method_name) + if method[0] == method_name: + input_type = method[1] + output_type = method[2] + is_found = True + break + if is_found: + break + + if not is_found: + print(f"Method '{method_name}' is not found in service") + return None + + inputs = {var[1]: float(input(f"{var[1]}: ")) for var in messages[input_type]} + + print("Service calling...") + + result = active_service.call_rpc(method_name, input_type, **inputs) + outputs = {var[1]: getattr(result, var[1]) for var in messages[output_type]} + + print("Result:", *map(lambda x: f"{x[0]}: {x[1]}", outputs.items()), sep="\n") +``` + +###### update_channels + +The function, which is called when the user enters the command 'update' in the channel menu. +Updates the list of open channels stored in 'channels'. Gets the list of open channels using +'load_open_channels' for each initialized service. +The specified method searches for channels through the blockchain, which is why it takes quite a long time +to work, so there is a warning for the user about this at the beginning. + +```python +def update_channels(): + if active_service is None: + print("No initialized services!\n" + "Please enter 'service' to go to the service menu and then enter 'add' to add a service.") + return None + + is_continue = input("""Updating the channel list makes sense if the channel data has changed through other entry points. +This procedure may take several minutes. +Continue? (y/n): """).strip() == 'y' + if not is_continue: + return None + + print("Updating the channel list...") + global channels + channels.clear() + + for service in initialized_services: + load_channels = service.load_open_channels() + for channel in load_channels: + channels.append((channel, service.org_id, service.service_id, service.group['group_name'])) + + print("Channels updated! Enter 'list' to print the updated list.") +``` + +###### open_channel + +The function, which is called when the user enters the command 'open' in the channel menu. +Opens a new channel for the active service. Checks the balance of the MPE contract and asks the user +if they want to deposit AGIX tokens into it if there isn't enough funds. Opens the channel using 'open_channel' +or 'deposit_and_open_channel' with the user-specified amount of AGIX tokens in cogs and expiration time. + +```python +def open_channel(): + global active_service + global channels + additions = False + if active_service is None: + print("No initialized services! The channel can only be opened for the service!\n" + "Please enter 'service' to go to the service menu and then enter 'add' to add a service.") + return None + else: + is_continue = input("The new channel will be opened for the active service. Continue? (y/n): ").strip() == 'y' + if not is_continue: + return None + + amount = int(input("Enter amount of AGIX tokens in cogs to put into the channel: ").strip()) + + balance = snet_sdk.account.escrow_balance() + is_deposit = False + if balance < amount: + print(f"Insufficient balance!\n\tCurrent MPE balance: {balance}\n\tAmount to put: {amount}") + is_deposit = input("Would you like to deposit needed amount of AGIX tokens in advance? (y/n): ").strip() == 'y' + if not is_deposit: + print("Channel is not opened!") + return None + + expiration = int(input("Enter expiration time in blocks: ").strip()) + + if is_deposit: + channel = active_service.open_channel(amount=amount, expiration=expiration) + else: + channel = active_service.deposit_and_open_channel(amount=amount, expiration=expiration) + channels.append((channel, active_service.org_id, active_service.service_id, active_service.group['group_name'])) +``` + +The entire application code can be viewed at the +[link](https://github.com/Arondondon/snet-sdk-python/blob/depelopment-app-example/examples/console_app.py) to GitHub. + From e3ecb864a8ccf62c3ccdeba7e1908bb37907d2cd Mon Sep 17 00:00:00 2001 From: Arondondon Date: Fri, 6 Sep 2024 13:11:26 +0300 Subject: [PATCH 42/55] Some fixes. --- docs/main/account.md | 2 +- docs/main/concurrency_manager.md | 2 +- docs/main/generic_client_interceptor.md | 0 docs/main/init.md | 2 +- docs/main/service_client.md | 2 +- .../ipfs_metadata_provider.md | 2 +- docs/metadata_provider/metadata_provider.md | 2 +- docs/mpe/mpe_contract.md | 2 +- docs/mpe/payment_channel.md | 2 +- docs/mpe/payment_channel_provider.md | 2 +- .../default_payment_strategy.md | 2 +- .../freecall_payment_strategy.md | 2 +- .../paidcall_payment_strategy.md | 2 +- docs/payment_strategies/payment_strategy.md | 2 +- .../prepaid_payment_strategy.md | 2 +- docs/training/training.md | 131 ------------------ 16 files changed, 14 insertions(+), 145 deletions(-) delete mode 100644 docs/main/generic_client_interceptor.md delete mode 100644 docs/training/training.md diff --git a/docs/main/account.md b/docs/main/account.md index d3d8974..c79aef2 100644 --- a/docs/main/account.md +++ b/docs/main/account.md @@ -1,6 +1,6 @@ ## module: sdk.account -[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/account.py) to GitHub +[Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/account.py) to GitHub Entities: 1. [TransactionError](#class-transactionerror) diff --git a/docs/main/concurrency_manager.md b/docs/main/concurrency_manager.md index 978efe4..c52eaef 100644 --- a/docs/main/concurrency_manager.md +++ b/docs/main/concurrency_manager.md @@ -1,6 +1,6 @@ ## module: sdk.concurrency_manager -[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/concurrency_manager.py) to GitHub +[Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/concurrency_manager.py) to GitHub Entities: 1. [ConcurrencyManager](#class-concurrencymanager) diff --git a/docs/main/generic_client_interceptor.md b/docs/main/generic_client_interceptor.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/main/init.md b/docs/main/init.md index d89af00..2bb602e 100644 --- a/docs/main/init.md +++ b/docs/main/init.md @@ -1,6 +1,6 @@ ## module: sdk.\_\_init\_\_ -[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/__init__.py) to GitHub +[Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/__init__.py) to GitHub Entities: 1. [Arguments](#class-arguments) diff --git a/docs/main/service_client.md b/docs/main/service_client.md index f3d9976..bf22bc1 100644 --- a/docs/main/service_client.md +++ b/docs/main/service_client.md @@ -1,6 +1,6 @@ ## module: sdk.service_client -[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/service_client.py) to GitHub +[Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/service_client.py) to GitHub Entities: 1. [ServiceClient](#class-serviceclient) diff --git a/docs/metadata_provider/ipfs_metadata_provider.md b/docs/metadata_provider/ipfs_metadata_provider.md index 9edde79..bf219b3 100644 --- a/docs/metadata_provider/ipfs_metadata_provider.md +++ b/docs/metadata_provider/ipfs_metadata_provider.md @@ -1,6 +1,6 @@ ## module: sdk.metadata_provider.ipfs_metadata_provider -[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/metadata_provider/metadata_provider.py) to GitHub +[Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/metadata_provider/metadata_provider.py) to GitHub Entities: 1. [IPFSMetadataProvider](#class-transactionerror) diff --git a/docs/metadata_provider/metadata_provider.md b/docs/metadata_provider/metadata_provider.md index f3d8347..42c09f0 100644 --- a/docs/metadata_provider/metadata_provider.md +++ b/docs/metadata_provider/metadata_provider.md @@ -1,6 +1,6 @@ ## module: sdk.metadata_provider.metadata_provider -[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/metadata_provider/metadata_provider.py) to GitHub +[Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/metadata_provider/metadata_provider.py) to GitHub Entities: 1. [MetadataProvider](#class-transactionerror) diff --git a/docs/mpe/mpe_contract.md b/docs/mpe/mpe_contract.md index 957fe39..3957ec7 100644 --- a/docs/mpe/mpe_contract.md +++ b/docs/mpe/mpe_contract.md @@ -1,6 +1,6 @@ ## module: sdk.mpe.mpe_contract -[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/mpe/mpe_contract.py) to GitHub +[Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/mpe/mpe_contract.py) to GitHub Entities: 1. [MPEContract](#class-mpecontract) diff --git a/docs/mpe/payment_channel.md b/docs/mpe/payment_channel.md index 648ada7..eb80598 100644 --- a/docs/mpe/payment_channel.md +++ b/docs/mpe/payment_channel.md @@ -1,6 +1,6 @@ ## module: sdk.mpe.payment_channel -[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/mpe/payment_channel.py) to GitHub +[Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/mpe/payment_channel.py) to GitHub Entities: 1. [PaymentChannel](#class-paymentchannel) diff --git a/docs/mpe/payment_channel_provider.md b/docs/mpe/payment_channel_provider.md index 99a339d..db6a957 100644 --- a/docs/mpe/payment_channel_provider.md +++ b/docs/mpe/payment_channel_provider.md @@ -1,6 +1,6 @@ ## module: sdk.mpe.payment_channel_provider -[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/mpe/payment_channel_provider.py) to GitHub +[Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/mpe/payment_channel_provider.py) to GitHub Entities: 1. [PaymentChannelProvider](#class-paymentchannelprovider) diff --git a/docs/payment_strategies/default_payment_strategy.md b/docs/payment_strategies/default_payment_strategy.md index b5c31f5..fc81605 100644 --- a/docs/payment_strategies/default_payment_strategy.md +++ b/docs/payment_strategies/default_payment_strategy.md @@ -1,6 +1,6 @@ ## module: sdk.payment_strategies.default_payment_strategy -[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/payment_strategies/default_payment_strategy.py) to GitHub +[Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/payment_strategies/default_payment_strategy.py) to GitHub Entities: 1. [DefaultPaymentStrategy](#class-defaultpaymentstrategy) diff --git a/docs/payment_strategies/freecall_payment_strategy.md b/docs/payment_strategies/freecall_payment_strategy.md index 2338e8f..29b0dc6 100644 --- a/docs/payment_strategies/freecall_payment_strategy.md +++ b/docs/payment_strategies/freecall_payment_strategy.md @@ -1,6 +1,6 @@ ## module: sdk.payment_strategies.freecall_payment_strategy -[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/payment_strategies/freecall_payment_strategy.py) to GitHub +[Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/payment_strategies/freecall_payment_strategy.py) to GitHub Entities: 1. [FreeCallPaymentStrategy](#class-freecallpaymentstrategy) diff --git a/docs/payment_strategies/paidcall_payment_strategy.md b/docs/payment_strategies/paidcall_payment_strategy.md index 2cd2c99..d39a09d 100644 --- a/docs/payment_strategies/paidcall_payment_strategy.md +++ b/docs/payment_strategies/paidcall_payment_strategy.md @@ -1,6 +1,6 @@ ## module: sdk.payment_strategies.paidcall_payment_strategy -[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/payment_strategies/paidcall_payment_strategy.py) to GitHub +[Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/payment_strategies/paidcall_payment_strategy.py) to GitHub Entities: 1. [PaidCallPaymentStrategy](#class-paidcallpaymentstrategy) diff --git a/docs/payment_strategies/payment_strategy.md b/docs/payment_strategies/payment_strategy.md index 87527e1..7beb8c0 100644 --- a/docs/payment_strategies/payment_strategy.md +++ b/docs/payment_strategies/payment_strategy.md @@ -2,7 +2,7 @@ -[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/payment_strategies/payment_staregy.py) to GitHub +[Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/payment_strategies/payment_staregy.py) to GitHub Entities: 1. [PaymentStrategy](#class-paymentstrategy) diff --git a/docs/payment_strategies/prepaid_payment_strategy.md b/docs/payment_strategies/prepaid_payment_strategy.md index c5e28a7..e133991 100644 --- a/docs/payment_strategies/prepaid_payment_strategy.md +++ b/docs/payment_strategies/prepaid_payment_strategy.md @@ -1,6 +1,6 @@ ## module: sdk.payment_strategies.prepaid_payment_strategy -[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/payment_strategies/prepaid_payment_strategy.py) to GitHub +[Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/payment_strategies/prepaid_payment_strategy.py) to GitHub Entities: 1. [PrePaidPaymentStrategy](#class-prepaidpaymentstrategy) diff --git a/docs/training/training.md b/docs/training/training.md deleted file mode 100644 index f27efd6..0000000 --- a/docs/training/training.md +++ /dev/null @@ -1,131 +0,0 @@ -## module: sdk.training.training - -[link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/training/training.py) to GitHub - -Entities: -1. [ModelMethodMessage](#enum-modelmethodmessage) -2. [TrainingModel](#class-trainingmodel) - - [\_\_init\_\_](#__init__) - - [_invoke_model](#_invoke_model) - - [create_model](#create_model) - - [get_model_status](#get_model_status) - - [delete_model](#delete_model) - - [update_model_access](#_update_model_access) - - [get_all_models](#_get_all_models) - -### enum `ModelMethodMessage` - -#### description - -An enumeration containing method messages for the model from daemon code. - -_Note_: This is a class that inherits from "Enum" - -#### elements - -- `CreateModel` -- `GetModelStatus` -- `UpdateModelAccess` -- `GetAllModels` -- `DeleteModel` - -### class `TrainingModel` - -extends: - - -is extended by: - - -#### description - - - -#### attributes - -- - -#### methods - -#### `__init__` - -Initializes a new instance of the class. - -###### args: - -- - -###### returns: - -- _None_ - -#### `_invoke_model` - - - -###### args: - -- - -###### returns: - -- - -#### `create_model` - - - -###### args: - -- - -###### returns: - -- - -#### `get_model_status` - - - -###### args: - -- - -###### returns: - -- - -#### `delete_model` - - - -###### args: - -- - -###### returns: - -- - -#### `update_model_access` - - - -###### args: - -- - -###### returns: - -- - -#### `get_all_models` - - - -###### args: - -- - -###### returns: - -- - From 35bd4c237a456cb76c7c0b47adab43ebdcfff2bd Mon Sep 17 00:00:00 2001 From: Arondondon Date: Fri, 6 Sep 2024 13:21:32 +0300 Subject: [PATCH 43/55] Tiny fix. --- docs/main/service_client.md | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/docs/main/service_client.md b/docs/main/service_client.md index bf22bc1..0439072 100644 --- a/docs/main/service_client.md +++ b/docs/main/service_client.md @@ -155,17 +155,7 @@ Retrieves the metadata required for making a service call using the payment stra - Payment metadata. (list[tuple[str, Any]]) -#### `_intercept_call` - - - -###### args: - -- - -###### returns: - -- + #### `_filter_existing_channels_from_new_payment_channels` From 05db37c1e3c6ad9a35dee53d0916228b18288588 Mon Sep 17 00:00:00 2001 From: Arondondon Date: Tue, 10 Sep 2024 14:40:09 +0300 Subject: [PATCH 44/55] Global changes after getting rid of CLI dependencies. Unneeded modules and functions removed, structure partially changed. --- snet/sdk/__init__.py | 34 +- snet/sdk/client_lib_generator.py | 70 ++ snet/sdk/commands/__init__.py | 0 snet/sdk/commands/commands.py | 842 ------------------ snet/sdk/commands/mpe_service.py | 559 ------------ snet/sdk/commands/sdk_command.py | 39 - snet/sdk/contract.py | 27 - snet/sdk/identity.py | 341 ------- snet/sdk/metadata/__init__.py | 0 snet/sdk/metadata/organization.py | 355 -------- .../ipfs_metadata_provider.py | 2 +- .../service_metadata.py} | 0 snet/sdk/payment.py | 6 - snet/sdk/utils/config.py | 62 -- snet/sdk/utils/ipfs_utils.py | 60 +- snet/sdk/utils/utils.py | 160 +--- 16 files changed, 90 insertions(+), 2467 deletions(-) create mode 100644 snet/sdk/client_lib_generator.py delete mode 100644 snet/sdk/commands/__init__.py delete mode 100644 snet/sdk/commands/commands.py delete mode 100644 snet/sdk/commands/mpe_service.py delete mode 100644 snet/sdk/commands/sdk_command.py delete mode 100644 snet/sdk/contract.py delete mode 100644 snet/sdk/identity.py delete mode 100644 snet/sdk/metadata/__init__.py delete mode 100644 snet/sdk/metadata/organization.py rename snet/sdk/{metadata/service.py => metadata_provider/service_metadata.py} (100%) delete mode 100644 snet/sdk/payment.py delete mode 100644 snet/sdk/utils/config.py diff --git a/snet/sdk/__init__.py b/snet/sdk/__init__.py index 96f1f95..1b59145 100644 --- a/snet/sdk/__init__.py +++ b/snet/sdk/__init__.py @@ -14,8 +14,7 @@ from snet.sdk.metadata_provider.ipfs_metadata_provider import IPFSMetadataProvider from snet.sdk.payment_strategies.default_payment_strategy import DefaultPaymentStrategy -from snet.sdk.commands.sdk_command import SDKCommand -from snet.sdk.commands.commands import BlockchainCommand +from snet.sdk.client_lib_generator import ClientLibGenerator from snet.sdk.config import Config from snet.sdk.utils.utils import bytes32_to_str, type_converter @@ -35,7 +34,7 @@ from snet.contracts import get_contract_object -from snet.sdk.metadata.service import mpe_service_metadata_from_json +from snet.sdk.metadata_provider.service_metadata import mpe_service_metadata_from_json from snet.sdk.utils.ipfs_utils import bytesuri_to_hash, get_from_ipfs_and_checkhash from snet.sdk.utils.utils import find_file_by_keyword @@ -43,14 +42,6 @@ ServiceStub = NewType('ServiceStub', Any) -class Arguments: - def __init__(self, org_id=None, service_id=None): - self.org_id = org_id - self.service_id = service_id - self.language = "python" - self.protodir = Path("~").expanduser().joinpath(".snet") - - class SnetSDK: """Base Snet SDK""" @@ -118,20 +109,20 @@ def create_service_client(self, org_id: str, service_id: str, group_name=None, options=None, concurrent_calls=1): - # Create and instance of the Config object, so we can create an instance of SDKCommand + # Create and instance of the Config object, so we can create an instance of ClientLibGenerator sdk_config_object = Config(sdk_config=self._sdk_config) - sdk = SDKCommand(sdk_config_object, args=Arguments(org_id, service_id)) + lib_generator = ClientLibGenerator(sdk_config_object, self.registry_contract, org_id, service_id) # Download the proto file and generate stubs if needed force_update = self._sdk_config.get('force_update', False) if force_update: - sdk.generate_client_library() + lib_generator.generate_client_library() else: path_to_pb_files = self.get_path_to_pb_files(org_id, service_id) pb_2_file_name = find_file_by_keyword(path_to_pb_files, keyword="pb2.py") pb_2_grpc_file_name = find_file_by_keyword(path_to_pb_files, keyword="pb2_grpc.py") if not pb_2_file_name or not pb_2_grpc_file_name: - sdk.generate_client_library() + lib_generator.generate_client_library() if payment_channel_management_strategy is None: payment_channel_management_strategy = DefaultPaymentStrategy(concurrent_calls) @@ -218,21 +209,16 @@ def _get_service_group_details(self, service_metadata, group_name): return self._get_group_by_group_name(service_metadata, group_name) def get_organization_list(self) -> list: - global_config = Config(sdk_config=self._sdk_config) - blockchain_command = BlockchainCommand(config=global_config, args=Arguments()) - org_list = blockchain_command.call_contract_command( - "Registry", "listOrganizations", []) + org_list = self.registry_contract.functions.listOrganizations().call() organization_list = [] for idx, org_id in enumerate(org_list): organization_list.append(bytes32_to_str(org_id)) return organization_list def get_services_list(self, org_id: str) -> list: - global_config = Config(sdk_config=self._sdk_config) - blockchain_command = BlockchainCommand(config=global_config, args=Arguments()) - (found, org_service_list) = blockchain_command.call_contract_command("Registry", - "listServicesForOrganization", - [type_converter("bytes32")(org_id)]) + found, org_service_list = self.registry_contract.functions.listServicesForOrganization( + type_converter("bytes32")(org_id) + ).call() if not found: raise Exception(f"Organization with id={org_id} doesn't exist!") org_service_list = list(map(bytes32_to_str, org_service_list)) diff --git a/snet/sdk/client_lib_generator.py b/snet/sdk/client_lib_generator.py new file mode 100644 index 0000000..8623162 --- /dev/null +++ b/snet/sdk/client_lib_generator.py @@ -0,0 +1,70 @@ +import os +from pathlib import Path, PurePath + +from snet.sdk.utils import ipfs_utils +from snet.sdk.utils.utils import compile_proto, type_converter +from snet.sdk.metadata_provider.service_metadata import mpe_service_metadata_from_json + + +class ClientLibGenerator: + + def __init__(self, sdk_config, registry_contract, org_id, service_id): + self.sdk_config = sdk_config + self.registry_contract = registry_contract + self.org_id = org_id + self.service_id = service_id + self.language = "python" + self.protodir = Path("~").expanduser().joinpath(".snet") + + def generate_client_library(self): + + if os.path.isabs(self.protodir): + client_libraries_base_dir_path = PurePath(self.protodir) + else: + cur_dir_path = PurePath(os.getcwd()) + client_libraries_base_dir_path = cur_dir_path.joinpath(self.protodir) + + os.makedirs(client_libraries_base_dir_path, exist_ok=True) + + # Create service client libraries path + library_language = self.language + library_org_id = self.org_id + library_service_id = self.service_id + + library_dir_path = client_libraries_base_dir_path.joinpath(library_org_id, + library_service_id, + library_language) + + try: + metadata = self._get_service_metadata_from_registry() + model_ipfs_hash = metadata["model_ipfs_hash"] + + # Receive proto files + ipfs_utils.safe_extract_proto_from_ipfs(ipfs_utils.get_ipfs_client(self.sdk_config), + model_ipfs_hash, library_dir_path) + + # Compile proto files + compile_proto(Path(library_dir_path), library_dir_path, target_language=self.language) + + print(f'client libraries for service with id "{library_service_id}" in org with id "{library_org_id}" ' + f'generated at {library_dir_path}') + except Exception as e: + print(e) + + def _get_service_metadata_from_registry(self): + rez = self._get_service_registration() + metadata_hash = ipfs_utils.bytesuri_to_hash(rez["metadataURI"]) + metadata = ipfs_utils.get_from_ipfs_and_checkhash(ipfs_utils.get_ipfs_client(self.sdk_config), metadata_hash) + metadata = metadata.decode("utf-8") + metadata = mpe_service_metadata_from_json(metadata) + return metadata + + def _get_service_registration(self): + params = [type_converter("bytes32")(self.org_id), type_converter("bytes32")(self.service_id)] + rez = self.registry_contract.functions.getServiceRegistrationById(*params).call() + if not rez[0]: + raise Exception("Cannot find Service with id=%s in Organization with id=%s" % ( + self.service_id, self.org_id)) + return {"metadataURI": rez[2]} + + diff --git a/snet/sdk/commands/__init__.py b/snet/sdk/commands/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/snet/sdk/commands/commands.py b/snet/sdk/commands/commands.py deleted file mode 100644 index a5882f6..0000000 --- a/snet/sdk/commands/commands.py +++ /dev/null @@ -1,842 +0,0 @@ -import base64 -import getpass -import json -import secrets -import sys -from textwrap import indent -from urllib.parse import urljoin - -import ipfshttpclient -import yaml -from rfc3986 import urlparse -import web3 -from snet.contracts import get_contract_def - -from snet.sdk.contract import Contract -from snet.sdk.identity import KeyIdentityProvider, KeyStoreIdentityProvider, LedgerIdentityProvider, \ - MnemonicIdentityProvider, RpcIdentityProvider, TrezorIdentityProvider, get_kws_for_identity_type -from snet.sdk.metadata.organization import OrganizationMetadata, PaymentStorageClient, Payment, Group -from snet.sdk.utils.config import get_contract_address, get_field_from_args_or_session, \ - read_default_contract_address -from snet.sdk.utils.ipfs_utils import bytesuri_to_hash, get_from_ipfs_and_checkhash, \ - hash_to_bytesuri, publish_file_in_ipfs -from snet.sdk.utils.utils import DefaultAttributeObject, get_web3, is_valid_url, serializable, type_converter, \ - get_cli_version, bytes32_to_str - - -class Command(object): - def __init__(self, config, args, out_f=sys.stdout, err_f=sys.stderr): - self.config = config - self.args = args - self.out_f = out_f - self.err_f = err_f - - def _error(self, message): - self._printerr("ERROR: {}".format(message)) - sys.exit(1) - - def _ensure(self, condition, message): - if not condition: - self._error(message) - - @staticmethod - def _print(message, fd): - message = str(message) + "\n" - try: - fd.write(message) - except UnicodeEncodeError: - if hasattr(fd, "buffer"): - fd.buffer.write(message.encode("utf-8")) - else: - raise - - def _printout(self, message): - if self.out_f is not None: - self._print(message, self.out_f) - - def _printerr(self, message): - if self.err_f is not None: - self._print(message, self.err_f) - - def _pprint(self, item): - self._printout(indent(yaml.dump(json.loads(json.dumps(item, default=serializable)), default_flow_style=False, - indent=4), " ")) - - def _pprint_receipt_and_events(self, receipt, events): - if self.args.verbose: - self._pprint({"receipt": receipt, "events": events}) - elif self.args.quiet: - self._pprint({"transactionHash": receipt["transactionHash"]}) - else: - self._pprint({"receipt_summary": {"blockHash": receipt["blockHash"], - "blockNumber": receipt["blockNumber"], - "cumulativeGasUsed": receipt["cumulativeGasUsed"], - "gasUsed": receipt["gasUsed"], - "transactionHash": receipt["transactionHash"]}, - "event_summaries": [{"args": e["args"], "event": e["event"]} for e in events]}) - - def _get_ipfs_client(self): - ipfs_endpoint = self.config.get_ipfs_endpoint() - return ipfshttpclient.connect(ipfs_endpoint) - - -class VersionCommand(Command): - def show(self): - self._pprint({"version": get_cli_version()}) - - -""" -# Temporally deprecated - -class CachedGasPriceStrategy: - def __init__(self, gas_price_param): - self.gas_price_param = gas_price_param - self.cached_gas_price = None - - def __call__(self, w3, transaction_params): - if self.cached_gas_price is None: - self.cached_gas_price = int( - self.calc_gas_price(w3, transaction_params)) - return self.cached_gas_price - - def calc_gas_price(self, w3, transaction_params): - gas_price_param = self.gas_price_param - if gas_price_param.isdigit(): - return int(self.gas_price_param) - if gas_price_param == "fast": - return fast_gas_price_strategy(w3, transaction_params) - if gas_price_param == "medium": - return medium_gas_price_strategy(w3, transaction_params) - if gas_price_param == "slow": - return slow_gas_price_strategy(w3, transaction_params) - raise Exception("Unknown gas price strategy: %s" % gas_price_param) - - def is_going_to_calculate(self): - return self.cached_gas_price is None and not self.gas_price_param.isdigit() -""" - - -class BlockchainCommand(Command): - def __init__(self, config, args, out_f=sys.stdout, err_f=sys.stderr, w3=None, ident=None): - super(BlockchainCommand, self).__init__(config, args, out_f, err_f) - self.w3 = w3 or get_web3(self.get_eth_endpoint()) - self.ident = ident or self.get_identity() - - def get_eth_endpoint(self): - # the only one source of eth_rpc_endpoint is the configuration file - return self.config.get_session_field("default_eth_rpc_endpoint") - - def get_wallet_index(self): - return int(get_field_from_args_or_session(self.config, self.args, "wallet_index")) - - def get_gas_price_param(self): - return get_field_from_args_or_session(self.config, self.args, "gas_price") - - def get_gas_price_verbose(self): - # gas price is not given explicitly in Wei - self._printerr("# Calculating gas price... one moment..") - gas_price = self.w3.eth.gas_price - if gas_price <= 15000000000: - gas_price += gas_price * 1 / 3 - elif gas_price > 15000000000 and gas_price <= 50000000000: - gas_price += gas_price * 1 / 5 - elif gas_price > 50000000000 and gas_price <= 150000000000: - gas_price += 7000000000 - elif gas_price > 150000000000: - gas_price += gas_price * 1 / 10 - self._printerr("# gas_price = %f GWei" % (gas_price * 1E-9)) - return int(gas_price) - - def get_mpe_address(self): - return get_contract_address(self, "MultiPartyEscrow") - - def get_registry_address(self): - return get_contract_address(self, "Registry") - - def get_identity(self): - identity_type = self.config.get_session_field("identity_type") - - if identity_type == "rpc": - return RpcIdentityProvider(self.w3, self.get_wallet_index()) - if identity_type == "mnemonic": - return MnemonicIdentityProvider(self.w3, self.config.get_session_field("mnemonic"), self.get_wallet_index()) - if identity_type == "trezor": - return TrezorIdentityProvider(self.w3, self.get_wallet_index()) - if identity_type == "ledger": - return LedgerIdentityProvider(self.w3, self.get_wallet_index()) - if identity_type == "key": - return KeyIdentityProvider(self.w3, self.config.get_session_field("private_key")) - if identity_type == "keystore": - return KeyStoreIdentityProvider(self.w3, self.config.get_session_field("keystore_path")) - - def get_contract_argser(self, contract_address, contract_function, contract_def, **kwargs): - def f(*positional_inputs, **named_inputs): - args_dict = self.args.__dict__.copy() - args_dict.update(dict( - at=contract_address, - contract_function=contract_function, - contract_def=contract_def, - contract_positional_inputs=positional_inputs, - **kwargs - )) - for k, v in named_inputs.items(): - args_dict["contract_named_input_{}".format(k)] = v - return DefaultAttributeObject(**args_dict) - - return f - - def get_contract_command(self, contract_name, contract_address, contract_fn, contract_params, is_silent=True): - contract_def = get_contract_def(contract_name) - if is_silent: - out_f = None - err_f = None - else: - out_f = self.out_f - err_f = self.err_f - return ContractCommand(config=self.config, - args=self.get_contract_argser( - contract_address=contract_address, - contract_function=contract_fn, - contract_def=contract_def, - contract_name=contract_name)(*contract_params), - out_f=out_f, - err_f=err_f, - w3=self.w3, - ident=self.ident) - - def call_contract_command(self, contract_name, contract_fn, contract_params, is_silent=True): - contract_address = get_contract_address(self, contract_name) - return self.get_contract_command(contract_name, contract_address, - contract_fn, contract_params, is_silent).call() - - def transact_contract_command(self, contract_name, contract_fn, contract_params, is_silent=False): - contract_address = get_contract_address(self, contract_name) - return self.get_contract_command(contract_name, contract_address, contract_fn, contract_params, - is_silent).transact() - - -class IdentityCommand(Command): - def create(self): - identity = {} - - identity_name = self.args.identity_name - self._ensure(identity_name not in self.config.get_all_identities_names(), - "identity_name {} already exists".format(identity_name)) - - identity_type = self.args.identity_type - identity["identity_type"] = identity_type - - for kw, is_secret in get_kws_for_identity_type(identity_type): - value = getattr(self.args, kw) - if value is None and is_secret: - kw_prompt = "{}: ".format(" ".join(kw.capitalize().split("_"))) - value = getpass.getpass(kw_prompt) or None - self._ensure( - value is not None, "--{} is required for identity_type {}".format(kw, identity_type)) - identity[kw] = value - - if self.args.network: - identity["network"] = self.args.network - identity["default_wallet_index"] = self.args.wallet_index - self.config.add_identity(identity_name, identity, self.out_f) - - def list(self): - for identity_section in filter(lambda x: x.startswith("identity."), self.config.sections()): - identity = self.config[identity_section] - key_is_secret_lookup = {} - - identity_type = self.config.get(identity_section, 'identity_type') - for kw, is_secret in get_kws_for_identity_type(identity_type): - key_is_secret_lookup[kw] = is_secret - - self._pprint({ - identity_section[len("identity."):]: { - k: (v if not key_is_secret_lookup.get(k, False) else "xxxxxx") for k, v in identity.items() - } - }) - - def delete(self): - self.config.delete_identity(self.args.identity_name) - - def set(self): - self.config.set_session_identity(self.args.identity_name, self.out_f) - - -class NetworkCommand(Command): - def list(self): - for network_section in filter(lambda x: x.startswith("network."), self.config.sections()): - network = self.config[network_section] - self._pprint({network_section[len("network."):]: { - k: v for k, v in network.items()}}) - - def create(self): - network_id = None - w3 = get_web3(self.args.eth_rpc_endpoint) - if not self.args.skip_check: - # check endpoint by getting its network_id - network_id = w3.net.version - - self._printout("add network with name='%s' with networkId='%s'" % ( - self.args.network_name, str(network_id))) - - default_gas_price = w3.eth.gas_price - self.config.add_network( - self.args.network_name, self.args.eth_rpc_endpoint, default_gas_price) - - def set(self): - self.config.set_session_network(self.args.network_name, self.out_f) - - -class SessionSetCommand(Command): - def set(self): - self.config.set_session_field( - self.args.key, self.args.value, self.out_f) - - def unset(self): - self.config.unset_session_field(self.args.key, self.out_f) - - -class SessionShowCommand(BlockchainCommand): - def show(self): - rez = self.config.session_to_dict() - key = "network.%s" % rez['session']['network'] - self.populate_contract_address(rez, key) - - # we don't want to who private_key and mnemonic - for d in rez.values(): - d.pop("private_key", None) - d.pop("mnemonic", None) - self._pprint(rez) - - def populate_contract_address(self, rez, key): - try: - rez[key]['default_registry_at'] = read_default_contract_address( - w3=self.w3, contract_name="Registry") - rez[key]['default_multipartyescrow_at'] = read_default_contract_address( - w3=self.w3, contract_name="MultiPartyEscrow") - rez[key]['default_singularitynettoken_at'] = read_default_contract_address( - w3=self.w3, contract_name="SingularityNetToken") - except Exception as e: - pass - return - - -class ContractCommand(BlockchainCommand): - def call(self): - contract_address = get_contract_address(self, self.args.contract_name, - "--at is required to specify target contract address") - - abi = self.args.contract_def["abi"] - - contract = Contract(self.w3, contract_address, abi) - - positional_inputs = getattr( - self.args, "contract_positional_inputs", []) - named_inputs = { - name[len("contract_named_input_"):]: value for name, value - in self.args.__dict__.items() if name.startswith("contract_named_input_") - } - - result = contract.call(self.args.contract_function, - *positional_inputs, **named_inputs) - self._printout(result) - return result - - def transact(self): - contract_address = get_contract_address(self, self.args.contract_name, - "--at is required to specify target contract address") - - abi = self.args.contract_def["abi"] - - contract = Contract(self.w3, contract_address, abi) - - positional_inputs = getattr( - self.args, "contract_positional_inputs", []) - named_inputs = { - name[len("contract_named_input_"):]: value for name, value - in self.args.__dict__.items() if name.startswith("contract_named_input_") - } - - gas_price = self.get_gas_price_verbose() - - txn = contract.build_transaction(self.args.contract_function, - self.ident.get_address(), - gas_price, - *positional_inputs, - **named_inputs) - - if not self.args.yes or self.args.verbose: - self._pprint({"transaction": txn}) - - proceed = self.args.yes or input("Proceed? (y/n): ") == "y" - - if proceed: - receipt = self.ident.transact(txn, self.err_f) - events = contract.process_receipt(receipt) - self._pprint_receipt_and_events(receipt, events) - return receipt, events - else: - self._error("Cancelled") - - -class OrganizationCommand(BlockchainCommand): - - def add_group(self): - metadata_file = self.args.metadata_file - - try: - with open(metadata_file, 'r') as f: - org_metadata = OrganizationMetadata.from_json(json.load(f)) - except Exception as e: - print( - "Organization metadata json file not found ,Please check --metadata-file path ") - raise e - - for endpoint in self.args.endpoints: - if not is_valid_url(endpoint): - raise Exception(f"Invalid {endpoint} endpoint passed") - - payment_storage_client = PaymentStorageClient(self.args.payment_channel_connection_timeout, - self.args.payment_channel_request_timeout, self.args.endpoints) - payment = Payment(self.args.payment_address, self.args.payment_expiration_threshold, - self.args.payment_channel_storage_type, payment_storage_client) - group_id = base64.b64encode(secrets.token_bytes(32)) - - group = Group(self.args.group_name, group_id.decode("ascii"), payment) - org_metadata.add_group(group) - org_metadata.save_pretty(metadata_file) - - def remove_group(self): - group_id = self.args.group_id - metadata_file = self.args.metadata_file - - try: - with open(metadata_file, 'r') as f: - org_metadata = OrganizationMetadata.from_json(json.load(f)) - except Exception as e: - print( - "Organization metadata json file not found ,Please check --metadata-file path ") - raise e - - existing_groups = org_metadata.groups - updated_groups = [ - group for group in existing_groups if not group_id == group.group_id] - org_metadata.groups = updated_groups - org_metadata.save_pretty(metadata_file) - - def set_changed_values_for_group(self, group): - # if value of a parameter is None that means it was not updated - - if self.args.endpoints: - group.update_endpoints(self.args.endpoints) - if self.args.payment_address: - group.update_payment_address(self.args.payment_address) - if self.args.payment_expiration_threshold: - group.update_payment_expiration_threshold( - self.args.payment_expiration_threshold) - if self.args.payment_channel_storage_type: - group.update_payment_channel_storage_type( - self.args.payment_channel_storage_type) - if self.args.payment_channel_connection_timeout: - group.update_connection_timeout( - self.args.payment_channel_connection_timeout) - if self.args.payment_channel_request_timeout: - group.update_request_timeout( - self.args.payment_channel_request_timeout) - - def update_group(self): - group_id = self.args.group_id - metadata_file = self.args.metadata_file - try: - with open(metadata_file, 'r') as f: - org_metadata = OrganizationMetadata.from_json(json.load(f)) - except Exception as e: - print( - "Organization metadata json file not found ,Please check --metadata-file path ") - raise e - existing_groups = org_metadata.groups - for group in existing_groups: - if group_id == group.group_id: - self.set_changed_values_for_group(group) - - org_metadata.save_pretty(metadata_file) - - def initialize_metadata(self): - org_id = self.args.org_id - metadata_file_name = self.args.metadata_file - - # Check if Organization already exists - found = self._get_organization_by_id(org_id)[0] - if found: - raise Exception( - "\nOrganization with id={} already exists!\n".format(org_id)) - org_metadata = OrganizationMetadata(self.args.org_name, org_id, self.args.org_type) - org_metadata.save_pretty(metadata_file_name) - - def print_metadata(self): - org_id = self.args.org_id - org_metadata = self._get_organization_metadata_from_registry(org_id) - self._printout(org_metadata.get_json_pretty()) - - def _get_organization_registration(self, org_id): - params = [type_converter("bytes32")(org_id)] - rez = self.call_contract_command( - "Registry", "getOrganizationById", params) - if not rez[0]: - raise Exception("Cannot find Organization with id=%s" % ( - self.args.org_id)) - return {"orgMetadataURI": rez[2]} - - def _get_organization_metadata_from_registry(self, org_id): - rez = self._get_organization_registration(org_id) - metadata_hash = bytesuri_to_hash(rez["orgMetadataURI"]) - metadata = get_from_ipfs_and_checkhash( - self._get_ipfs_client(), metadata_hash) - metadata = metadata.decode("utf-8") - return OrganizationMetadata.from_json(json.loads(metadata)) - - def _get_organization_by_id(self, org_id): - org_id_bytes32 = type_converter("bytes32")(org_id) - if len(org_id_bytes32) > 32: - raise Exception("Your org_id is too long, it should be 32 bytes or less. len(org_id_bytes32)=%i" % ( - len(org_id_bytes32))) - return self.call_contract_command("Registry", "getOrganizationById", [org_id_bytes32]) - - # TODO: It would be better to have standard nargs="+" in argparse for members. - # But we keep comma separated members for backward compatibility - def get_members_from_args(self): - if not self.args.members: - return [] - members = [m.replace("[", "").replace("]", "") - for m in self.args.members.split(',')] - for m in members: - if not web3.Web3.is_checksum_address(m): - raise Exception( - "Member account %s is not a valid Ethereum checksum address" % m) - return members - - def list(self): - org_list = self.call_contract_command( - "Registry", "listOrganizations", []) - - self._printout("# OrgId") - for idx, org_id in enumerate(org_list): - self._printout(bytes32_to_str(org_id)) - - def list_org_name(self): - org_list = self.call_contract_command( - "Registry", "listOrganizations", []) - - self._printout("# OrgName OrgId") - for idx, org_id in enumerate(org_list): - rez = self.call_contract_command( - "Registry", "getOrganizationById", [org_id]) - if not rez[0]: - raise Exception( - "Organization was removed during this call. Please retry.") - org_name = rez[2] - self._printout("%s %s" % (org_name, bytes32_to_str(org_id))) - - def error_organization_not_found(self, org_id, found): - if not found: - raise Exception( - "Organization with id={} doesn't exist!\n".format(org_id)) - - def info(self): - org_id = self.args.org_id - (found, org_id, org_name, owner, members, serviceNames) = self._get_organization_by_id(org_id) - self.error_organization_not_found(self.args.org_id, found) - - org_m = self._get_organization_metadata_from_registry(web3.Web3.to_text(org_id)) - org_name = org_m.org_name - org_type = org_m.org_type - description = org_m.description.get("description", "") - - self._printout("\nOrganization Name:\n - %s" % org_name) - self._printout("\nId:\n - %s" % bytes32_to_str(org_id)) - self._printout("\nType:\n - %s" % org_type) - self._printout("\nDescription:\n - %s" % description) - - if members: - self._printout("\nMembers:") - for idx, member in enumerate(members): - self._printout(" - {}".format(member)) - if serviceNames: - self._printout("\nServices:") - for idx, service in enumerate(serviceNames): - self._printout(" - {}".format(bytes32_to_str(service))) - - def create(self): - - metadata_file = self.args.metadata_file - - try: - with open(metadata_file, 'r') as f: - org_metadata = OrganizationMetadata.from_json(json.load(f)) - except Exception as e: - print( - "Organization metadata json file not found ,Please check --metadata-file path ") - raise e - org_id = self.args.org_id - # validate the metadata before creating - org_metadata.validate() - - # R Check if Organization already exists - found = self._get_organization_by_id(org_id)[0] - if found: - raise Exception( - "\nOrganization with id={} already exists!\n".format(org_id)) - - members = self.get_members_from_args() - - ipfs_metadata_uri = publish_file_in_ipfs( - self._get_ipfs_client(), metadata_file, False) - params = [type_converter("bytes32")( - org_id), hash_to_bytesuri(ipfs_metadata_uri), members] - self._printout("Creating transaction to create organization name={} id={}\n".format( - org_metadata.org_name, org_id)) - self.transact_contract_command( - "Registry", "createOrganization", params) - self._printout("id:\n%s" % org_id) - - def delete(self): - org_id = self.args.org_id - # Check if Organization exists - (found, _, org_name, _, _, _) = self._get_organization_by_id(org_id) - self.error_organization_not_found(org_id, found) - - self._printout("Creating transaction to delete organization with name={} id={}".format( - org_name, org_id)) - try: - self.transact_contract_command("Registry", "deleteOrganization", [ - type_converter("bytes32")(org_id)]) - except Exception as e: - self._printerr( - "\nTransaction error!\nHINT: Check if you are the owner of organization with id={}\n".format(org_id)) - raise - - def update_metadata(self): - metadata_file = self.args.metadata_file - - try: - with open(metadata_file, 'r') as f: - org_metadata = OrganizationMetadata.from_json(json.load(f)) - except Exception as e: - print( - "Organization metadata json file not found ,Please check --metadata-file path ") - raise e - # validate the metadata before updating - - org_id = self.args.org_id - existing_registry_org_metadata = self._get_organization_metadata_from_registry( - org_id) - org_metadata.validate(existing_registry_org_metadata) - - # Check if Organization already exists - found = self._get_organization_by_id(org_id)[0] - if not found: - raise Exception( - "\nOrganization with id={} does not exists!\n".format(org_id)) - - ipfs_metadata_uri = publish_file_in_ipfs( - self._get_ipfs_client(), metadata_file, False) - params = [type_converter("bytes32")( - org_id), hash_to_bytesuri(ipfs_metadata_uri)] - self._printout( - "Creating transaction to create organization name={} id={}\n".format(org_metadata.org_name, org_id)) - self.transact_contract_command( - "Registry", "changeOrganizationMetadataURI", params) - self._printout("id:\n%s" % org_id) - - def list_services(self): - org_id = self.args.org_id - (found, org_service_list) = self.call_contract_command("Registry", "listServicesForOrganization", - [type_converter("bytes32")(org_id)]) - self.error_organization_not_found(org_id, found) - if org_service_list: - self._printout("\nList of {}'s Services:".format(org_id)) - for idx, org_service in enumerate(org_service_list): - self._printout("- {}".format(bytes32_to_str(org_service))) - else: - self._printout( - "Organization with id={} exists but has no registered services.".format(org_id)) - - def change_owner(self): - org_id = self.args.org_id - # Check if Organization exists - (found, _, _, owner, _, _) = self._get_organization_by_id(org_id) - self.error_organization_not_found(org_id, found) - - new_owner = self.args.owner - if not web3.Web3.is_checksum_address(new_owner): - raise Exception( - "New owner account %s is not a valid Ethereum checksum address" % new_owner) - - if new_owner.lower() == owner.lower(): - raise Exception( - "\n{} is the owner of Organization with id={}!\n".format(new_owner, org_id)) - - self._printout( - "Creating transaction to change organization {}'s owner...\n".format(org_id)) - try: - self.transact_contract_command("Registry", "changeOrganizationOwner", - [type_converter("bytes32")(org_id), self.args.owner]) - except Exception as e: - self._printerr( - "\nTransaction error!\nHINT: Check if you are the owner of {}\n".format(org_id)) - raise - - def add_members(self): - org_id = self.args.org_id - # Check if Organization exists and member is not part of it - (found, _, _, _, members, _) = self._get_organization_by_id(org_id) - self.error_organization_not_found(org_id, found) - - members = [member.lower() for member in members] - add_members = [] - for add_member in self.get_members_from_args(): - if add_member.lower() in members: - self._printout( - "{} is already a member of organization {}".format(add_member, org_id)) - else: - add_members.append(add_member) - - if not add_members: - self._printout("No member was added to {}!\n".format(org_id)) - return - - params = [type_converter("bytes32")(org_id), add_members] - self._printout( - "Creating transaction to add {} members into organization {}...\n".format(len(add_members), org_id)) - try: - self.transact_contract_command( - "Registry", "addOrganizationMembers", params) - except Exception as e: - self._printerr( - "\nTransaction error!\nHINT: Check if you are the owner of {}\n".format(org_id)) - raise - - def rem_members(self): - org_id = self.args.org_id - # Check if Organization exists and member is part of it - (found, _, _, _, members, _) = self._get_organization_by_id(org_id) - self.error_organization_not_found(org_id, found) - - members = [member.lower() for member in members] - rem_members = [] - for rem_member in self.get_members_from_args(): - if rem_member.lower() not in members: - self._printout( - "{} is not a member of organization {}".format(rem_member, org_id)) - else: - rem_members.append(rem_member) - - if not rem_members: - self._printout("No member was removed from {}!\n".format(org_id)) - return - - params = [type_converter("bytes32")(org_id), rem_members] - self._printout( - "Creating transaction to remove {} members from organization with id={}...\n".format(len(rem_members), - org_id)) - try: - self.transact_contract_command( - "Registry", "removeOrganizationMembers", params) - except Exception as e: - self._printerr( - "\nTransaction error!\nHINT: Check if you are the owner of {}\n".format(org_id)) - raise - - def list_my(self): - """ Find organization that has the current identity as the owner or as the member """ - org_list = self.call_contract_command( - "Registry", "listOrganizations", []) - - rez_owner = [] - rez_member = [] - for idx, org_id in enumerate(org_list): - (found, org_id, org_name, owner, members, serviceNames) = self.call_contract_command( - "Registry", "getOrganizationById", [org_id]) - if not found: - raise Exception( - "Organization was removed during this call. Please retry.") - if self.ident.address == owner: - rez_owner.append((org_name, bytes32_to_str(org_id))) - - if self.ident.address in members: - rez_member.append((org_name, bytes32_to_str(org_id))) - - if rez_owner: - self._printout("# Organizations you are the owner of") - self._printout("# OrgName OrgId") - for n, i in rez_owner: - self._printout("%s %s" % (n, i)) - - if rez_member: - self._printout("# Organizations you are the member of") - self._printout("# OrgName OrgId") - for n, i in rez_member: - self._printout("%s %s" % (n, i)) - - def metadata_add_asset_to_ipfs(self): - metadata_file = self.args.metadata_file - org_metadata = OrganizationMetadata.from_file(metadata_file) - asset_file_ipfs_hash_base58 = publish_file_in_ipfs(self._get_ipfs_client(), - self.args.asset_file_path) - - org_metadata.add_asset(asset_file_ipfs_hash_base58, self.args.asset_type) - org_metadata.save_pretty(self.args.metadata_file) - - def metadata_remove_assets_of_a_given_type(self): - metadata_file = self.args.metadata_file - org_metadata = OrganizationMetadata.from_file(metadata_file) - org_metadata.remove_assets(self.args.asset_type) - org_metadata.save_pretty(self.args.metadata_file) - - def metadata_remove_all_assets(self): - metadata_file = self.args.metadata_file - org_metadata = OrganizationMetadata.from_file(metadata_file) - org_metadata.remove_all_assets() - org_metadata.save_pretty(self.args.metadata_file) - - def metadata_add_description(self): - description = self.args.description - url = self.args.url - short_description = self.args.short_description - metadata_file = self.args.metadata_file - org_metadata = OrganizationMetadata.from_file(metadata_file) - if description: - org_metadata.add_description(description) - if short_description: - org_metadata.add_short_description(short_description) - if url: - org_metadata.add_url(url) - if description is None and url is None and short_description is None: - raise Exception("No attributes are given") - org_metadata.save_pretty(metadata_file) - - def metadata_add_contact(self): - args = self.args.__dict__ - metadata_file = args["metadata_file"] - contact_type = args.get("contact_type", None) - phone = args.get("phone", None) - email = args.get("email", None) - if phone is None and email is None: - self._printout("email and phone both can not be empty") - else: - org_metadata = OrganizationMetadata.from_file(metadata_file) - org_metadata.add_contact(contact_type, phone, email) - org_metadata.save_pretty(metadata_file) - - def metadata_remove_contact_by_type(self): - metadata_file = self.args.metadata_file - contact_type = self.args.contact_type - org_metadata = OrganizationMetadata.from_file(metadata_file) - org_metadata.remove_contact_by_type(contact_type) - org_metadata.save_pretty(metadata_file) - - def metadata_remove_all_contacts(self): - metadata_file = self.args.metadata_file - org_metadata = OrganizationMetadata.from_file(metadata_file) - org_metadata.remove_all_contacts() - org_metadata.save_pretty(metadata_file) diff --git a/snet/sdk/commands/mpe_service.py b/snet/sdk/commands/mpe_service.py deleted file mode 100644 index 2a21b60..0000000 --- a/snet/sdk/commands/mpe_service.py +++ /dev/null @@ -1,559 +0,0 @@ -import json -from collections import defaultdict -from pathlib import Path -from re import search -from sys import exit - -from grpc_health.v1 import health_pb2 as heartb_pb2 -from grpc_health.v1 import health_pb2_grpc as heartb_pb2_grpc -from jsonschema import validate, ValidationError - -from snet.sdk.commands.commands import BlockchainCommand -from snet.sdk.metadata.organization import OrganizationMetadata -from snet.sdk.metadata.service import MPEServiceMetadata, load_mpe_service_metadata, mpe_service_metadata_from_json -from snet.sdk.utils import ipfs_utils -from snet.sdk.utils.utils import is_valid_url, open_grpc_channel, type_converter - - -class MPEServiceCommand(BlockchainCommand): - - def publish_proto_in_ipfs(self): - """ Publish proto files in ipfs and print hash """ - ipfs_hash_base58 = ipfs_utils.publish_proto_in_ipfs( - self._get_ipfs_client(), self.args.protodir) - self._printout(ipfs_hash_base58) - - def service_metadata_init(self): - """Utility for creating a service metadata file. - - CLI questionnaire for service metadata creation. Creates a `service_metadata.json` - (if file name is not set) with values entered by the user in the questionnaire utility. - - Mandatory args: - display_name: Display name of the service. - org_id: Organization ID the service would be assosciated with. - protodir_path: Directory containing protobuf files. - groups: Payment groups supported by the organization (default: `default_group`). If multiple - payment groups, ask user for entry. - endpoints: Storage end points for the clients to connect. - daemon_addresses: Ethereum public addresses of daemon in given payment group of service. - - Optional args: - url: Service user guide resource. - long_description: Long description of service. - short_description: Service overview. - contributors: Contributor name and email-id. - file_name: Service metdadata filename. - """ - print("This utility will walk you through creating the service metadata file.", - "It only covers the most common items and tries to guess sensible defaults.", - "", - "See `snet service metadata-init-utility -h` on how to use this utility.", - "", - "Press ^C at any time to quit.", sep='\n') - try: - metadata = MPEServiceMetadata() - while True: - display_name = input("display name: ").strip() - if display_name == "": - print("display name is required.") - else: - break - # Find number of payment groups available for organization - # If only 1, set `default_group` as payment group - while True: - org_id = input(f"organization id `{display_name}` service would be linked to: ").strip() - while org_id == "": - org_id = input(f"organization id required: ").strip() - try: - org_metadata = self._get_organization_metadata_from_registry(org_id) - no_of_groups = len(org_metadata.groups) - break - except Exception: - print(f"`{org_id}` is invalid.") - while True: - try: - protodir_path = input("protodir path: ") - model_ipfs_hash_base58 = ipfs_utils.publish_proto_in_ipfs(self._get_ipfs_client(), protodir_path) - break - except Exception: - print(f'Invalid path: "{protodir_path}"') - if no_of_groups == 1: - metadata.group_init('default_group') - else: - while input("Add group? [y/n] ") == 'y': - metadata.group_init(input('group name: ')) - metadata.add_description() - metadata.add_contributor(input('Enter contributor name: '), input('Enter contributor email: ')) - while input('Add another contributor? [y/n] ').lower() == 'y': - metadata.add_contributor(input('Enter contributor name '), input('Enter contributor email: ')) - mpe_address = self.get_mpe_address() - - metadata.set_simple_field('model_ipfs_hash', model_ipfs_hash_base58) - metadata.set_simple_field('mpe_address', mpe_address) - metadata.set_simple_field('display_name', display_name) - print('', '', json.dumps(metadata.m, indent=2), sep='\n') - print("Are you sure you want to create? [y/n] ", end='') - if input() == 'y': - file_name = input(f"Choose file name: (service_metadata) ") or 'service_metadata' - file_name += '.json' - metadata.save_pretty(file_name) - print(f"{file_name} created.") - else: - exit("ABORTED.") - except KeyboardInterrupt: - exit("\n`snet service metadata-init-utility` CANCELLED.") - - def publish_proto_metadata_init(self): - model_ipfs_hash_base58 = ipfs_utils.publish_proto_in_ipfs( - self._get_ipfs_client(), self.args.protodir) - - metadata = MPEServiceMetadata() - mpe_address = self.get_mpe_address() - metadata.set_simple_field("model_ipfs_hash", model_ipfs_hash_base58) - metadata.set_simple_field("mpe_address", mpe_address) - metadata.set_simple_field("display_name", self.args.display_name) - metadata.set_simple_field("encoding", self.args.encoding) - metadata.set_simple_field("service_type", self.args.service_type) - - if self.args.group_name: - metadata.add_group(self.args.group_name) - if self.args.endpoints: - for endpoint in self.args.endpoints: - if not is_valid_url(endpoint): - raise Exception(f"Invalid {endpoint} endpoint passed") - metadata.add_endpoint_to_group( - self.args.group_name, endpoint) - if self.args.fixed_price is not None: - metadata.set_fixed_price_in_cogs( - self.args.group_name, self.args.fixed_price) - elif self.args.group_name or self.args.fixed_price: - raise Exception( - "endpoints / fixed price can be attached to a group please pass group_name") - metadata.save_pretty(self.args.metadata_file) - - def publish_proto_metadata_update(self): - """ Publish protobuf model in ipfs and update existing metadata file """ - metadata = load_mpe_service_metadata(self.args.metadata_file) - ipfs_hash_base58 = ipfs_utils.publish_proto_in_ipfs( - self._get_ipfs_client(), self.args.protodir) - metadata.set_simple_field("model_ipfs_hash", ipfs_hash_base58) - metadata.save_pretty(self.args.metadata_file) - - def metadata_set_fixed_price(self): - metadata = load_mpe_service_metadata(self.args.metadata_file) - metadata.set_fixed_price_in_cogs(self.args.group_name, self.args.price) - metadata.save_pretty(self.args.metadata_file) - - def metadata_set_method_price(self): - metadata = load_mpe_service_metadata(self.args.metadata_file) - metadata.set_method_price_in_cogs( - self.args.group_name, self.args.package_name, self.args.service_name, self.args.method, self.args.price) - metadata.save_pretty(self.args.metadata_file) - - def _metadata_add_group(self, metadata): - metadata.add_group(self.args.group_name) - - def metadata_add_group(self): - metadata = load_mpe_service_metadata(self.args.metadata_file) - self._metadata_add_group(metadata) - metadata.save_pretty(self.args.metadata_file) - - def metadata_remove_group(self): - metadata = load_mpe_service_metadata(self.args.metadata_file) - metadata.remove_group(self.args.group_name) - metadata.save_pretty(self.args.metadata_file) - - def metadata_set_free_calls(self): - metadata = load_mpe_service_metadata(self.args.metadata_file) - metadata.set_free_calls_for_group(self.args.group_name, int(self.args.free_calls)) - metadata.save_pretty(self.args.metadata_file) - - def metadata_set_freecall_signer_address(self): - metadata = load_mpe_service_metadata(self.args.metadata_file) - metadata.set_freecall_signer_address(self.args.group_name, self.args.signer_address) - metadata.save_pretty(self.args.metadata_file) - - def metadata_add_daemon_addresses(self): - """ Metadata: add daemon addresses to the group """ - metadata = load_mpe_service_metadata(self.args.metadata_file) - group_name = metadata.get_group_name_nonetrick(self.args.group_name) - for daemon_address in self.args.daemon_addresses: - metadata.add_daemon_address_to_group(group_name, daemon_address) - metadata.save_pretty(self.args.metadata_file) - - def metadata_remove_all_daemon_addresses(self): - """ Metadata: remove all daemon addresses from all groups """ - metadata = load_mpe_service_metadata(self.args.metadata_file) - metadata.remove_all_daemon_addresses_for_group(self.args.group_name) - metadata.save_pretty(self.args.metadata_file) - - def metadata_update_daemon_addresses(self): - """ Metadata: Remove all daemon addresses from the group and add new ones """ - metadata = load_mpe_service_metadata(self.args.metadata_file) - group_name = metadata.get_group_name_nonetrick(self.args.group_name) - metadata.remove_all_daemon_addresses_for_group(group_name) - for daemon_address in self.args.daemon_addresses: - metadata.add_daemon_address_to_group(group_name, daemon_address) - metadata.save_pretty(self.args.metadata_file) - - def metadata_add_endpoints(self): - """ Metadata: add endpoint to the group """ - metadata = load_mpe_service_metadata(self.args.metadata_file) - group_name = metadata.get_group_name_nonetrick(self.args.group_name) - for endpoint in self.args.endpoints: - metadata.add_endpoint_to_group(group_name, endpoint) - metadata.save_pretty(self.args.metadata_file) - - def metadata_remove_all_endpoints(self): - """ Metadata: remove all endpoints from all groups """ - metadata = load_mpe_service_metadata(self.args.metadata_file) - metadata.remove_all_endpoints_for_group(self.args.group_name) - metadata.save_pretty(self.args.metadata_file) - - def metadata_update_endpoints(self): - """ Metadata: Remove all endpoints from the group and add new ones """ - metadata = load_mpe_service_metadata(self.args.metadata_file) - group_name = metadata.get_group_name_nonetrick(self.args.group_name) - metadata.remove_all_endpoints_for_group(group_name) - for endpoint in self.args.endpoints: - metadata.add_endpoint_to_group(group_name, endpoint) - metadata.save_pretty(self.args.metadata_file) - - def metadata_add_asset_to_ipfs(self): - metadata = load_mpe_service_metadata(self.args.metadata_file) - asset_file_ipfs_hash_base58 = ipfs_utils.publish_file_in_ipfs(self._get_ipfs_client(), - self.args.asset_file_path) - metadata.add_asset(asset_file_ipfs_hash_base58, self.args.asset_type) - metadata.save_pretty(self.args.metadata_file) - - def metadata_remove_all_assets(self): - metadata = load_mpe_service_metadata(self.args.metadata_file) - metadata.remove_all_assets() - metadata.save_pretty(self.args.metadata_file) - - def metadata_remove_assets_of_a_given_type(self): - metadata = load_mpe_service_metadata(self.args.metadata_file) - metadata.remove_assets(self.args.asset_type) - metadata.save_pretty(self.args.metadata_file) - - def metadata_add_media(self): - """Metadata: Add new individual media - - Detects media type for files to be added on IPFS, explict declaration for external resources. - """ - metadata = load_mpe_service_metadata(self.args.metadata_file) - # Support endpoints only with SSL Certificate - url_validator = r'https?:\/\/(www\.)?([-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b[-a-zA-Z0-9()@:%_\+.~#?&//=]*)' - # Automatic media type identification - if search(r'^.+\.(jpg|jpeg|png|gif)$', self.args.media_url): - media_type = 'image' - elif search(r'^.+\.(mp4)$', self.args.media_url): - media_type = 'video' - elif search(url_validator, self.args.media_url): - while True: - try: - media_type = input(f"Enter the media type (image, video) present at {self.args.media_url}: ") - except ValueError: - print("Choose only between (image, video).") - else: - if media_type not in ('image', 'video'): - print("Choose only between (image, video).") - else: - break - else: - if search(r'(https:\/\/)?(www\.)+([-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b[-a-zA-Z0-9()@:%_\+.~#?&//=]*)', self.args.media_url): - raise ValueError("Media endpoint supported only for secure sites.") - else: - raise ValueError(f"Entered url '{self.args.media_url}' is invalid.") - file_extension_validator = r'^.+\.(jpg|jpeg|JPG|png|gif|GIF|mp4)$' - # Detect whether to add asset on IPFS or if external resource - if search(file_extension_validator, self.args.media_url): - asset_file_ipfs_hash_base58 = ipfs_utils.publish_file_in_ipfs(self._get_ipfs_client(), self.args.media_url) - metadata.add_media(asset_file_ipfs_hash_base58, media_type, self.args.hero_image) - else: - metadata.add_media(self.args.media_url, media_type, self.args.hero_image) - metadata.save_pretty(self.args.metadata_file) - - def metadata_remove_media(self): - """Metadata: Remove individual media using unique order key.""" - metadata = load_mpe_service_metadata(self.args.metadata_file) - metadata.remove_media(self.args.order) - metadata.save_pretty(self.args.metadata_file) - - def metadata_remove_all_media(self): - """Metadata: Remove all individual media.""" - metadata = load_mpe_service_metadata(self.args.metadata_file) - metadata.remove_all_media() - metadata.save_pretty(self.args.metadata_file) - - def metadata_swap_media_order(self): - """Metadata: Swap order of two individual media given 'from' and 'to'.""" - metadata = load_mpe_service_metadata(self.args.metadata_file) - metadata.swap_media_order(self.args.move_from, self.args.move_to) - metadata.save_pretty(self.args.metadata_file) - - def metadata_change_media_order(self): - """Metadata: REPL to change order of all individual media.""" - metadata = load_mpe_service_metadata(self.args.metadata_file) - metadata.change_media_order() - metadata.save_pretty(self.args.metadata_file) - - def metadata_add_contributor(self): - metadata = load_mpe_service_metadata(self.args.metadata_file) - metadata.add_contributor(self.args.name, self.args.email_id) - metadata.save_pretty(self.args.metadata_file) - - def metadata_remove_contributor(self): - metadata = load_mpe_service_metadata(self.args.metadata_file) - metadata.remove_contributor_by_email(self.args.email_id) - metadata.save_pretty(self.args.metadata_file) - - def metadata_add_description(self): - """ Metadata: add description """ - service_description = {} - if self.args.json: - service_description = json.loads(self.args.json) - if self.args.url: - if "url" in service_description: - raise Exception( - "json service description already contains url field") - service_description["url"] = self.args.url - if self.args.description: - if "description" in service_description: - raise Exception( - "json service description already contains description field") - service_description["description"] = self.args.description - if self.args.short_description: - if "short_description" in service_description: - raise Exception( - "json service description already contains short description field") - if len(self.args.short_description) > 180: - raise Exception( - "size of short description must be less than 181 characters" - ) - service_description["short_description"] = self.args.short_description - metadata = load_mpe_service_metadata(self.args.metadata_file) - # merge with old service_description if necessary - if "service_description" in metadata: - service_description = { - **metadata["service_description"], **service_description} - metadata.set_simple_field("service_description", service_description) - metadata.save_pretty(self.args.metadata_file) - - def metadata_validate(self): - """Validates the service metadata file for structure and input consistency. - - Validates whether service metadata (`service_metadata.json` if not provided as argument) is consistent - with the schema provided in `service_schema` present in `snet_cli/snet/snet_cli/resources.` - - Args: - metadata_file: Option provided through the command line. (default: service_metadata.json) - service_schema: Schema of a consistent service metadata file. - - Raises: - ValidationError: Inconsistent service metadata structure or missing values. - docs -> Handling ValidationErrors (https://python-jsonschema.readthedocs.io/en/stable/errors/) - """ - # Set path to `service_schema` stored in the `resources` directory from cwd of `mpe_service.py` - current_path = Path(__file__).parent - relative_path = '../../snet/snet_cli/resources/service_schema' - path_to_schema = (current_path / relative_path).resolve() - with open(path_to_schema, 'r') as f: - schema = json.load(f) - metadata = load_mpe_service_metadata(self.args.metadata_file) - try: - validate(instance=metadata.m, schema=schema) - except Exception as e: - docs = "http://snet-cli-docs.singularitynet.io/service.html" - error_message = f"\nVisit {docs} for more information." - if e.validator == 'required': - raise ValidationError(e.message + error_message) - elif e.validator == 'minLength': - raise ValidationError(f"`{e.path[-1]}` -> cannot be empty." + error_message) - elif e.validator == 'minItems': - raise ValidationError(f"`{e.path[-1]}` -> minimum 1 item required." + error_message) - elif e.validator == 'type': - raise ValidationError(f"`{e.path[-1]}` -> {e.message}" + error_message) - elif e.validator == 'enum': - raise ValidationError(f"`{e.path[-1]}` -> {e.message}" + error_message) - elif e.validator == 'additionalProperties': - if len(e.path) != 0: - raise ValidationError(f"{e.message} in `{e.path[-2]}`." + error_message) - else: - raise ValidationError(f"{e.message} in main object." + error_message) - else: - exit("OK. Ready to publish.") - - def _publish_metadata_in_ipfs(self, metadata_file): - metadata = load_mpe_service_metadata(metadata_file) - mpe_address = self.get_mpe_address() - if self.args.update_mpe_address: - metadata.set_simple_field("mpe_address", mpe_address) - metadata.save_pretty(self.args.metadata_file) - - if mpe_address.lower() != metadata["mpe_address"].lower(): - raise Exception( - "\n\nmpe_address in metadata does not correspond to the current MultiPartyEscrow contract address\n" + - "You have two possibilities:\n" + - "1. You can use --multipartyescrow-at to set current mpe address\n" + - "2. You can use --update-mpe-address parameter to update mpe_address in metadata before publishing it\n") - return self._get_ipfs_client().add_bytes(metadata.get_json().encode("utf-8")) - - def publish_metadata_in_ipfs(self): - """ Publish metadata in ipfs and print hash """ - self._printout(self._publish_metadata_in_ipfs(self.args.metadata_file)) - - #def _get_converted_tags(self): - # return [type_converter("bytes32")(tag) for tag in self.args.tags] - - def _get_organization_metadata_from_registry(self, org_id): - rez = self._get_organization_registration(org_id) - metadata_hash = ipfs_utils.bytesuri_to_hash(rez["orgMetadataURI"]) - metadata = ipfs_utils.get_from_ipfs_and_checkhash( - self._get_ipfs_client(), metadata_hash) - metadata = metadata.decode("utf-8") - return OrganizationMetadata.from_json(json.loads(metadata)) - - def _get_organization_registration(self, org_id): - params = [type_converter("bytes32")(org_id)] - result = self.call_contract_command( - "Registry", "getOrganizationById", params) - if result[0] == False: - raise Exception("Cannot find Organization with id=%s" % ( - self.args.org_id)) - return {"orgMetadataURI": result[2]} - - def _validate_service_group_with_org_group_and_update_group_id(self, org_id, metadata_file): - org_metadata = self._get_organization_metadata_from_registry(org_id) - new_service_metadata = load_mpe_service_metadata(metadata_file) - org_groups = {} - for group in org_metadata.groups: - org_groups[group.group_name] = group - - for group in new_service_metadata.m["groups"]: - if group["group_name"] in org_groups: - group["group_id"] = org_groups[group["group_name"]].group_id - new_service_metadata.save_pretty(metadata_file) - else: - raise Exception( - "Group name %s does not exist in organization" % group["group_name"]) - - def publish_service_with_metadata(self): - self._validate_service_group_with_org_group_and_update_group_id( - self.args.org_id, self.args.metadata_file) - metadata_uri = ipfs_utils.hash_to_bytesuri( - self._publish_metadata_in_ipfs(self.args.metadata_file)) - #tags = self._get_converted_tags() - params = [type_converter("bytes32")(self.args.org_id), type_converter( - "bytes32")(self.args.service_id), metadata_uri] - self.transact_contract_command( - "Registry", "createServiceRegistration", params) - - def publish_metadata_in_ipfs_and_update_registration(self): - # first we check that we do not change payment_address or group_id in existed payment groups - self._validate_service_group_with_org_group_and_update_group_id( - self.args.org_id, self.args.metadata_file) - metadata_uri = ipfs_utils.hash_to_bytesuri( - self._publish_metadata_in_ipfs(self.args.metadata_file)) - params = [type_converter("bytes32")(self.args.org_id), type_converter( - "bytes32")(self.args.service_id), metadata_uri] - self.transact_contract_command( - "Registry", "updateServiceRegistration", params) - - #def _get_params_for_tags_update(self): - # tags = self._get_converted_tags() - # params = [type_converter("bytes32")(self.args.org_id), type_converter( - # "bytes32")(self.args.service_id), tags] - # return params - - def metadata_add_tags(self): - metadata = load_mpe_service_metadata(self.args.metadata_file) - [metadata.add_tag(tag) for tag in self.args.tags] - metadata.save_pretty(self.args.metadata_file) - - def metadata_remove_tags(self): - metadata = load_mpe_service_metadata(self.args.metadata_file) - [metadata.remove_tag(tag) for tag in self.args.tags] - metadata.save_pretty(self.args.metadata_file) - - def update_registration_add_tags(self): - self._printout("This command has been deprecated. Please use `snet service metadata-add-tags` instead") - - def update_registration_remove_tags(self): - self._printout("This command has been deprecated. Please use `snet service metadata-remove-tags` instead") - - def _get_service_registration(self): - params = [type_converter("bytes32")(self.args.org_id), type_converter( - "bytes32")(self.args.service_id)] - rez = self.call_contract_command( - "Registry", "getServiceRegistrationById", params) - if rez[0] == False: - raise Exception("Cannot find Service with id=%s in Organization with id=%s" % ( - self.args.service_id, self.args.org_id)) - return {"metadataURI": rez[2]} - - def _get_service_metadata_from_registry(self): - rez = self._get_service_registration() - metadata_hash = ipfs_utils.bytesuri_to_hash(rez["metadataURI"]) - metadata = ipfs_utils.get_from_ipfs_and_checkhash( - self._get_ipfs_client(), metadata_hash) - metadata = metadata.decode("utf-8") - metadata = mpe_service_metadata_from_json(metadata) - return metadata - - def print_service_metadata_from_registry(self): - metadata = self._get_service_metadata_from_registry() - self._printout(metadata.get_json_pretty()) - - def _service_status(self, url, secure=True): - try: - channel = open_grpc_channel(endpoint=url) - stub = heartb_pb2_grpc.HealthStub(channel) - response = stub.Check( - heartb_pb2.HealthCheckRequest(service=""), timeout=10) - if response != None and response.status == 1: - return True - return False - except Exception as e: - return False - - def print_service_status(self): - metadata = self._get_service_metadata_from_registry() - if self.args.group_name != None: - groups = {self.args.group_name: metadata.get_all_endpoints_for_group( - self.args.group_name)} - else: - groups = metadata.get_all_group_endpoints() - service_status = defaultdict(list) - for name, group_endpoints in groups.items(): - for endpoint in group_endpoints: - status = "Available" if self._service_status( - url=endpoint) else "Not Available" - service_status[name].append( - {"endpoint": endpoint, "status": status}) - if service_status == {}: - self._printout( - "Error: No endpoints found to check service status.") - return - self._pprint(service_status) - - def print_service_tags_from_registry(self): - metadata = self._get_service_metadata_from_registry() - self._printout(" ".join(metadata.get_tags())) - - def extract_service_api_from_metadata(self): - metadata = load_mpe_service_metadata(self.args.metadata_file) - ipfs_utils.safe_extract_proto_from_ipfs(self._get_ipfs_client( - ), metadata["model_ipfs_hash"], self.args.protodir) - - def extract_service_api_from_registry(self): - metadata = self._get_service_metadata_from_registry() - ipfs_utils.safe_extract_proto_from_ipfs(self._get_ipfs_client( - ), metadata["model_ipfs_hash"], self.args.protodir) - - def delete_service_registration(self): - params = [type_converter("bytes32")(self.args.org_id), type_converter( - "bytes32")(self.args.service_id)] - self.transact_contract_command( - "Registry", "deleteServiceRegistration", params) diff --git a/snet/sdk/commands/sdk_command.py b/snet/sdk/commands/sdk_command.py deleted file mode 100644 index 903f474..0000000 --- a/snet/sdk/commands/sdk_command.py +++ /dev/null @@ -1,39 +0,0 @@ -import os -from pathlib import Path, PurePath - -from snet.sdk.utils.ipfs_utils import safe_extract_proto_from_ipfs -from snet.sdk.utils.utils import compile_proto -from snet.sdk.commands.mpe_service import MPEServiceCommand - - -class SDKCommand(MPEServiceCommand): - def generate_client_library(self): - - if os.path.isabs(self.args.protodir): - client_libraries_base_dir_path = PurePath(self.args.protodir) - else: - cur_dir_path = PurePath(os.getcwd()) - client_libraries_base_dir_path = cur_dir_path.joinpath(self.args.protodir) - - os.makedirs(client_libraries_base_dir_path, exist_ok=True) - - # Create service client libraries path - library_language = self.args.language - library_org_id = self.args.org_id - library_service_id = self.args.service_id - - library_dir_path = client_libraries_base_dir_path.joinpath(library_org_id, library_service_id, library_language) - - metadata = self._get_service_metadata_from_registry() - model_ipfs_hash = metadata["model_ipfs_hash"] - - # Receive proto files - safe_extract_proto_from_ipfs(self._get_ipfs_client(), model_ipfs_hash, library_dir_path) - - # Compile proto files - compile_proto(Path(library_dir_path), library_dir_path, target_language=self.args.language) - - self._printout( - 'client libraries for service with id "{}" in org with id "{}" generated at {}'.format(library_service_id, - library_org_id, - library_dir_path)) diff --git a/snet/sdk/contract.py b/snet/sdk/contract.py deleted file mode 100644 index 6c79d1f..0000000 --- a/snet/sdk/contract.py +++ /dev/null @@ -1,27 +0,0 @@ -class Contract: - def __init__(self, w3, address, abi): - self.w3 = w3 - self.contract = self.w3.eth.contract(address=self.w3.to_checksum_address(address), abi=abi) - self.abi = abi - - def call(self, function_name, *positional_inputs, **named_inputs): - return getattr(self.contract.functions, function_name)(*positional_inputs, **named_inputs).call() - - def build_transaction(self, function_name, from_address, gas_price, *positional_inputs, **named_inputs): - nonce = self.w3.eth.get_transaction_count(from_address) - chain_id = self.w3.net.version - return getattr(self.contract.functions, function_name)(*positional_inputs, **named_inputs).build_transaction({ - "from": from_address, - "nonce": nonce, - "gasPrice": gas_price, - "chainId": int(chain_id) - }) - - def process_receipt(self, receipt): - events = [] - - contract_events = map(lambda e: e["name"], filter(lambda e: e["type"] == "event", self.abi)) - for contract_event in contract_events: - events.extend(getattr(self.contract.events, contract_event)().process_receipt(receipt)) - - return events diff --git a/snet/sdk/identity.py b/snet/sdk/identity.py deleted file mode 100644 index 18b4927..0000000 --- a/snet/sdk/identity.py +++ /dev/null @@ -1,341 +0,0 @@ -import abc -import json -import struct -import time -import getpass - -import rlp -from eth_account import Account -from eth_account.messages import defunct_hash_message -from eth_account._utils.legacy_transactions import encode_transaction, \ - UnsignedTransaction, serializable_unsigned_transaction_from_dict -from ledgerblue.comm import getDongle -from ledgerblue.commException import CommException -from trezorlib.client import TrezorClient -from trezorlib import messages as proto -from trezorlib.transport.hid import HidTransport - - -from snet.sdk.utils.utils import get_address_from_private, normalize_private_key - -BIP32_HARDEN = 0x80000000 - - -class IdentityProvider(abc.ABC): - @abc.abstractmethod - def get_address(self): - raise NotImplementedError() - - @abc.abstractmethod - def transact(self, transaction, out_f): - raise NotImplementedError() - - @abc.abstractmethod - def sign_message_after_solidity_keccak(self, message): - raise NotImplementedError() - - -class KeyIdentityProvider(IdentityProvider): - def __init__(self, w3, private_key): - self.w3 = w3 - self.private_key = normalize_private_key(private_key) - self.address = get_address_from_private(self.private_key) - - def get_address(self): - return self.address - - def transact(self, transaction, out_f): - raw_transaction = sign_transaction_with_private_key( - self.w3, self.private_key, transaction) - return send_and_wait_for_transaction(raw_transaction, self.w3, out_f) - - def sign_message_after_solidity_keccak(self, message): - return sign_message_with_private_key(self.w3, self.private_key, message) - - -class KeyStoreIdentityProvider(IdentityProvider): - def __init__(self, w3, path_to_keystore): - self.w3 = w3 - try: - with open(path_to_keystore) as keyfile: - encrypted_key = keyfile.read() - self.address = self.w3.to_checksum_address( - json.loads(encrypted_key)["address"]) - self.path_to_keystore = path_to_keystore - self.private_key = None - except CommException: - raise RuntimeError( - "Error decrypting your keystore. Are you sure it is the correct path?") - - def get_address(self): - return self.address - - def transact(self, transaction, out_f): - - if self.private_key is None: - self.private_key = unlock_keystore_with_password( - self.w3, self.path_to_keystore) - - raw_transaction = sign_transaction_with_private_key( - self.w3, self.private_key, transaction) - return send_and_wait_for_transaction(raw_transaction, self.w3, out_f) - - def sign_message_after_solidity_keccak(self, message): - - if self.private_key is None: - self.private_key = unlock_keystore_with_password( - self.w3, self.path_to_keystore) - - return sign_message_with_private_key(self.w3, self.private_key, message) - - -class RpcIdentityProvider(IdentityProvider): - def __init__(self, w3, index): - self.w3 = w3 - self.address = self.w3.personal.listAccounts[index] - - def get_address(self): - return self.address - - def transact(self, transaction, out_f): - print("Submitting transaction...\n", file=out_f) - txn_hash = self.w3.eth.sendTransaction(transaction) - return send_and_wait_for_transaction_receipt(txn_hash, self.w3) - - def sign_message_after_solidity_keccak(self, message): - return self.w3.eth.sign(self.get_address(), message) - - -class MnemonicIdentityProvider(IdentityProvider): - def __init__(self, w3, mnemonic, index): - self.w3 = w3 - Account.enable_unaudited_hdwallet_features() - account = Account.from_mnemonic(mnemonic, account_path=f"m/44'/60'/0'/0/{index}") - self.private_key = account.key.hex() - self.address = account.address - - def get_address(self): - return self.address - - def transact(self, transaction, out_f): - raw_transaction = sign_transaction_with_private_key( - self.w3, self.private_key, transaction) - return send_and_wait_for_transaction(raw_transaction, self.w3, out_f) - - def sign_message_after_solidity_keccak(self, message): - return sign_message_with_private_key(self.w3, self.private_key, message) - - -class TrezorIdentityProvider(IdentityProvider): - def __init__(self, w3, index): - self.w3 = w3 - self.client = TrezorClient(HidTransport.enumerate()[0]) - self.index = index - self.address = self.w3.to_checksum_address( - "0x" + bytes(self.client.ethereum_get_address([44 + BIP32_HARDEN, - 60 + BIP32_HARDEN, - BIP32_HARDEN, 0, - index])).hex()) - - def get_address(self): - return self.address - - def transact(self, transaction, out_f): - print("Sending transaction to trezor for signature...\n", file=out_f) - signature = self.client.ethereum_sign_tx(n=[44 + BIP32_HARDEN, 60 + BIP32_HARDEN, - BIP32_HARDEN, 0, self.index], - nonce=transaction["nonce"], - gas_price=transaction["gasPrice"], - gas_limit=transaction["gas"], - to=bytearray.fromhex( - transaction["to"][2:]), - value=transaction["value"], - data=bytearray.fromhex(transaction["data"][2:])) - - transaction.pop("from") - unsigned_transaction = serializable_unsigned_transaction_from_dict( - transaction) - raw_transaction = encode_transaction(unsigned_transaction, - vrs=(signature[0], - int(signature[1].hex(), 16), - int(signature[2].hex(), 16))) - return send_and_wait_for_transaction(raw_transaction, self.w3, out_f) - - def sign_message_after_solidity_keccak(self, message): - n = self.client._convert_prime([44 + BIP32_HARDEN, - 60 + BIP32_HARDEN, - BIP32_HARDEN, - 0, - self.index]) - return self.client.call(proto.EthereumSignMessage(address_n=n, message=message)).signature - - -class LedgerIdentityProvider(IdentityProvider): - GET_ADDRESS_OP = b"\xe0\x02\x00\x00" - SIGN_TX_OP = b"\xe0\x04\x00\x00" - SIGN_TX_OP_CONT = b"\xe0\x04\x80\x00" - SIGN_MESSAGE_OP = b"\xe0\x08\x00\x00" - - def __init__(self, w3, index): - self.w3 = w3 - try: - self.dongle = getDongle(False) - except CommException: - raise RuntimeError( - "Received commException from Ledger. Are you sure your device is plugged in?") - self.dongle_path = parse_bip32_path("44'/60'/0'/0/{}".format(index)) - apdu = LedgerIdentityProvider.GET_ADDRESS_OP - apdu += bytearray([len(self.dongle_path) + 1, - int(len(self.dongle_path) / 4)]) + self.dongle_path - try: - result = self.dongle.exchange(apdu) - except CommException: - raise RuntimeError("Received commException from Ledger. Are you sure your device is unlocked and the " - "Ethereum app is running?") - - offset = 1 + result[0] - self.address = self.w3.to_checksum_address(bytes(result[offset + 1: offset + 1 + result[offset]]) - .decode("utf-8")) - - def get_address(self): - return self.address - - def transact(self, transaction, out_f): - tx = UnsignedTransaction( - nonce=transaction["nonce"], - gasPrice=transaction["gasPrice"], - gas=transaction["gas"], - to=bytes(bytearray.fromhex(transaction["to"][2:])), - value=transaction["value"], - data=bytes(bytearray.fromhex(transaction["data"][2:])) - ) - - encoded_tx = rlp.encode(tx, UnsignedTransaction) - - overflow = len(self.dongle_path) + 1 + len(encoded_tx) - 255 - - if overflow > 0: - encoded_tx, remaining_tx = encoded_tx[:- - overflow], encoded_tx[-overflow:] - - apdu = LedgerIdentityProvider.SIGN_TX_OP - apdu += bytearray([len(self.dongle_path) + 1 + - len(encoded_tx), int(len(self.dongle_path) / 4)]) - apdu += self.dongle_path + encoded_tx - try: - print("Sending transaction to Ledger for signature...\n", file=out_f) - result = self.dongle.exchange(apdu) - while overflow > 0: - encoded_tx = remaining_tx - overflow = len(encoded_tx) - 255 - - if overflow > 0: - encoded_tx, remaining_tx = encoded_tx[:- - overflow], encoded_tx[-overflow:] - - apdu = LedgerIdentityProvider.SIGN_TX_OP_CONT - apdu += bytearray([len(encoded_tx)]) - apdu += encoded_tx - result = self.dongle.exchange(apdu) - except CommException as e: - if e.sw == 27013: - raise RuntimeError("Transaction denied from Ledger by user") - raise RuntimeError(e.message, e.sw) - - transaction.pop("from") - unsigned_transaction = serializable_unsigned_transaction_from_dict( - transaction) - raw_transaction = encode_transaction(unsigned_transaction, - vrs=(result[0], - int.from_bytes( - result[1:33], byteorder="big"), - int.from_bytes(result[33:65], byteorder="big"))) - return send_and_wait_for_transaction(raw_transaction, self.w3, out_f) - - def sign_message_after_solidity_keccak(self, message): - apdu = LedgerIdentityProvider.SIGN_MESSAGE_OP - apdu += bytearray([len(self.dongle_path) + 1 + - len(message) + 4, int(len(self.dongle_path) / 4)]) - apdu += self.dongle_path + struct.pack(">I", len(message)) + message - try: - result = self.dongle.exchange(apdu) - except CommException: - raise RuntimeError("Received commException from Ledger. Are you sure your device is unlocked and the " - "Ethereum app is running?") - - return result[1:] + result[0:1] - - -def send_and_wait_for_transaction_receipt(txn_hash, w3): - # Wait for transaction to be mined - receipt = dict() - while not receipt: - time.sleep(1) - try: - receipt = w3.eth.get_transaction_receipt(txn_hash) - if receipt and "blockHash" in receipt and receipt["blockHash"] is None: - receipt = dict() - except: - receipt = dict() - return receipt - - -def send_and_wait_for_transaction(raw_transaction, w3, out_f): - print("Submitting transaction...\n", file=out_f) - txn_hash = w3.eth.send_raw_transaction(raw_transaction) - return send_and_wait_for_transaction_receipt(txn_hash, w3) - - -def parse_bip32_path(path): - if len(path) == 0: - return b"" - result = b"" - elements = path.split('/') - for pathElement in elements: - element = pathElement.split('\'') - if len(element) == 1: - result = result + struct.pack(">I", int(element[0])) - else: - result = result + struct.pack(">I", BIP32_HARDEN | int(element[0])) - return result - - -def get_kws_for_identity_type(identity_type): - SECRET = True - PLAINTEXT = False - - if identity_type == "rpc": - return [("network", PLAINTEXT)] - elif identity_type == "mnemonic": - return [("mnemonic", SECRET)] - elif identity_type == "key": - return [("private_key", SECRET)] - elif identity_type == "trezor": - return [] - elif identity_type == "ledger": - return [] - elif identity_type == "keystore": - return [("keystore_path", PLAINTEXT)] - else: - raise RuntimeError( - "unrecognized identity_type {}".format(identity_type)) - - -def get_identity_types(): - return ["rpc", "mnemonic", "key", "trezor", "ledger", "keystore"] - - -def sign_transaction_with_private_key(w3, private_key, transaction): - return w3.eth.account.sign_transaction(transaction, private_key).rawTransaction - - -def sign_message_with_private_key(w3, private_key, message): - h = defunct_hash_message(message) - return w3.eth.account.signHash(h, private_key).signature - - -def unlock_keystore_with_password(w3, path_to_keystore): - password = getpass.getpass("Password : ") or "" - with open(path_to_keystore) as keyfile: - encrypted_key = keyfile.read() - return w3.eth.account.decrypt(encrypted_key, password) diff --git a/snet/sdk/metadata/__init__.py b/snet/sdk/metadata/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/snet/sdk/metadata/organization.py b/snet/sdk/metadata/organization.py deleted file mode 100644 index 84f9e50..0000000 --- a/snet/sdk/metadata/organization.py +++ /dev/null @@ -1,355 +0,0 @@ -import base64 -from enum import Enum -from json import JSONEncoder -import json - -from snet.sdk.utils.utils import is_valid_url - - -class DefaultEncoder(JSONEncoder): - def default(self, o): - return o.__dict__ - - -class AssetType(Enum): - HERO_IMAGE = "hero_image" - - -class PaymentStorageClient(object): - - def __init__(self, connection_timeout=None, request_timeout="", endpoints=None): - if endpoints is None: - endpoints = [] - self.connection_timeout = connection_timeout - self.request_timeout = request_timeout - self.endpoints = endpoints - - def add_payment_storage_client_details(self, connection_time_out, request_timeout, endpoints): - self.connection_timeout = connection_time_out - self.request_timeout = request_timeout - self.endpoints = endpoints - - @classmethod - def from_json(cls, json_data: dict): - endpoints = json_data["endpoints"] - if endpoints: - for endpoint in endpoints: - if not is_valid_url(endpoint): - raise Exception("Invalid endpoint passed in json file") - return cls(**json_data) - - def validate(self): - if len(self.endpoints) < 1: - raise Exception( - "At least one endpoint is required for payment channel ") - - -class Payment(object): - - def __init__(self, payment_address="", payment_expiration_threshold="", payment_channel_storage_type="", - payment_channel_storage_client=PaymentStorageClient()): - self.payment_address = payment_address - self.payment_expiration_threshold = payment_expiration_threshold - self.payment_channel_storage_type = payment_channel_storage_type - self.payment_channel_storage_client = payment_channel_storage_client - - @classmethod - def from_json(cls, json_data: dict): - payment_channel_storage_client = PaymentStorageClient.from_json( - json_data['payment_channel_storage_client']) - return cls(json_data['payment_address'], json_data['payment_expiration_threshold'], - json_data['payment_channel_storage_type'], payment_channel_storage_client) - - def validate(self): - if self.payment_address is None: - raise Exception("Payment address cannot be null") - if self.payment_channel_storage_type is None: - raise Exception("Payment channel storage type cannot be null") - if self.payment_expiration_threshold is None: - raise Exception("Payment expiration threshold cannot be null") - - if self.payment_channel_storage_client is None: - raise Exception("Payment channel storage client cannot be null") - else: - self.payment_channel_storage_client.validate() - - def update_connection_timeout(self, connection_timeout): - self.payment_channel_storage_client.connection_timeout = connection_timeout - - def update_request_timeout(self, request_timeout): - self.payment_channel_storage_client.request_timeout = request_timeout - - def update_endpoints(self, endpoints): - self.payment_channel_storage_client.endpoints = endpoints - - -class Group(object): - - def __init__(self, group_name="", group_id="", payment=Payment()): - self.group_name = group_name - self.group_id = group_id - self.payment = payment - - @classmethod - def from_json(cls, json_data: dict): - payment = Payment() - if 'payment' in json_data: - payment = Payment.from_json(json_data['payment']) - return cls(json_data['group_name'], json_data['group_id'], payment) - - def add_group_details(self, group_name, group_id, payment): - self.group_name = group_name - self.group_id = group_id - self.payment = payment - - def validate(self): - if self.group_name is None: - raise Exception("group name cannot be null") - if self.group_id is None: - raise Exception("group_id is cannot be null") - - if self.payment is None: - raise Exception( - "payment details cannot be null for group_name %s", self.group_name) - else: - self.payment.validate() - - def update_payment_expiration_threshold(self, payment_expiration_threshold): - self.payment.payment_expiration_threshold = payment_expiration_threshold - - def update_payment_channel_storage_type(self, payment_channel_storage_type): - self.update_payment_channel_storage_type = payment_channel_storage_type - - def update_payment_address(self, payment_address): - self.payment.payment_address = payment_address - - def update_connection_timeout(self, connection_timeout): - self.payment.update_connection_timeout(connection_timeout) - - def update_request_timeout(self, request_timeout): - self.payment.update_request_timeout(request_timeout) - - def update_endpoints(self, endpoints): - self.payment.update_endpoints(endpoints) - - def get_group_id(self, group_name=None): - return base64.b64decode(self.get_group_id_base64(group_name)) - - def get_payment_address(self): - return self.payment.payment_address - - -class OrganizationMetadata(object): - """ - { - "org_name": "organization_name", - "org_id": "org_id1", - org_type: "organization"/"individual", - "contacts": [ - { - "contact_type": "support", - "email_id":"abcd@abcdef.com", - "phone":"1234567890", - }, - { - "contact_type": "dummy", - "email_id":"dummy@abcdef.com", - "phone":"1234567890", - }, - ], - "description": "We do this and that ... Describe your organization here ", - "assets": { - "hero_image": "QmNW2jjz11enwbRrF1mJ2LdaQPeZVEtmKU8Uq7kpEkmXCc/hero_gene-annotation.png" - }, - "groups": [ - { - "group_name": "default_group2", - "group_id": "99ybRIg2wAx55mqVsA6sB4S7WxPQHNKqa4BPu/bhj+U=", - "payment": { - "payment_address": "0x671276c61943A35D5F230d076bDFd91B0c47bF09", - "payment_expiration_threshold": 40320, - "payment_channel_storage_type": "etcd", - "payment_channel_storage_client": { - "connection_timeout": "5s", - "request_timeout": "3s", - "endpoints": [ - "http://127.0.0.1:2379" - ] - } - } - }, - { - "group_name": "default_group2", - "group_id": "99ybRIg2wAx55mqVsA6sB4S7WxPQHNKqa4BPu/bhj+U=", - "payment": { - "payment_address": "0x671276c61943A35D5F230d076bDFd91B0c47bF09", - "payment_expiration_threshold": 40320, - "payment_channel_storage_type": "etcd", - "payment_channel_storage_client": { - "connection_timeout": "5s", - "request_timeout": "3s", - "endpoints": [ - "http://127.0.0.1:2379" - ] - } - } - } - ] - } - """ - - def __init__(self, org_name="", org_id="", org_type="",contacts=[], description={}, - assets={}, groups=[]): - self.org_name = org_name - self.org_id = org_id - self.org_type = org_type - self.description = description - self.assets = assets - self.contacts = contacts - self.groups = groups - - def add_group(self, group): - self.groups.append(group) - - def get_json_pretty(self): - return json.dumps(self, indent=4, cls=DefaultEncoder) - - def save_pretty(self, file_name): - with open(file_name, 'w') as f: - f.write(self.get_json_pretty()) - - @classmethod - def from_json(cls, json_data: dict): - groups = [] - if 'groups' in json_data: - groups = list(map(Group.from_json, json_data["groups"])) - if "contacts" not in json_data: - json_data["contacts"] = [] - if "description" not in json_data: - json_data["description"] = {} - if "assets" not in json_data: - json_data["assets"] = {} - if "org_type" not in json_data: - json_data["org_type"] = "" - return cls( - org_name=json_data['org_name'], - org_id=json_data['org_id'], - org_type=json_data['org_type'], - contacts=json_data['contacts'], - description=json_data['description'], - groups=groups, - assets=json_data['assets'] - ) - - @classmethod - def from_file(cls, filepath): - try: - with open(filepath, 'r') as f: - return OrganizationMetadata.from_json(json.load(f)) - except Exception as e: - print( - "Organization metadata json file not found ,Please check --metadata-file path ") - raise e - - def is_removing_existing_group_from_org(self, current_group_name, existing_registry_metadata_group_names): - if len(existing_registry_metadata_group_names-current_group_name) == 0: - pass - else: - removed_groups = existing_registry_metadata_group_names - current_group_name - raise Exception("Cannot remove existing group from organization as it might be attached" - " to services, groups you are removing are %s" % removed_groups) - - def validate(self, existing_registry_metadata=None): - - if self.org_id is None: - raise Exception("Org_id cannot be null") - if self.org_name is None: - raise Exception("Org_name cannot be null") - if self.org_type is None: - raise Exception("Org_type cannot be null") - if self.contacts is None: - raise Exception("contact_details can not be null") - if self.description is None: - raise Exception("description can not be null") - if self.groups: - unique_group_names = set() - for group in self.groups: - unique_group_names.add(group.group_name) - - if len(unique_group_names) < len(self.groups): - raise Exception("Cannot create group with duplicate names") - if len(self.groups) < 1: - raise Exception( - "At least One group is required to create an organization") - else: - for group in self.groups: - group.validate() - - existing_registry_metadata_group_names = set() - if existing_registry_metadata: - for group in existing_registry_metadata.groups: - existing_registry_metadata_group_names.add(group.group_name) - - self.is_removing_existing_group_from_org( - unique_group_names, existing_registry_metadata_group_names) - - def get_payment_address_for_group(self, group_name): - for group in self.groups: - if group.group_name == group_name: - return group.get_payment_address() - - def get_group_id_by_group_name(self, group_name): - for group in self.groups: - if group.group_name == group_name: - return group.group_id - - def get_group_by_group_id(self, group_id): - for group in self.groups: - if group.group_id == group_id: - return group - - def add_asset(self, asset_ipfs_hash, asset_type): - if asset_type == AssetType.HERO_IMAGE.value: - self.assets[asset_type] = asset_ipfs_hash - else: - raise Exception("Invalid asset type %s" % asset_type) - - def remove_all_assets(self): - self.assets = {} - - def remove_assets(self, asset_type): - if asset_type == AssetType.HERO_IMAGE.value: - self.assets[asset_type] = "" - else: - raise Exception("Invalid asset type %s" % asset_type) - - def add_description(self, description): - self.description["description"] = description - - def add_short_description(self, short_description): - self.description["short_description"] = short_description - - def add_url(self, url): - self.description["url"] = url - - def remove_description(self): - self.description = {} - - def add_contact(self, contact_type, phone, email): - if phone is None: - phone = "" - if email is None: - email = "" - - contact = { - "contact_type": contact_type, - "email_id": email, - "phone": phone - } - self.contacts.append(contact) - - def remove_contact_by_type(self, contact_type): - self.contacts = [contact for contact in self.contacts if contact["contact_type"] != contact_type] - - def remove_all_contacts(self): - self.contacts = [] diff --git a/snet/sdk/metadata_provider/ipfs_metadata_provider.py b/snet/sdk/metadata_provider/ipfs_metadata_provider.py index f461fb5..a1c7067 100644 --- a/snet/sdk/metadata_provider/ipfs_metadata_provider.py +++ b/snet/sdk/metadata_provider/ipfs_metadata_provider.py @@ -1,7 +1,7 @@ import json import web3 -from snet.sdk.metadata.service import mpe_service_metadata_from_json +from snet.sdk.metadata_provider.service_metadata import mpe_service_metadata_from_json from snet.sdk.utils.ipfs_utils import bytesuri_to_hash, get_from_ipfs_and_checkhash diff --git a/snet/sdk/metadata/service.py b/snet/sdk/metadata_provider/service_metadata.py similarity index 100% rename from snet/sdk/metadata/service.py rename to snet/sdk/metadata_provider/service_metadata.py diff --git a/snet/sdk/payment.py b/snet/sdk/payment.py deleted file mode 100644 index fcb4219..0000000 --- a/snet/sdk/payment.py +++ /dev/null @@ -1,6 +0,0 @@ -class Payment(object): - - def to_metadata(self): - pass - - diff --git a/snet/sdk/utils/config.py b/snet/sdk/utils/config.py deleted file mode 100644 index 4543ebf..0000000 --- a/snet/sdk/utils/config.py +++ /dev/null @@ -1,62 +0,0 @@ -from snet.contracts import get_contract_def - - -def get_contract_address(cmd, contract_name, error_message=None): - """ - We try to get config address from the different sources. - The order of priorioty is following: - - command line argument (at) - - command line argument (_at) - - current session configuration (current__at) - - networks/*json - """ - - # try to get from command line argument at or contractname_at - a = "at" - if hasattr(cmd.args, a) and getattr(cmd.args, a): - return cmd.w3.to_checksum_address(getattr(cmd.args, a)) - - # try to get from command line argument contractname_at - a = "%s_at" % contract_name.lower() - if hasattr(cmd.args, a) and getattr(cmd.args, a): - return cmd.w3.to_checksum_address(getattr(cmd.args, a)) - - # try to get from current session configuration - rez = cmd.config.get_session_field("current_%s_at" % (contract_name.lower()), exception_if_not_found=False) - if rez: - return cmd.w3.to_checksum_address(rez) - - error_message = error_message or "Fail to read %s address from \"networks\", you should " \ - "specify address by yourself via --%s_at parameter" % ( - contract_name, contract_name.lower()) - # try to take address from networks - return read_default_contract_address(w3=cmd.w3, contract_name=contract_name) - - -def read_default_contract_address(w3, contract_name): - chain_id = w3.net.version # this will raise exception if endpoint is invalid - contract_def = get_contract_def(contract_name) - networks = contract_def["networks"] - contract_address = networks.get(chain_id, {}).get("address", None) - if not contract_address: - raise Exception() - contract_address = w3.to_checksum_address(contract_address) - return contract_address - - -def get_field_from_args_or_session(config, args, field_name): - """ - We try to get field_name from diffent sources: - The order of priorioty is following: - read_default_contract_address - command line argument (--) - - current session configuration (default_) - """ - rez = getattr(args, field_name, None) - # type(rez) can be int in case of wallet-index, so we cannot make simply if(rez) - if rez is not None: - return rez - rez = config.get_session_field("default_%s" % field_name, exception_if_not_found=False) - if rez: - return rez - raise Exception("Fail to get default_%s from config, should specify %s via --%s parameter" % ( - field_name, field_name, field_name.replace("_", "-"))) diff --git a/snet/sdk/utils/ipfs_utils.py b/snet/sdk/utils/ipfs_utils.py index 0cfb9a2..fae3125 100644 --- a/snet/sdk/utils/ipfs_utils.py +++ b/snet/sdk/utils/ipfs_utils.py @@ -1,55 +1,13 @@ """ Utilities related to ipfs """ import tarfile -import glob import io import os import base58 +import ipfshttpclient import multihash -def publish_file_in_ipfs(ipfs_client, filepath, wrap_with_directory=True): - """ - push a file to ipfs given its path - """ - try: - with open(filepath, 'r+b') as file: - result = ipfs_client.add( - file, pin=True, wrap_with_directory=wrap_with_directory) - if wrap_with_directory: - return result[1]['Hash']+'/'+result[0]['Name'] - return result['Hash'] - except Exception as err: - print("File error ", err) - - -def publish_proto_in_ipfs(ipfs_client, protodir): - """ - make tar from protodir/*proto, and publish this tar in ipfs - return base58 encoded ipfs hash - """ - - if not os.path.isdir(protodir): - raise Exception("Directory %s doesn't exists" % protodir) - - files = glob.glob(os.path.join(protodir, "*.proto")) - - if len(files) == 0: - raise Exception("Cannot find any %s files" % - (os.path.join(protodir, "*.proto"))) - - # We are sorting files before we add them to the .tar since an archive containing the same files in a different - # order will produce a different content hash; - files.sort() - - tarbytes = io.BytesIO() - tar = tarfile.open(fileobj=tarbytes, mode="w") - for f in files: - tar.add(f, os.path.basename(f)) - tar.close() - return ipfs_client.add_bytes(tarbytes.getvalue()) - - def get_from_ipfs_and_checkhash(ipfs_client, ipfs_hash_base58, validate=True): """ Get file from ipfs @@ -83,15 +41,6 @@ def get_from_ipfs_and_checkhash(ipfs_client, ipfs_hash_base58, validate=True): return data -def hash_to_bytesuri(s): - """ - Convert in and from bytes uri format used in Registry contract - """ - # TODO: we should pad string with zeros till closest 32 bytes word because of a bug in processReceipt (in snet_cli.contract.process_receipt) - s = "ipfs://" + s - return s.encode("ascii").ljust(32 * (len(s)//32 + 1), b"\0") - - def bytesuri_to_hash(s): s = s.rstrip(b"\0").decode('ascii') if not s.startswith("ipfs://"): @@ -120,3 +69,10 @@ def safe_extract_proto_from_ipfs(ipfs_client, ipfs_hash, protodir): print("%s removed." % fullname) # now it is safe to call extractall f.extractall(protodir) + + +def get_ipfs_client(config): + ipfs_endpoint = config.get_ipfs_endpoint() + return ipfshttpclient.connect(ipfs_endpoint) + + diff --git a/snet/sdk/utils/utils.py b/snet/sdk/utils/utils.py index 2c04daf..6085c11 100644 --- a/snet/sdk/utils/utils.py +++ b/snet/sdk/utils/utils.py @@ -19,59 +19,6 @@ RESOURCES_PATH = PurePath(os.path.dirname(sdk.__file__)).joinpath("resources") -class DefaultAttributeObject(object): - def __init__(self, **kwargs): - for k, v in kwargs.items(): - if v is not None: - setattr(self, k, v) - - def getstring(self, item): - return getattr(self, item) - - def getint(self, item): - if getattr(self, item) is None: - return None - return int(getattr(self, item)) - - def getfloat(self, item): - if getattr(self, item) is None: - return None - return float(getattr(self, item)) - - def getboolean(self, item): - if getattr(self, item) is None: - return None - i = self.getstring(item) - if i in ["yes", "on", "true", "True", "1"]: - return True - return False - - def __getattr__(self, item): - return self.__dict__.get(item, None) - - def __repr__(self): - return self.__dict__.__repr__() - - def __str__(self): - return self.__dict__.__str__() - - -def get_web3(rpc_endpoint): - if rpc_endpoint.startswith("ws:"): - provider = web3.WebsocketProvider(rpc_endpoint) - else: - provider = web3.HTTPProvider(rpc_endpoint) - - return web3.Web3(provider) - - -def serializable(o): - if isinstance(o, bytes): - return o.hex() - else: - return o.__dict__ - - def safe_address_converter(a): if not web3.Web3.is_checksum_address(a): raise Exception("%s is not is not a valid Ethereum checksum address" % a) @@ -99,48 +46,6 @@ def bytes32_to_str(b): return b.rstrip(b"\0").decode("utf-8") -def _add_next_paths(path, entry_path, seen_paths, next_paths): - with open(path) as f: - for line in f: - if line.strip().startswith("import"): - import_statement = "".join(line.split('"')[1::2]) - if not import_statement.startswith("google/protobuf"): - import_statement_path = Path(path.parent.joinpath(import_statement)).resolve() - if entry_path.parent in path.parents: - if import_statement_path not in seen_paths: - seen_paths.add(import_statement_path) - next_paths.append(import_statement_path) - else: - raise ValueError("Path must not be a parent of entry path") - - -def walk_imports(entry_path): - seen_paths = set() - next_paths = [] - for file_path in os.listdir(entry_path): - if file_path.endswith(".proto"): - file_path = entry_path.joinpath(file_path) - seen_paths.add(file_path) - next_paths.append(file_path) - while next_paths: - path = next_paths.pop() - if os.path.isfile(path): - _add_next_paths(path, entry_path, seen_paths, next_paths) - else: - raise IOError("Import path must be a valid file: {}".format(path)) - return seen_paths - - -def read_temp_tar(f): - f.flush() - f.seek(0) - return f - - -def get_cli_version(): - return distribution("snet.cli").version - - def compile_proto(entry_path, codegen_dir, proto_file=None, target_language="python"): try: if not os.path.exists(codegen_dir): @@ -187,23 +92,6 @@ def compile_proto(entry_path, codegen_dir, proto_file=None, target_language="pyt return False -def abi_get_element_by_name(abi, name): - """ Return element of abi (return None if fails to find) """ - if abi and "abi" in abi: - for a in abi["abi"]: - if "name" in a and a["name"] == name: - return a - return None - - -def abi_decode_struct_to_dict(abi, struct_list): - return {el_abi["name"]: el for el_abi, el in zip(abi["outputs"], struct_list)} - - -def int4bytes_big(b): - return int.from_bytes(b, byteorder='big') - - def is_valid_endpoint(url): """ Just ensures the url has a scheme (http/https), and a net location (IP or domain name). @@ -231,40 +119,6 @@ def is_valid_endpoint(url): return False -def remove_http_https_prefix(endpoint): - """remove http:// or https:// prefix if presented in endpoint""" - endpoint = endpoint.replace("https://", "") - endpoint = endpoint.replace("http://", "") - return endpoint - - -def open_grpc_channel(endpoint): - """ - open grpc channel: - - for http:// we open insecure_channel - - for https:// we open secure_channel (with default credentials) - - without prefix we open insecure_channel - """ - _GB = 1024 ** 3 - options = [('grpc.max_send_message_length', _GB), - ('grpc.max_receive_message_length', _GB)] - if endpoint.startswith("https://"): - return grpc.secure_channel(remove_http_https_prefix(endpoint), grpc.ssl_channel_credentials(root_certificates=certificate)) - return grpc.insecure_channel(remove_http_https_prefix(endpoint)) - - -def rgetattr(obj, attr): - """ - >>> from types import SimpleNamespace - >>> args = SimpleNamespace(a=1, b=SimpleNamespace(c=2, d='e')) - >>> rgetattr(args, "a") - 1 - >>> rgetattr(args, "b.c") - 2 - """ - return functools.reduce(getattr, [obj] + attr.split('.')) - - def normalize_private_key(private_key): if private_key.startswith("0x"): private_key = bytes(bytearray.fromhex(private_key[2:])) @@ -277,7 +131,7 @@ def get_address_from_private(private_key): return web3.Account.from_key(private_key).address -class add_to_path(): +class add_to_path: def __init__(self, path): self.path = path @@ -296,15 +150,3 @@ def find_file_by_keyword(directory, keyword): for file in files: if keyword in file: return file - - -def is_valid_url(url): - regex = re.compile( - r'^(?:http|ftp)s?://' - r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' - r'localhost|' - r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' - r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' - r'(?::\d+)?' - r'(?:/?|[/?]\S+)$', re.IGNORECASE) - return re.match(regex, url) is not None From 5ba5ca345df4ec4679bee39bc46d64eabc6e1fdb Mon Sep 17 00:00:00 2001 From: Arondondon Date: Tue, 10 Sep 2024 19:30:27 +0300 Subject: [PATCH 45/55] The documentation structure of the SDK version without CLI dependencies has been defined. Some existing .md files have been changed. Finished utils.md file. --- docs/main/client_lib_generator.md | 0 docs/main/config.md | 0 docs/main/init.md | 38 +----- docs/metadata_provider/service_metadata.md | 0 docs/snet-sdk-python-documentation.md | 19 ++- docs/utils/ipfs_utils.md | 0 docs/utils/utils.md | 140 +++++++++++++++++++++ snet/sdk/utils/utils.py | 13 +- 8 files changed, 159 insertions(+), 51 deletions(-) create mode 100644 docs/main/client_lib_generator.md create mode 100644 docs/main/config.md create mode 100644 docs/metadata_provider/service_metadata.md create mode 100644 docs/utils/ipfs_utils.md create mode 100644 docs/utils/utils.md diff --git a/docs/main/client_lib_generator.md b/docs/main/client_lib_generator.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/main/config.md b/docs/main/config.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/main/init.md b/docs/main/init.md index 2bb602e..6c1532f 100644 --- a/docs/main/init.md +++ b/docs/main/init.md @@ -3,9 +3,7 @@ [Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/__init__.py) to GitHub Entities: -1. [Arguments](#class-arguments) - - [\_\_init\_\_](#__init__) -2. [SnetSDK](#class-snetsdk) +1. [SnetSDK](#class-snetsdk) - [\_\_init\_\_](#__init__-1) - [setup_config](#setup_config) - [set_session_identity](#set_session_identity) @@ -20,38 +18,6 @@ Entities: - [get_organization_list](#get_organization_list) - [get_services_list](#get_services_list) -### class `Arguments` - -extends: - - -is extended by: - - -#### description - -Represents the arguments for the `BlockchainCommand` from `snet.cli`. - -#### attributes - -- `org_id` (str): The organization id. -- `service_id` (str): The service id. -- `language` (str): The language used. Defaults to "python". -- `protodir` (Path): The path to the directory with protobuf files. Defaults to "~/USER_NAME/.snet". - -#### methods - -#### `__init__` - -Initializes a new instance of the class. - -###### args: - -- `org_id` (str): The organization id. Defaults to _None_. -- `service_id` (str): The service id. Defaults to _None_. - -###### returns: - -- _None_ - ### class `SnetSDK` extends: - @@ -134,7 +100,7 @@ Sets the session identity in the given config. #### `create_service_client` If `force_update` is True or if there are no gRPC stubs for the given service, the proto files are loaded -and compiled using the `generate_client_library()` method of the `SDKCommand` class instance. +and compiled using the `generate_client_library()` method of the `ClientLibGenerator` class instance. It then initializes `payment_channel_management_strategy` to `DefaultPaymentStrategy` if it is not specified. It also sets the `options` dictionary with some default values. If `self._metadata_provider` is not specified it is initialized by `IPFSMetadataProvider`. It also gets the service stub using the `self.get_service_stub` diff --git a/docs/metadata_provider/service_metadata.md b/docs/metadata_provider/service_metadata.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/snet-sdk-python-documentation.md b/docs/snet-sdk-python-documentation.md index 9a792b2..51cebec 100644 --- a/docs/snet-sdk-python-documentation.md +++ b/docs/snet-sdk-python-documentation.md @@ -21,21 +21,28 @@ A getting started guide for the SNET SDK for Python is available [here](https:// 2. [account](main/account.md) 3. [service_client](main/service_client.md) 4. [concurrency_manager](main/concurrency_manager.md) -5. [generic_client_interceptor](main/generic_client_interceptor.md) -6. metadata_provider +5. [config](main/config.md) +6. [client_lib_generator](main/client_lib_generator.md) +7. metadata_provider 1. [metadata_provider](metadata_provider/metadata_provider.md) 2. [ipfs_metadata_provider](metadata_provider/ipfs_metadata_provider.md) -7. mpe + 3. [service_metadata](metadata_provider/service_metadata.md) +8. mpe 1. [mpe_contract](mpe/mpe_contract.md) 2. [payment_channel](mpe/payment_channel.md) 3. [payment_channel_provider](mpe/payment_channel_provider.md) -8. payment_strategies +9. payment_strategies 1. [payment_strategy](payment_strategies/payment_strategy.md) 2. [default_payment_strategy](payment_strategies/default_payment_strategy.md) 3. [freecall_payment_strategy](payment_strategies/freecall_payment_strategy.md) 4. [paidcall_payment_strategy](payment_strategies/paidcall_payment_strategy.md) 5. [prepaid_payment_strategy](payment_strategies/prepaid_payment_strategy.md) -9. training - 1. [training](training/training.md) +10. utils + 1. [utils](utils/utils.md) + 2. [ipfs_utils](utils/ipfs_utils.md) + diff --git a/docs/utils/ipfs_utils.md b/docs/utils/ipfs_utils.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/utils/utils.md b/docs/utils/utils.md new file mode 100644 index 0000000..b99c538 --- /dev/null +++ b/docs/utils/utils.md @@ -0,0 +1,140 @@ +## module: sdk.utils.utils + +[Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/utils/utils.py) to GitHub + +Entities: +1. [safe_address_converter]() +2. [type_converter]() +2. [bytes32_to_str]() +3. [compile_proto]() +4. [is_valid_endpoint]() +5. [normalize_private_key]() +6. [get_address_from_private]() +7. [add_to_path]() +8. [find_file_by_keyword]() + + +#### Function `safe_address_converter` + +Checks if the address is a valid checksum address and returns it, otherwise raises an exception. + +###### args: + +- `a` (Any): The address to check. + +###### returns: + +- The address if it is valid. (Any) + +###### raises: + +- `Exception`: If the address isn't a valid checksum address. + +#### Function `type_converter + +Creates a function that converts a value to the specified type. + +###### args: + +- `t` (Any): The type to convert the value to. + +###### returns: + +- A function that converts the value to the specified type. (Any -> Any) + +#### Function `bytes32_to_str` + +Converts a bytes32 value to a string. + +###### args: + +- `b` (bytes): The bytes32 value to convert. + +###### returns: + +- The string representation of the bytes32 value. (str) + +#### Function `compile_proto` + +Compiles Protocol Buffer (protobuf) files into code for a specific target language. +Generated files as well as .proto files are stored in the `~/.snet` directory. + +###### args: + +- entry_path (Path): The path to the .proto file. +- codegen_dir (PurePath): The directory where the compiled code will be generated. +- proto_file (str): The name of the .proto file to compile. Defaults to `None`. +- target_language (str, optional): The target language for the compiled code. Defaults to "python". + +###### returns: + +- True if the compilation is successful, False otherwise. (bool) + +###### raises: + +- `Exception`: If the error occurs while performing the function. + +#### Function `is_valid_endpoint` + +Checks if the given endpoint is valid. + +###### args: + +- url (str): The endpoint to check. + +###### returns: + +- True if the endpoint is valid, False otherwise. (bool) + +###### raises: + +- `ValueError`: If the error occurs while performing the function. + +#### Function `normalize_private_key` + +Returns the normalized private key. + +###### args: + +- private_key (str): The private key in hex format to normalize. + +###### returns: + +- The normalized private key. (bytes) + +#### Function `get_address_from_private` + +Returns the wallet address from the private key. + +###### args: + +- private_key (Any): The private key. + +###### returns: + +- The wallet address. (ChecksumAddress) + +#### Class `add_to_path` + +`add_to_path` class is a _**context manager**_ that temporarily adds a given path to the system's `sys.path` list. +This allows for the import of modules or packages from that path. The `__enter__` method is called when the context +manager is entered, and it inserts the path at the beginning of sys.path. The `__exit__` method is called when the +context manager is exited, and it removes the path from sys.path. + +###### args: + +- path (str): The path to add to sys.path. + +#### Function `find_file_by_keyword` + +Finds a file by keyword in the current directory and subdirectories. + +###### args: + +- directory (AnyStr | PathLike[AnyStr]): The directory to search in. +- keyword (AnyStr): The keyword to search for. + +###### returns: + +- The name of the file that contains the keyword, or `None` if no file is found. (AnyStr | None) + diff --git a/snet/sdk/utils/utils.py b/snet/sdk/utils/utils.py index 6085c11..ebb7cc1 100644 --- a/snet/sdk/utils/utils.py +++ b/snet/sdk/utils/utils.py @@ -1,20 +1,15 @@ import json import os import subprocess -import functools -import re import sys import importlib.resources -from importlib.metadata import distribution from urllib.parse import urlparse from pathlib import Path, PurePath import web3 -import grpc from grpc_tools.protoc import main as protoc from snet import sdk -from snet.sdk.resources.root_certificate import certificate RESOURCES_PATH = PurePath(os.path.dirname(sdk.__file__)).joinpath("resources") @@ -146,7 +141,7 @@ def __exit__(self, exc_type, exc_value, traceback): def find_file_by_keyword(directory, keyword): - for root, dirs, files in os.walk(directory): - for file in files: - if keyword in file: - return file + for root, dirs, files in os.walk(directory): + for file in files: + if keyword in file: + return file From 4125936c5fa2d6cb0f73172da450c508e5eebbae Mon Sep 17 00:00:00 2001 From: Arondondon Date: Wed, 11 Sep 2024 14:03:15 +0300 Subject: [PATCH 46/55] Finished client_lib_generator.md and ipfs_utils.md. Some fixes in several other files. --- docs/main/client_lib_generator.md | 75 +++++ docs/main/config.md | 530 ++++++++++++++++++++++++++++++ docs/utils/ipfs_utils.md | 80 +++++ docs/utils/utils.md | 38 +-- snet/sdk/utils/ipfs_utils.py | 2 +- 5 files changed, 705 insertions(+), 20 deletions(-) diff --git a/docs/main/client_lib_generator.md b/docs/main/client_lib_generator.md index e69de29..6dde35b 100644 --- a/docs/main/client_lib_generator.md +++ b/docs/main/client_lib_generator.md @@ -0,0 +1,75 @@ +## module: sdk.client_lib_generator + +[Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/client_lib_generator.py) to GitHub + +Entities: +1. [ClientLibGenerator](#class-clientlibgenerator) + - [\_\_init\_\_](#__init__) + - [generate_client_library](#generate_client_library) + - [_get_service_metadata_from_registry](#_get_service_metadata_from_registry) + - [_get_service_registration](#_get_service_registration) + +### class `ClientLibGenerator` + +extends: - + +is extended by: - + +#### description + +This class is used to generate client library files for a given service. + +#### attributes + +- `sdk_config` (Config): An instance of the `Config` class +- `registry_contract` (web3.contract.Contract): An instance of the `Contract` class (from `snet.cli`) for interacting with the Registry contract. +- `org_id` (str): The organization ID of the service. +- `service_id` (str): The service ID. +- `language` (str): The language of the client library. Default is `python`. +- `protodir` (str): The directory where the .proto files are located. Default is `~/.snet`. + +#### methods + +#### `__init__` + +Initializes a new instance of the class. Initializes the attributes by arguments values. + +###### args: + +- `sdk_config` (Config): An instance of the `Config` class +- `registry_contract` (web3.contract.Contract): An instance of the `Contract` class (from `snet.cli`) for interacting with the Registry contract. +- `org_id` (str): The organization ID of the service. +- `service_id` (str): The service ID. + +###### returns: + +- _None_ + +#### `generate_client_library` + +Generates client library stub files based on specified organization and service ids, including: +- getting service metadata from Registry +- getting .proto file from IPFS +- compiling .proto file to stub file and saving it in a given directory + +#### `_get_service_metadata_from_registry` + +Retrieves service metadata. Fetches the service registration from the Registry contract, extracts the metadata URI, +downloads the metadata from IPFS, and parses it into a `MPEServiceMetadata` object. + +###### returns: + +- The service metadata. (MPEServiceMetadata) + +#### `_get_service_registration` + +Retrieves the service registration from the Registry contract using the `getServiceRegistrationById` function of it. + +###### returns: + +- Dictionary containing the service registration. (dict[str, Any]) + +###### raises: + +- `Exception`: If the service with the specified ID is not found in the organization. + diff --git a/docs/main/config.md b/docs/main/config.md index e69de29..09e41d2 100644 --- a/docs/main/config.md +++ b/docs/main/config.md @@ -0,0 +1,530 @@ +## module: sdk.config + +[Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/config.py) to GitHub + +Entities: +1. [ClientLibGenerator](#class-clientlibgenerator) + - [\_\_init\_\_](#__init__) + - [get_session_network_name](#get_session_network_name) + - [safe_get_session_identity_network_names](#safe_get_session_identity_network_names) + - [set_session_network](#set_session_network) + - [_set_session_network](#_set_session_network) + - [set_session_identity](#set_session_identity) + - [get_session_field](#get_session_field) + - [set_session_field](#set_session_field) + - [unset_session_field](#unset_session_field) + - [session_to_dict](#session_to_dict) + - [add_network](#add_network) + - [set_network_field](#set_network_field) + - [add_identity](#add_identity) + - [set_identity_field](#set_identity_field) + - [_get_network_section](#_get_network_section) + - [_get_identity_section](#_get_identity_section) + - [get_ipfs_endpoint](#get_ipfs_endpoint) + - [set_ipfs_endpoint](#set_ipfs_endpoint) + - [get_all_identities_names](#get_all_identities_names) + - [get_all_networks_names](#get_all_networks_names) + - [delete_identity](#delete_identity) + - [create_default_config](#create_default_config) + - [_check_section](#_check_section) + - [_persist](#_persist) + - [get_param_from_sdk_config](#get_param_from_sdk_config) + - [setup_identity](#setup_identity) +2. [first_identity_message_and_exit](#function-first_identity_message_and_exit) +3. [get_session_identity_keys](#function-get_session_identity_keys) +4. [get_session_network_keys](#function-get_session_network_keys) +5. [get_session_network_keys_removable](#function-get_session_network_keys_removable) +6. [get_session_keys](#function-get_session_keys) + +### class `ClientLibGenerator` + +extends: - + +is extended by: - + +#### description + + + +#### attributes + +- + +#### methods + +#### `__init__` + +Initializes a new instance of the class. + +###### args: + +- + +###### returns: + +- _None_ + +#### `get_session_network_name` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### `safe_get_session_identity_network_names` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### `set_session_network` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### `_set_session_network` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### `set_session_identity` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### `get_session_field` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### `set_session_field` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### `unset_session_field` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### `session_to_dict` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### `add_network` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### `set_network_field` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### `add_identity` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### `set_identity_field` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### `_get_network_section` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### `_get_identity_section` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### `get_ipfs_endpoint` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### `get_all_identities_names` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### `get_all_networks_names` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### `delete_identity` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### `create_default_config` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### `_check_section` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### `_persist` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### `get_param_from_sdk_config` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### `setup_identity` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### Function `first_identity_message_and_exit` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### Function `get_session_identity_keys` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### Function `get_session_network_keys` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### Function `get_session_network_keys_removable` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + +#### Function `get_session_keys` + + + +###### args: + +- + +###### returns: + +- + +###### raises: + +- + diff --git a/docs/utils/ipfs_utils.md b/docs/utils/ipfs_utils.md index e69de29..2afcd23 100644 --- a/docs/utils/ipfs_utils.md +++ b/docs/utils/ipfs_utils.md @@ -0,0 +1,80 @@ +## module: sdk.utils.ipfs_utils + +[Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/utils/ipfs_utils.py) to GitHub + +Entities: +1. [get_from_ipfs_and_checkhash](#get_from_ipfs_and_checkhash) +2. [bytesuri_to_hash](#bytesuri_to_hash) +3. [safe_extract_proto_from_ipfs](#safe_extract_proto_from_ipfs) +4. [get_ipfs_client](#get_ipfs_client) + + + +#### Function `get_from_ipfs_and_checkhash` + +This function retrieves a file from IPFS and optionally verifies its integrity by +checking the hash. If `validate` is True, it manually parses the IPFS block, extracts the file data, and verifies +the hash using a multihash object. If the hash does not match, it raises an exception. If `validate` is False, +it simply retrieves the file data. + +###### args: + +- `ipfs_client` (ipfshttpclient.client.Client): The IPFS client instance. +- `ipfs_hash_base58` (Any): The base58-encoded IPFS hash of the file. +- `validate` (bool): A boolean indicating whether to validate the hash (default is True). + +###### returns: + +- The contents of the file retrieved from IPFS. (bytes) + +###### raises: + +- `Exception`: If the hash validation fails or if the IPFS hash is not a file. + +#### Function `bytesuri_to_hash` + +Converts a bytes URI to a hash. + +###### args: + +- `s` (bytes): The bytes URI to convert. + +###### returns: + +- The hash extracted from the bytes URI. (str) + +###### raises: + +- `Exception`: If the input is not an IPFS URI. + +#### Function `safe_extract_proto_from_ipfs` + +This function safely extracts a tar file from IPFS to a specified directory. It checks for potential security risks by: +- Ensuring the tar file does not contain directories +- Ensuring all contents are files +- Removing any existing files with the same name before extraction + +If any of these checks fail, it raises an exception. Otherwise, it extracts the tar file to the specified directory. + +###### args: + +- `ipfs_client` (ipfshttpclient.client.Client): The IPFS client instance. +- `ipfs_hash` (str): The IPFS hash of the tar file to extract. +- `protodir` (str): The directory to extract the tar file to. + +###### raises: + +- `Exception`: If the tarball contains directories or non-file entries. + +#### Function `get_ipfs_client` + +Returns an IPFS client instance based on the provided configuration. + +###### args: + +- config (Config): The configuration object containing the IPFS endpoint. + +###### returns: + +- An IPFS client instance connected to the specified endpoint. (ipfshttpclient.client.Client) + diff --git a/docs/utils/utils.md b/docs/utils/utils.md index b99c538..94a886f 100644 --- a/docs/utils/utils.md +++ b/docs/utils/utils.md @@ -3,15 +3,15 @@ [Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/utils/utils.py) to GitHub Entities: -1. [safe_address_converter]() -2. [type_converter]() -2. [bytes32_to_str]() -3. [compile_proto]() -4. [is_valid_endpoint]() -5. [normalize_private_key]() -6. [get_address_from_private]() -7. [add_to_path]() -8. [find_file_by_keyword]() +1. [safe_address_converter](#safe_address_converter) +2. [type_converter](#type_converter) +3. [bytes32_to_str](#bytes32_to_str) +4. [compile_proto](#compile_proto) +5. [is_valid_endpoint](#is_valid_endpoint) +6. [normalize_private_key](#normalize_private_key) +7. [get_address_from_private](#get_address_from_private) +8. [add_to_path](#add_to_path) +9. [find_file_by_keyword](#find_file_by_keyword) #### Function `safe_address_converter` @@ -61,10 +61,10 @@ Generated files as well as .proto files are stored in the `~/.snet` directory. ###### args: -- entry_path (Path): The path to the .proto file. -- codegen_dir (PurePath): The directory where the compiled code will be generated. -- proto_file (str): The name of the .proto file to compile. Defaults to `None`. -- target_language (str, optional): The target language for the compiled code. Defaults to "python". +- `entry_path` (Path): The path to the .proto file. +- `codegen_dir` (PurePath): The directory where the compiled code will be generated. +- `proto_file` (str): The name of the .proto file to compile. Defaults to `None`. +- `target_language` (str, optional): The target language for the compiled code. Defaults to "python". ###### returns: @@ -80,7 +80,7 @@ Checks if the given endpoint is valid. ###### args: -- url (str): The endpoint to check. +- `url` (str): The endpoint to check. ###### returns: @@ -96,7 +96,7 @@ Returns the normalized private key. ###### args: -- private_key (str): The private key in hex format to normalize. +- `private_key` (str): The private key in hex format to normalize. ###### returns: @@ -108,7 +108,7 @@ Returns the wallet address from the private key. ###### args: -- private_key (Any): The private key. +- `private_key` (Any): The private key. ###### returns: @@ -123,7 +123,7 @@ context manager is exited, and it removes the path from sys.path. ###### args: -- path (str): The path to add to sys.path. +- `path` (str): The path to add to sys.path. #### Function `find_file_by_keyword` @@ -131,8 +131,8 @@ Finds a file by keyword in the current directory and subdirectories. ###### args: -- directory (AnyStr | PathLike[AnyStr]): The directory to search in. -- keyword (AnyStr): The keyword to search for. +- `directory` (AnyStr | PathLike[AnyStr]): The directory to search in. +- `keyword` (AnyStr): The keyword to search for. ###### returns: diff --git a/snet/sdk/utils/ipfs_utils.py b/snet/sdk/utils/ipfs_utils.py index fae3125..4e18c95 100644 --- a/snet/sdk/utils/ipfs_utils.py +++ b/snet/sdk/utils/ipfs_utils.py @@ -62,7 +62,7 @@ def safe_extract_proto_from_ipfs(ipfs_client, ipfs_hash, protodir): "tarball has directories. We do not support it.") if not m.isfile(): raise Exception( - "tarball contains %s which is not a files" % m.name) + "tarball contains %s which is not a file" % m.name) fullname = os.path.join(protodir, m.name) if os.path.exists(fullname): os.remove(fullname) From a777305e3791aab316e82a3952b8ced9b09b2de6 Mon Sep 17 00:00:00 2001 From: Arondondon Date: Thu, 12 Sep 2024 18:53:53 +0300 Subject: [PATCH 47/55] Finished service_metadata.md and config.md is half done. Some fixes in other .md files. --- docs/main/account.md | 4 +- docs/main/client_lib_generator.md | 2 +- docs/main/concurrency_manager.md | 2 +- docs/main/config.md | 121 ++- docs/main/init.md | 2 +- docs/main/service_client.md | 2 +- .../ipfs_metadata_provider.md | 2 +- docs/metadata_provider/metadata_provider.md | 2 +- docs/metadata_provider/service_metadata.md | 730 ++++++++++++++++++ docs/mpe/mpe_contract.md | 2 +- docs/mpe/payment_channel.md | 2 +- docs/mpe/payment_channel_provider.md | 2 +- .../default_payment_strategy.md | 2 +- .../freecall_payment_strategy.md | 2 +- .../paidcall_payment_strategy.md | 2 +- docs/payment_strategies/payment_strategy.md | 2 +- .../prepaid_payment_strategy.md | 2 +- docs/utils/ipfs_utils.md | 12 +- docs/utils/utils.md | 18 +- .../sdk/metadata_provider/service_metadata.py | 10 +- 20 files changed, 821 insertions(+), 102 deletions(-) diff --git a/docs/main/account.md b/docs/main/account.md index c79aef2..f2a4c63 100644 --- a/docs/main/account.md +++ b/docs/main/account.md @@ -18,7 +18,7 @@ Entities: - [approve_transfer](#approve_transfer) - [allowance](#allowance) -### class `TransactionError` +### Class `TransactionError` extends: `Exception` @@ -57,7 +57,7 @@ Returns a string representation of the `TransactionError` object. - A string containing the `message` attribute of the TransactionError object. (str) -### class `Account` +### Class `Account` extends: - diff --git a/docs/main/client_lib_generator.md b/docs/main/client_lib_generator.md index 6dde35b..77cd6d5 100644 --- a/docs/main/client_lib_generator.md +++ b/docs/main/client_lib_generator.md @@ -9,7 +9,7 @@ Entities: - [_get_service_metadata_from_registry](#_get_service_metadata_from_registry) - [_get_service_registration](#_get_service_registration) -### class `ClientLibGenerator` +### Class `ClientLibGenerator` extends: - diff --git a/docs/main/concurrency_manager.md b/docs/main/concurrency_manager.md index c52eaef..ff578fa 100644 --- a/docs/main/concurrency_manager.md +++ b/docs/main/concurrency_manager.md @@ -12,7 +12,7 @@ Entities: - [__get_token_for_amount](#__get_token_for_amount) - [record_successful_call](#record_successful_call) -### class `ConcurrencyManager` +### Class `ConcurrencyManager` extends: - diff --git a/docs/main/config.md b/docs/main/config.md index 09e41d2..cdc300b 100644 --- a/docs/main/config.md +++ b/docs/main/config.md @@ -3,7 +3,7 @@ [Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/config.py) to GitHub Entities: -1. [ClientLibGenerator](#class-clientlibgenerator) +1. [Config](#class-clientlibgenerator) - [\_\_init\_\_](#__init__) - [get_session_network_name](#get_session_network_name) - [safe_get_session_identity_network_names](#safe_get_session_identity_network_names) @@ -36,29 +36,33 @@ Entities: 5. [get_session_network_keys_removable](#function-get_session_network_keys_removable) 6. [get_session_keys](#function-get_session_keys) -### class `ClientLibGenerator` +### Class `Config` -extends: - +extends: `ConfigParser` is extended by: - #### description - +This is a configuration manager for the SDK. It is responsible for handling configuration settings for the SDK. #### attributes -- +- `_config_file` (Path): The path to the configuration file. +- `sdk_config` (dict): The SDK configuration. +- `is_sdk` (bool): Whether the configuration is for the SDK. #### methods #### `__init__` -Initializes a new instance of the class. +Initializes a new instance of the class. Reads the configuration file or creates a default one if it doesn't exist. +Initializes attributes by the passed arguments. ###### args: -- +- `_snet_folder` (Path): The path to the folder where the configuration file is located. Defaults to "~/.snet". +- `sdk_config` (dict): The SDK configuration. Defaults to _None_. ###### returns: @@ -66,147 +70,126 @@ Initializes a new instance of the class. #### `get_session_network_name` - - -###### args: - -- +Returns the name of the session network. ###### returns: -- - -###### raises: - -- +- (str): The name of the session network. #### `safe_get_session_identity_network_names` - - -###### args: - -- +Returns the names of the session network and identity. ###### returns: -- +- The names of the session network and identity. (str, str) ###### raises: -- +- Exception: If the session identity does not bind to the session network. #### `set_session_network` - +Sets the session network using `_set_session_network`. ###### args: -- +- `network` (str): The name of the session network. +- `out_f` (TextIO): The output to write messages to. ###### returns: -- - -###### raises: - -- +- _None_ #### `_set_session_network` - +Sets the session network. ###### args: -- +- `network` (str): The name of the session network. +- `out_f` (TextIO): The output to write messages to. ###### returns: -- +- _None_ ###### raises: -- +- Exception: If the network is not in the config. #### `set_session_identity` - +Sets the session identity. ###### args: -- +- `identity` (str): The name of the session identity. +- `out_f` (TextIO): The output to write messages to. ###### returns: -- +- _None_ ###### raises: -- +- Exception: If the identity is not in the config. #### `get_session_field` - +Retrieves a session field based on the provided key. ###### args: -- +- `key` (str): The key of the session field to retrieve. +- `exception_if_not_found` (bool): Whether to raise an exception if the field is not found. Defaults to _True_. ###### returns: -- +- The value of the session field if found, otherwise _None_. (str | None) ###### raises: -- +- `Exception`: If the field is not found and `exception_if_not_found` is _True_. #### `set_session_field` - +Sets a session field based on the provided key and value. ###### args: -- +- `key` (str): The key of the session field to set. +- `value` (str): The value of the session field to set. +- `out_f` (TextIO): The output to write messages to. ###### returns: -- +- _None_ ###### raises: -- +- `Exception`: If the key is not in the config. #### `unset_session_field` - +Unsets a session field based on the provided key. ###### args: -- +- `key` (str): The key of the session field to unset. +- `out_f` (TextIO): The output to write messages to. ###### returns: -- - -###### raises: - -- +- _None_ #### `session_to_dict` - - -###### args: - -- +Converts the session configuration to a dictionary. ###### returns: -- - -###### raises: - -- +- The session configuration as a dictionary. (dict) #### `add_network` @@ -448,7 +431,7 @@ Initializes a new instance of the class. - -#### Function `first_identity_message_and_exit` +### Function `first_identity_message_and_exit` @@ -464,7 +447,7 @@ Initializes a new instance of the class. - -#### Function `get_session_identity_keys` +### Function `get_session_identity_keys` @@ -480,7 +463,7 @@ Initializes a new instance of the class. - -#### Function `get_session_network_keys` +### Function `get_session_network_keys` @@ -496,7 +479,7 @@ Initializes a new instance of the class. - -#### Function `get_session_network_keys_removable` +### Function `get_session_network_keys_removable` @@ -512,7 +495,7 @@ Initializes a new instance of the class. - -#### Function `get_session_keys` +### Function `get_session_keys` diff --git a/docs/main/init.md b/docs/main/init.md index 6c1532f..c576680 100644 --- a/docs/main/init.md +++ b/docs/main/init.md @@ -18,7 +18,7 @@ Entities: - [get_organization_list](#get_organization_list) - [get_services_list](#get_services_list) -### class `SnetSDK` +### Class `SnetSDK` extends: - diff --git a/docs/main/service_client.md b/docs/main/service_client.md index 0439072..03cb483 100644 --- a/docs/main/service_client.md +++ b/docs/main/service_client.md @@ -31,7 +31,7 @@ Entities: - [get_services_and_messages_info](#get_services_and_messages_info) - [get_services_and_messages_info_as_pretty_string](#get_services_and_messages_info_as_pretty_string) -### class `ServiceClient` +### Class `ServiceClient` extends: - diff --git a/docs/metadata_provider/ipfs_metadata_provider.md b/docs/metadata_provider/ipfs_metadata_provider.md index bf219b3..85cbad5 100644 --- a/docs/metadata_provider/ipfs_metadata_provider.md +++ b/docs/metadata_provider/ipfs_metadata_provider.md @@ -9,7 +9,7 @@ Entities: - [fetch_service_metadata](#fetch_service_metadata) - [enhance_service_metadata](#enhance_service_metadata) -### class `IPFSMetadataProvider` +### Class `IPFSMetadataProvider` extends: `object` diff --git a/docs/metadata_provider/metadata_provider.md b/docs/metadata_provider/metadata_provider.md index 42c09f0..74831d2 100644 --- a/docs/metadata_provider/metadata_provider.md +++ b/docs/metadata_provider/metadata_provider.md @@ -8,7 +8,7 @@ Entities: - [fetch_service_metadata](#fetch_service_metadata) - [enhance_service_group_details](#enhance_service_group_details) -### abstract class `MetadataProvider` +### Abstract Class `MetadataProvider` extends: `object` diff --git a/docs/metadata_provider/service_metadata.md b/docs/metadata_provider/service_metadata.md index e69de29..2b7d346 100644 --- a/docs/metadata_provider/service_metadata.md +++ b/docs/metadata_provider/service_metadata.md @@ -0,0 +1,730 @@ +## module: sdk.metadata_provider.service_metadata + +[Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/metadata_provider/service_metadata.py) to GitHub + +Entities: +1. [AssetType](#class-assettype) + - [is_single_value](#is_single_value) +2. [MPEServiceMetadata](#class-mpeservicemetadata) + - [\_\_init\_\_](#__init__) + - [set_simple_field](#set_simple_field) + - [set_fixed_price_in_cogs](set_fixed_price_in_cogs) + - [set_method_price_in_cogs](set_method_price_in_cogs) + - [add_group](#add_group) + - [remove_group](#remove_group) + - [get_tags](#get_tags) + - [add_tag](#add_tag) + - [remove_tag](#remove_tag) + - [add_asset](#add_asset) + - [remove_all_assets](#remove_all_assets) + - [remove_asset](#remove_asset) + - [add_endpoint_to_group](#add_endpoint_to_group) + - [remove_all_endpoints_for_group](#remove_all_endpoints_for_group) + - [is_group_name_exists](#is_group_name_exists) + - [get_group_by_group_id](#get_group_by_group_id) + - [set_free_calls_for_group](#set_free_calls_for_group) + - [set_freecall_signer_address](#set_freecall_signer_address) + - [get_json](#get_json) + - [get_json_pretty](#get_json_pretty) + - [set_from_json](#set_from_json) + - [load](#load) + - [save_pretty](#save_pretty) + - [\_\_getitem\_\_](#__getitem__) + - [\_\_contains\_\_](#__contains__) + - [get_group_name_nonetrick](#get_group_name_nonetrick) + - [get_group](#get_group) + - [get_group_id_base64](#get_group_id_base64) + - [get_group_id](#get_group_id) + - [get_payment_address](#get_payment_address) + - [add_daemon_address_to_group](#add_daemon_address_to_group) + - [remove_all_daemon_addresses_for_group](#remove_all_daemon_addresses_for_group) + - [get_all_endpoints_for_group](#get_all_endpoints_for_group) + - [get_all_group_endpoints](#get_all_group_endpoints) + - [get_all_endpoints_with_group_name](#get_all_endpoints_with_group_name) + - [get_endpoints_for_group](#get_endpoints_for_group) + - [add_contributor](#add_contributor) + - [remove_contributor_by_email](#remove_contributor_by_email) + - [group_init](#group_init) + - [add_media](#add_media) + - [remove_media](#remove_media) + - [remove_all_media](#remove_all_media) + - [swap_media_order](#swap_media_order) + - [change_media_order](#change_media_order) + - [_is_asset_type_exists](#_is_asset_type_exists) + - [add_description](#add_description) +3. [load_mpe_service_metadata](#function-load_mpe_service_metadata) +4. [mpe_service_metadata_from_json](#function-mpe_service_metadata_from_json) + +### Class `AssetType` + +extends: `Enum` + +is extended by: - + +#### description + +This is an `enum` that represents the type of asset in the service metadata. + +#### members + +- `HERO_IMAGE` (str): The hero image asset type. Equals to "hero_image". +- `IMAGES` (str): The images asset type. Equals to "images". +- `DOCUMENTATION` (str): The documentation asset type. Equals to "documentation". +- `TERMS_OF_USE` (str): The terms of use asset type. Equals to "terms_of_use". + +#### methods + +#### `is_single_value` + +Static method. Checks if the asset type is a single value (`HERO_IMAGE`, `DOCUMENTATION` or `TERMS_OF_USE`). + +###### args: + +- `asset_type` (str): The asset type to check. + +###### returns: + +- `True` if the asset type is a single value, `False` otherwise. (bool) + +### Class `MPEServiceMetadata` + +extends: - + +is extended by: - + +#### description + +This class represents the service metadata. + +#### attributes + +- `m` (dict): A dictionary that contains all the service metadata fields. + +#### methods + +#### `__init__` + +Initializes a new instance of the class. Initializes the `m` dict with empty of default values. + +###### returns: + +- _None_ + +#### `set_simple_field` + +Sets a new value for a specified field in the `m` dict. Supported fields are: `display_name`, `encoding`, +`model_ipfs_hash`, `mpe_address`, `service_type`, `payment_expiration_threshold`, `service_description`. +If the field is not supported, an exception is raised. + +###### args: + +- `f` (str): The field name to set. +- `v` (Any): The value to set. + +###### returns: + +- _None_ + +###### raises: + +- `Exception`: If the field name is unknown. + +#### `set_fixed_price_in_cogs` + +Sets a new value for the `fixed_price` field in the specified payment group. + +###### args: + +- `group_name` (str): The name of the payment group. +- `price` (int): The new value for the `fixed_price` field. + +###### returns: + +- _None_ + +###### raises: + +- `Exception`: If the payment group with the specified name is not found. + +#### `set_method_price_in_cogs` + +Sets the price for a specific method in a service within a payment group. It checks if the group exists, then updates +the pricing details accordingly. If the pricing model or service does not exist, it creates a new one. + +###### args: + +- `group_name` (str): The name of the payment group. +- `package_name` (str): The name of the package. +- `service_name` (str): The name of the service. +- `method` (str): The name of the method. +- `price` (int): The new price for the method. + +###### returns: + +- _None_ + +###### raises: + +- `Exception`: If the payment group with the specified name is not found. + +#### `add_group` + +Adds a new payment group to the `m` dict. + +###### args: + +- `group_name` (str): The name of the new payment group. + +###### returns: + +- _None_ + +###### raises: + +- `Exception`: If the payment group with the specified name already exists. + +#### `remove_group` + +Removes a payment group from the `m` dict. + +###### args: + +- `group_name` (str): The name of the payment group to remove. + +###### returns: + +- _None_ + +#### `get_tags` + +Returns the list of tags from the `m` dict. If the `tags` field does not exist, an empty list is returned. + +###### returns: + +- The list of tags. (list[str]) + +#### `add_tag` + +Adds a new tag to the `tags` field in the `m` dict. If the tag already exists, it is not added again. + +###### args: + +- `tag_name` (str): The name of the new tag. + +###### returns: + +- _None_ + +#### `remove_tag` + +Removes a tag from the `tags` field in the `m` dict. If the tag does not exist, nothing happens. + +###### args: + +- `tag_name` (str): The name of the tag to remove. + +###### returns: + +- _None_ + +#### `add_asset` + +Adds a new asset to the `assets` field in the `m` dict. If the asset already exists, it is not added again. + +###### args: + +- `asset_ipfs_hash` (str): The IPFS hash of the asset. +- `asset_type` (str): The type of the asset. + +###### returns: + +- _None_ + +###### raises: + +- `Exception`: If the asset type is not supported. + +#### `remove_all_assets` + +Removes all assets from the `assets` field in the `m` dict. + +###### returns: + +- _None_ + +#### `remove_asset` + +Removes an asset from the `assets` field in the `m` dict. If the asset does not exist, nothing happens. +If the asset type is not supported, an exception is raised. + +###### args: + +- `asste_type` (str): The type of the asset to remove. + +###### returns: + +- _None_ + +###### raises: + +- `Exception`: If the asset type is not supported. + +#### `add_endpoint_to_group` + +Checks the endpoint is valid and adds it to the `endpoints` field of the specified payment group in the `m` dict. +If the endpoint is not valid of if the group does not exist or if the endpoint is already present, an exception is +raised. + +###### args: + +- `group_name` (str): The name of the payment group. +- `endpoint` (str): The new endpoint to add. + +###### returns: + +- _None_ + +###### raises: + +- `Exception`: If the payment group with the specified name is not found, if the endpoint is not valid, or if the +endpoint is already present. + +#### `remove_all_endpoints_from_group` + +Removes all endpoints from the `endpoints` field of the specified payment group in the `m` dict. If the group +does not exist, an exception is raised. + +###### args: + +- `group_name` (str): The name of the payment group. + +###### returns: + +- _None_ + +###### raises: + +- `Exception`: If the payment group with the specified name is not found. + +#### `is_group_name_exists` + +Checks if the payment group with the specified name exists in the `m` dict. + +###### args: + +- `group_name` (str): The name of the payment group. + +###### returns: + +- _True_ if the payment group exists, _False_ otherwise. (bool) + +#### `get_group_by_group_id` + +Returns group with given group id (returns _None_ if it doesn't exist). + +###### args: + +- `group_id` (str): The id of the payment group. + +###### returns: + +- The group with the given id. (dict[str, Any] | None) + +#### `set_free_calls_for_group` + +Sets the `free_calls` field of the specified payment group in the `m` dict, if the group exists. + +###### args: + +- `group_name` (str): The name of the payment group. +- `free_calls` (int): The new value for the `free_calls` field - amount of free calls. + +###### returns: + +- _None_ + +#### `set_freecall_signer_address` + +Sets the `freecall_signer_address` field of the specified payment group in the `m` dict, if the group exists. + +###### args: + +- `group_name` (str): The name of the payment group. +- `freecall_signer_address` (str): The new value for the `freecall_signer_address` field. + +###### returns: + +- _None_ + +#### `get_json` + +Returns the JSON representation of the `m` dict. + +###### returns: + +- The JSON representation of the `m` dict. (str) + +#### `get_json_pretty` + +Returns the pretty-printed JSON representation of the `m` dict. + +###### returns: + +- The pretty-printed JSON representation of the `m` dict. (str) + +#### `set_from_json` + +Sets the `m` dict from the service metadata JSON representation. + +###### args: + +- `j` (str): The service metadata JSON representation. + +###### returns: + +- _None_ + +#### `load` + +Loads the service metadata from the specified file and sets it in the `m` dict. + +###### args: + +- `file_name` (str): The name of the file containing the service metadata. + +###### returns: + +- _None_ + +#### `save_pretty` + +Saves the pretty-printed JSON representation of the `m` dict to the specified file. + +###### args: + +- `file_name` (str): The name of the file to save. + +###### returns: + +- _None_ + +#### `__getitem__` + +Overrides the `__getitem__` Python special method. Returns the value associated with the given key from +the `m` dict. + +###### args: + +- `key` (str): The name of the field. + +###### returns: + +- The value associated with the given key. (Any) + +#### `__contains__` + +Overrides the `__contains__` Python special method. + +###### args: + +- `key` (str): The name of the field. + +###### returns: + +- _True_ if the key is in the `m` dict, _False_ otherwise. (bool) + +#### `get_group_name_nonetrick` + +Returns a group name from metadata. If no group name is provided (group_name=None), it checks if there's only one +group in the metadata and returns its name. If there are multiple groups, it raises an exception, requiring a +specific group name to be provided. If a group name is provided, it simply returns that name. + +###### args: + +- `group_name` (str): The name of the payment group. Defaults to _None_. + +###### returns: + +- The name of the payment group. (str) + +###### raises: + +- `Exception`: If there are no groups in the metadata or if there are multiple groups in the metadata and no +group name is provided. + +#### `get_group` + +Retrieves a group from metadata by its name. If no name is provided, it uses `get_group_name_nonetrick` to determine +the name. Searches for a matching group in the metadata and returns it. If no matching group is found, +raises an exception. + +###### args: + +- `group_name` (str): The name of the payment group. Defaults to _None_. + +###### returns: + +- The payment group. (dict[str, Any]) + +###### raises: + +- `Exception`: If no group with the specified name is found in the `m` dict. + +#### `get_group_id_base64` + +Returns the group id base64 encoded. + +###### args: + +- `group_name` (str): The name of the payment group. Defaults to _None_. + +###### returns: + +- The group id base64 encoded. (str) + +#### `get_group_id` + +Returns the group id as bytes from `m` dict. + +###### args: + +- `group_name` (str): The name of the payment group. Defaults to _None_. + +###### returns: + +- The group id. (bytes) + +#### `get_payment_address` + +Returns the payment address of the group from `m` dict. + +###### args: + +- `group_name` (str): The name of the payment group. Defaults to _None_. + +###### returns: + +- The payment address. (str) + +#### `add_daemon_address_to_group` + +Adds a daemon address to the `daemon_addresses` field of the specified payment group in the `m` dict. + +###### args: + +- `group_name` (str): The name of the payment group. +- `daemon_address` (str): The new daemon address to add. + +###### returns: + +- _None_ + +###### raises: + +- `Exception`: If the payment group with the specified name does not exist. + +#### `remove_all_daemon_addresses_for_group` + +Removes all daemon addresses from the `daemon_addresses` field of the specified payment group in the `m` dict. + +###### args: + +- `group_name` (str): The name of the payment group. + +###### returns: + +- _None_ + +###### raises: + +- `Exception`: If the payment group with the specified name does not exist. + +#### `get_all_endpoints_for_group` + +Returns all endpoints from the `endpoints` field of the specified payment group in the `m` dict. + +###### args: + +- `group_name` (str): The name of the payment group. + +###### returns: + +- The list of endpoints. (list[str]) + +#### `get_all_group_endpoints` + +Returns all endpoints from the `endpoints` of all payment groups in the `m` dict. + +###### returns: + +- The list of endpoints. (list[str]) + +#### `get_all_endpoints_with_group_name` + +Returns all endpoints from the `endpoints` for all payment groups in the `m` dict as dict with the group name as key +and the list of endpoints as value. + +###### returns: + +- Group names with lists of endpoints. (dict[str, list[str]]) + +#### `get_endpoints_for_group` + +Returns a list of endpoints that belong to a specific payment group. If no group name is provided, it will use +the default group name (if only one group exists). + +###### args: + +- `group_name` (str): The name of the payment group. Defaults to _None_. + +###### returns: + +- The list of endpoints. (list[str]) + +#### `add_contributor` + +Adds a contributor to the `contributors` field of the `m` dict. + +###### args: + +- `name` (str): The name of the contributor. +- `email_id` (str): The email id of the contributor. + +###### returns: + +- _None_ + +#### `remove_contributor_by_email` + +Removes contributors with the specified email from the `contributors` field of the `m` dict. + +###### args: + +- `email_id` (str): The email id of the contributor. + +###### returns: + +- _None_ + +#### `group_init` + +Initializes a new payment group in the `m` dict. Prompts the user to enter several fields, such as fixed price, +endpoints, etc. + +###### args: + +- `group_name` (str): The name of the payment group. + +###### returns: + +- _None_ + +###### raises: + +- `Exception`: If user enters same endpoints. +- `ValueError`: If user enters non-integer fixed price. + +#### `add_media` + +Adds a new media to the `media` field of the `m` dict. + +###### args: + +- `url` (str): The url of the media. +- `media_type` (str): The type of the media. +- `hero_img` (bool): Whether the media is a hero image. Defaults to _False_. + +###### returns: + +- _None_ + +#### `remove_media` + +Removes an individual media from the `media` field of the `m` dict using unique order key. + +###### args: + +- `order` (int): The order of the media. + +###### returns: + +- _None_ + +###### raises: + +- `Exception`: If the media with the specified order does not exist. + +#### `remove_all_media` + +Removes all individual media from the `media` field of the `m` dict. + +###### returns: + +- _None_ + +#### `swap_media_order` + +Swap orders of two different media given their individual orders. + +###### args: + +- `move_from` (int): The order of the first media to be moved. +- `move_to` (int): The order of the second media to be moved. + +###### returns: + +- _None_ + +###### raises: + +- `Exception`: If at least one of the media with the specified orders does not exist. + +#### `change_media_order` + +Mini REPL to change order of all individual media + +###### returns: + +- _None_ + +#### `_is_asset_type_exists` + +Returns whether the asset type exists in the media field of the `m` dict. + +###### returns: + +- _True_ if the asset type exists, _False_ otherwise. (bool) + +#### `add_description` + +Adds a new description to the `description` field of the `m` dict. Prompts the user to enter user guide url, +service long description and service short description. + +###### returns: + +- _None_ + +### Function `load_mpe_service_metadata` + +Loads the service metadata from the JSON file to `MPEServiceMetadata` object. + +###### args: + +- `f` (str): The name of the file containing the service metadata as JSON. + +###### returns: + +- The service metadata. (MPEServiceMetadata) + +### Function `mpe_service_metadata_from_json` + +Loads the service metadata from the JSON string to `MPEServiceMetadata` object. + +###### args: + +- `j` (str): The service metadata JSON representation. + +###### returns: + +- The service metadata. (MPEServiceMetadata) + diff --git a/docs/mpe/mpe_contract.md b/docs/mpe/mpe_contract.md index 3957ec7..461b795 100644 --- a/docs/mpe/mpe_contract.md +++ b/docs/mpe/mpe_contract.md @@ -14,7 +14,7 @@ Entities: - [channel_extend_and_add_funds](#channel_extend_and_add_funds) - [_fund_escrow_account](#_fund_escrow_account) -### class `MPEContract` +### Class `MPEContract` extends: - diff --git a/docs/mpe/payment_channel.md b/docs/mpe/payment_channel.md index eb80598..b5d33d6 100644 --- a/docs/mpe/payment_channel.md +++ b/docs/mpe/payment_channel.md @@ -11,7 +11,7 @@ Entities: - [sync_state](#sync_state) - [_get_current_channel_state](#_get_current_channel_state) -### class `PaymentChannel` +### Class `PaymentChannel` extends: - diff --git a/docs/mpe/payment_channel_provider.md b/docs/mpe/payment_channel_provider.md index db6a957..803f3b6 100644 --- a/docs/mpe/payment_channel_provider.md +++ b/docs/mpe/payment_channel_provider.md @@ -10,7 +10,7 @@ Entities: - [deposit_and_open_channel](#deposit_and_open_channel) - [_get_newly_opened_channel](#_get_newly_opened_channel) -### class `PaymentChannelProvider` +### Class `PaymentChannelProvider` extends: `object` diff --git a/docs/payment_strategies/default_payment_strategy.md b/docs/payment_strategies/default_payment_strategy.md index fc81605..dd271e5 100644 --- a/docs/payment_strategies/default_payment_strategy.md +++ b/docs/payment_strategies/default_payment_strategy.md @@ -10,7 +10,7 @@ Entities: - [get_payment_metadata](#get_payment_metadata) - [get_concurrency_token_and_channel](#get_concurrency_token_and_channel) -### class `DefaultPaymentStrategy` +### Class `DefaultPaymentStrategy` extends: `PaymentStrategy` diff --git a/docs/payment_strategies/freecall_payment_strategy.md b/docs/payment_strategies/freecall_payment_strategy.md index 29b0dc6..a563660 100644 --- a/docs/payment_strategies/freecall_payment_strategy.md +++ b/docs/payment_strategies/freecall_payment_strategy.md @@ -8,7 +8,7 @@ Entities: - [get_payment_metadata](#get_payment_metadata) - [generate_signature](#generate_signature) -### class `FreeCallPaymentStrategy` +### Class `FreeCallPaymentStrategy` extends: `PaymentStrategy` diff --git a/docs/payment_strategies/paidcall_payment_strategy.md b/docs/payment_strategies/paidcall_payment_strategy.md index d39a09d..1ab0830 100644 --- a/docs/payment_strategies/paidcall_payment_strategy.md +++ b/docs/payment_strategies/paidcall_payment_strategy.md @@ -12,7 +12,7 @@ Entities: - [_is_valid](#_is_valid) -### class `PaidCallPaymentStrategy` +### Class `PaidCallPaymentStrategy` extends: `PaymentStrategy` diff --git a/docs/payment_strategies/payment_strategy.md b/docs/payment_strategies/payment_strategy.md index 7beb8c0..34c2bee 100644 --- a/docs/payment_strategies/payment_strategy.md +++ b/docs/payment_strategies/payment_strategy.md @@ -9,7 +9,7 @@ Entities: - [get_payment_metadata](#get_payment_metadata) - [get_price](#get_price) -### abstract class `PaymentStrategy` +### Abstract Class `PaymentStrategy` extends: `object` diff --git a/docs/payment_strategies/prepaid_payment_strategy.md b/docs/payment_strategies/prepaid_payment_strategy.md index e133991..e5420bd 100644 --- a/docs/payment_strategies/prepaid_payment_strategy.md +++ b/docs/payment_strategies/prepaid_payment_strategy.md @@ -13,7 +13,7 @@ Entities: - [_is_valid](#_is_valid) -### class `PrePaidPaymentStrategy` +### Class `PrePaidPaymentStrategy` extends: `PaymentStrategy` diff --git a/docs/utils/ipfs_utils.md b/docs/utils/ipfs_utils.md index 2afcd23..cb7fa9b 100644 --- a/docs/utils/ipfs_utils.md +++ b/docs/utils/ipfs_utils.md @@ -10,7 +10,7 @@ Entities: -#### Function `get_from_ipfs_and_checkhash` +### Function `get_from_ipfs_and_checkhash` This function retrieves a file from IPFS and optionally verifies its integrity by checking the hash. If `validate` is True, it manually parses the IPFS block, extracts the file data, and verifies @@ -31,7 +31,7 @@ it simply retrieves the file data. - `Exception`: If the hash validation fails or if the IPFS hash is not a file. -#### Function `bytesuri_to_hash` +### Function `bytesuri_to_hash` Converts a bytes URI to a hash. @@ -47,7 +47,7 @@ Converts a bytes URI to a hash. - `Exception`: If the input is not an IPFS URI. -#### Function `safe_extract_proto_from_ipfs` +### Function `safe_extract_proto_from_ipfs` This function safely extracts a tar file from IPFS to a specified directory. It checks for potential security risks by: - Ensuring the tar file does not contain directories @@ -62,11 +62,15 @@ If any of these checks fail, it raises an exception. Otherwise, it extracts the - `ipfs_hash` (str): The IPFS hash of the tar file to extract. - `protodir` (str): The directory to extract the tar file to. +###### returns: + +- _None_ + ###### raises: - `Exception`: If the tarball contains directories or non-file entries. -#### Function `get_ipfs_client` +### Function `get_ipfs_client` Returns an IPFS client instance based on the provided configuration. diff --git a/docs/utils/utils.md b/docs/utils/utils.md index 94a886f..f57401c 100644 --- a/docs/utils/utils.md +++ b/docs/utils/utils.md @@ -14,7 +14,7 @@ Entities: 9. [find_file_by_keyword](#find_file_by_keyword) -#### Function `safe_address_converter` +### Function `safe_address_converter` Checks if the address is a valid checksum address and returns it, otherwise raises an exception. @@ -30,7 +30,7 @@ Checks if the address is a valid checksum address and returns it, otherwise rais - `Exception`: If the address isn't a valid checksum address. -#### Function `type_converter +### Function `type_converter Creates a function that converts a value to the specified type. @@ -42,7 +42,7 @@ Creates a function that converts a value to the specified type. - A function that converts the value to the specified type. (Any -> Any) -#### Function `bytes32_to_str` +### Function `bytes32_to_str` Converts a bytes32 value to a string. @@ -54,7 +54,7 @@ Converts a bytes32 value to a string. - The string representation of the bytes32 value. (str) -#### Function `compile_proto` +### Function `compile_proto` Compiles Protocol Buffer (protobuf) files into code for a specific target language. Generated files as well as .proto files are stored in the `~/.snet` directory. @@ -74,7 +74,7 @@ Generated files as well as .proto files are stored in the `~/.snet` directory. - `Exception`: If the error occurs while performing the function. -#### Function `is_valid_endpoint` +### Function `is_valid_endpoint` Checks if the given endpoint is valid. @@ -90,7 +90,7 @@ Checks if the given endpoint is valid. - `ValueError`: If the error occurs while performing the function. -#### Function `normalize_private_key` +### Function `normalize_private_key` Returns the normalized private key. @@ -102,7 +102,7 @@ Returns the normalized private key. - The normalized private key. (bytes) -#### Function `get_address_from_private` +### Function `get_address_from_private` Returns the wallet address from the private key. @@ -114,7 +114,7 @@ Returns the wallet address from the private key. - The wallet address. (ChecksumAddress) -#### Class `add_to_path` +### Class `add_to_path` `add_to_path` class is a _**context manager**_ that temporarily adds a given path to the system's `sys.path` list. This allows for the import of modules or packages from that path. The `__enter__` method is called when the context @@ -125,7 +125,7 @@ context manager is exited, and it removes the path from sys.path. - `path` (str): The path to add to sys.path. -#### Function `find_file_by_keyword` +### Function `find_file_by_keyword` Finds a file by keyword in the current directory and subdirectories. diff --git a/snet/sdk/metadata_provider/service_metadata.py b/snet/sdk/metadata_provider/service_metadata.py index 5c5f267..1772e6e 100644 --- a/snet/sdk/metadata_provider/service_metadata.py +++ b/snet/sdk/metadata_provider/service_metadata.py @@ -55,7 +55,8 @@ class AssetType(Enum): @staticmethod def is_single_value(asset_type): - if asset_type == AssetType.HERO_IMAGE.value or asset_type == AssetType.DOCUMENTATION.value or asset_type == AssetType.TERMS_OF_USE.value: + if (asset_type == AssetType.HERO_IMAGE.value or asset_type == AssetType.DOCUMENTATION.value + or asset_type == AssetType.TERMS_OF_USE.value): return True @@ -215,7 +216,7 @@ def add_asset(self, asset_ipfs_hash, asset_type): def remove_all_assets(self): self.m['assets'] = {} - def remove_assets(self, asset_type): + def remove_asset(self, asset_type): if 'assets' in self.m: if AssetType.is_single_value(asset_type): self.m['assets'][asset_type] = "" @@ -262,7 +263,7 @@ def is_group_name_exists(self, group_name): return False def get_group_by_group_id(self, group_id): - """ return group with given group_id (return None if doesn't exists) """ + """ return group with given group_id (return None if it doesn't exist) """ group_id_base64 = base64.b64encode(group_id).decode('ascii') groups = self.m["groups"] for g in groups: @@ -396,7 +397,8 @@ def add_contributor(self, name, email_id): def remove_contributor_by_email(self, email_id): self.m["contributors"] = [ - contributor for contributor in self.m["contributors"] if contributor["email_id"] != email_id] + contributor for contributor in self.m["contributors"] if contributor["email_id"] != email_id + ] def group_init(self, group_name): """Required values for creating a new payment group. From fada44a9f01c4d12f77e6e5915629560e106746b Mon Sep 17 00:00:00 2001 From: Arondondon Date: Fri, 13 Sep 2024 13:37:29 +0300 Subject: [PATCH 48/55] Finished config.md. --- docs/main/config.md | 229 +++++++++++++++----------------------------- 1 file changed, 79 insertions(+), 150 deletions(-) diff --git a/docs/main/config.md b/docs/main/config.md index cdc300b..89a2ed2 100644 --- a/docs/main/config.md +++ b/docs/main/config.md @@ -3,7 +3,7 @@ [Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/config.py) to GitHub Entities: -1. [Config](#class-clientlibgenerator) +1. [Config](#class-config) - [\_\_init\_\_](#__init__) - [get_session_network_name](#get_session_network_name) - [safe_get_session_identity_network_names](#safe_get_session_identity_network_names) @@ -193,321 +193,250 @@ Converts the session configuration to a dictionary. #### `add_network` - +Adds a new network configuration to the existing config. ###### args: -- +- `network` (str): The name of the network to add. +- `rpc_endpoint` (str): The RPC endpoint of the network. +- `default_gas_price` (str): The default gas price of the network. ###### returns: -- +- _None_ ###### raises: -- +- `Exception`: If the specified network section already exists in config. #### `set_network_field` - +Sets a network field based on the provided key and value. ###### args: -- +- `network` (str): The name of the network to set. +- `key` (str): The key of the network field to set. +- `value` (str): The value of the network field to set. ###### returns: -- - -###### raises: - -- +- _None_ #### `add_identity` - +Adds a new identity configuration to the existing config. ###### args: -- +- `identity_name` (str): The name of the identity to add. +- `identity` (dict): The identity configuration. +- `out_f` (TextIO): The output to write messages to. Defaults to _sys.stdout_. ###### returns: -- +- _None_ ###### raises: -- +- `Exception`: If the specified identity section already exists in config or if network of the identity is not in config. #### `set_identity_field` - +Sets an identity field based on the provided key and value. ###### args: -- +- `identity` (str): The name of the identity to set. +- `key` (str): The key of the identity field to set. +- `value` (str): The value of the identity field to set. ###### returns: -- - -###### raises: - -- +- _None_ #### `_get_network_section` - +Returns the config section for the specified network. ###### args: -- +- `network` (str): The name of the network. ###### returns: -- - -###### raises: - -- +- The config section for the specified network. (dict | SectionProxy) #### `_get_identity_section` - +Returns the config section for the specified identity. ###### args: -- +- `identity` (str): The name of the identity. ###### returns: -- - -###### raises: - -- +- The config section for the specified identity. (dict | SectionProxy) #### `get_ipfs_endpoint` - - -###### args: - -- +Returns default IPFS endpoint from config. ###### returns: -- - -###### raises: - -- - -#### `get_all_identities_names` +- The default IPFS endpoint. (str) +#### `set_ipfs_endpoint` +Sets default IPFS endpoint in config. ###### args: -- +- `ipfs_endpoint` (str): The IPFS endpoint to set. ###### returns: -- - -###### raises: +- _None_ -- +#### `get_all_identities_names` -#### `get_all_networks_names` +Returns all identity names from config. +###### returns: +- The list of identity names. (list[str]) -###### args: +#### `get_all_networks_names` -- +Returns all network names from config. ###### returns: -- - -###### raises: - -- +- The list of network names. (list[str]) #### `delete_identity` - +Deletes an identity from config. ###### args: -- +- `identity_name` (str): The name of the identity to delete. ###### returns: -- +- _None_ ###### raises: -- +- `Exception`: If the specified identity name does not exist in config or if the specified identity name is in use. #### `create_default_config` - - -###### args: - -- +Creates default configuration if config file does not exist. Determines default endpoints for each network and IPFS. +Sets identity if it is provided by sdk. ###### returns: -- - -###### raises: - -- +- _None_ #### `_check_section` - +Checks if the specified section exists in config, raises an exception if it does not. ###### args: -- +- `s` (sts): The section name to check. ###### returns: -- +- _None_ ###### raises: -- +- `Exception`: If the specified section does not exist in config. #### `_persist` - - -###### args: - -- +Persists the config to disk. ###### returns: -- - -###### raises: - -- +- _None_ #### `get_param_from_sdk_config` - +Returns the value of the specified parameter from the `sdk_config` dict. ###### args: -- +- `param` (str): The name of the parameter to get. +- `alternative` (any): The value to return if the parameter is not found in `sdk_config`. Defaults to _None_. ###### returns: -- - -###### raises: - -- +- The value of the parameter or `alternative` if the parameter is not found or _None_ if `sdk_config` is not set. (Any) #### `setup_identity` +Sets up and returns a new identity in the config with the parameters from the `sdk_config`. - -###### args: - -- +_Note_: currently, only `key` identity type is supported. ###### returns: -- +- The identity as a dict. (dict) ###### raises: -- +- Exception: If the `identity_type` is not passed in the `sdk_config`. ### Function `first_identity_message_and_exit` - +Prints a message instructing the user to create their first identity and then exits the program. +The message differs depending on whether the `is_sdk` parameter is True or False, indicating whether the function +is being called from an SDK or not. The message lists the available identity types and how to create an identity. ###### args: -- +- `is_sdk` (bool): Whether the function is being called from an SDK. ###### returns: -- - -###### raises: - -- +- _None_ ### Function `get_session_identity_keys` - - -###### args: - -- +Returns a list with the only one element "default_wallet_index". ###### returns: -- - -###### raises: - -- +- List with the only one element. (list[str]) ### Function `get_session_network_keys` - - -###### args: - -- +Returns a list with the elements "default_gas_price", "current_registry_at", "current_multipartyescrow_at", +"current_singularitynettoken_at" and "default_eth_rpc_endpoint". ###### returns: -- - -###### raises: - -- +- List with the elements. (list[str]) ### Function `get_session_network_keys_removable` - - -###### args: - -- +Returns a list with the elements "default_gas_price", "current_registry_at", "current_multipartyescrow_at" and +"current_singularitynettoken_at". ###### returns: -- - -###### raises: - -- +- List with the elements. (list[str]) ### Function `get_session_keys` - - -###### args: - -- +Returns a list with the elements from the `get_session_identity_keys` and `get_session_network_keys` methods, plus +the "default_ipfs_endpoint". ###### returns: -- - -###### raises: - -- +- List with the elements. (list[str]) From f8559971615ac6b769632faadfc95adabf44f7ad Mon Sep 17 00:00:00 2001 From: Arondondon Date: Fri, 13 Sep 2024 16:26:26 +0300 Subject: [PATCH 49/55] Added and finished training.md --- docs/snet-sdk-python-documentation.md | 3 +- docs/training/training.md | 166 ++++++++++++++++++++++++++ snet/sdk/training/training.py | 31 +++-- 3 files changed, 186 insertions(+), 14 deletions(-) create mode 100644 docs/training/training.md diff --git a/docs/snet-sdk-python-documentation.md b/docs/snet-sdk-python-documentation.md index 51cebec..f8820a1 100644 --- a/docs/snet-sdk-python-documentation.md +++ b/docs/snet-sdk-python-documentation.md @@ -40,9 +40,8 @@ A getting started guide for the SNET SDK for Python is available [here](https:// 10. utils 1. [utils](utils/utils.md) 2. [ipfs_utils](utils/ipfs_utils.md) - + diff --git a/docs/training/training.md b/docs/training/training.md new file mode 100644 index 0000000..0f946c7 --- /dev/null +++ b/docs/training/training.md @@ -0,0 +1,166 @@ +## module: sdk.training.training.py + +[Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/training/training.py) to GitHub + +Entities: +1. [ModelMethodMessage](#class-modelmethodmessage) +2. [TrainingModel](#class-trainingmodel) + - [\_\_init\_\_](#__init__) + - [_invoke_model](#_invoke_model) + - [create_model](#create_model) + - [get_model_status](#get_model_status) + - [delete_model](#delete_model) + - [update_model_access](#update_model_access) + - [get_all_models](#get_all_models) + +### Class `ModelMethodMessage` + +extends: `Enum` + +is extended by: - + +#### description + +This is an `enum` that represents the available methods that can be called in the training grpc service. + +#### members + +- `CreateModel` (str): The method to create a new model. +- `GetModelStatus` (str): The method to get the status of a model. +- `UpdateModelAccess` (str): The method to update the access of a model. +- `DeleteModel` (str): The method to delete a model. +- `GetAllModels` (str): The method to get all models. + +### Class `TrainingModel` + +extends: - + +is extended by: - + +#### description + +This is a class that represents a training gRPC service. + +#### attributes + +- `training_pb2` (ModuleType): The gRPC service module. +- `training_pb2_grpc` (ModuleType): The gRPC service module. + +#### methods + +#### `__init__` + +Initializes a new instance of the class. Imports gRPC service modules. + +###### returns: + +- _None_ + +#### `_invoke_model` + +Invokes the model by establishing a gRPC channel and generating an authorization request. + +###### args: + +- `service_client` (ServiceClient): The client object for the service. +- `msg` (ModelMethodMessage): The message containing the method to be invoked. + +###### returns: + +- A tuple containing the authorization request and the gRPC channel. (tuple[AuthorizationDetails, grpc.Channel]) + +###### raises: + +- `ValueError`: If the scheme in the service metadata is not supported. + +#### `create_model` + +Calls the `create_model` method in the gRPC training service stub to create a new model. + +###### args: + +- `service_client` (ServiceClient): The client object for the service. +- `grpc_method_name` (str): The name of the gRPC method to be invoked. +- `model_name` (str): The name of the model to be created. +- `description` (str): A description of the model. Defaults to ''. +- `training_data_link` (str): A link to the training data. Defaults to ''. +- `grpc_service_name` (str): The name of the gRPC service. Defaults to 'service'. +- `is_publicly_accessible` (bool): Whether the model is publicly accessible. Defaults to False. +- `address_list` (list[str]): A list of addresses. Defaults to None. + +###### returns: + +- The response from the create model request. (Any) + +_Note_: Returns an exception if an error occurs during the create model request. + +#### `get_model_status` + +Calls the `get_model_status` method in the gRPC training service stub to get a model status. + +###### args: + +- `service_client` (ServiceClient): The client object for the service. +- `model_id` (str): The ID of the model whose status to be retrieved. + +###### returns: + +- The response from the get model status request. (Any) + +_Note_: Returns an exception if an error occurs during the get model status request. + +#### `delete_model` + +Calls the `delete_model` method in the gRPC training service stub to delete a model. + +###### args: + +- `service_client` (ServiceClient): The client object for the service. +- `model_id` (str): The ID of the model to be deleted. +- `grpc_service_name` (str): The name of the gRPC service. Defaults to 'service'. +- `grpc_method_name` (str): The name of the gRPC method to be invoked. + +###### returns: + +- The response from the delete model request. (Any) + +_Note_: Returns an exception if an error occurs during the delete model request. + +#### `update_model_access` + +Calls the `update_model_access` method in the gRPC training service stub to update the access of a model. + +###### args: + +- `service_client` (ServiceClient): The client object for the service. +- `model_id` (str): The ID of the model whose access to be updated. +- `grpc_method_name` (str): The name of the gRPC method to be invoked. +- `model_name` (str): The name of the model. +- `is_punlic` (bool): Whether the model is publicly accessible. +- `description` (str): A description of the model. +- `grpc_service_name` (str): The name of the gRPC service. Defaults to 'service'. +- `address_list` (list[str]): A list of addresses. + +###### returns: + +- The response from the update model access request. (Any) + +_Note_: Returns an exception if an error occurs during the update model access request. + +#### `get_all_models` + +Calls the `get_all_models` method in the gRPC training service stub to get all models. + +###### args: + +- `service_client` (ServiceClient): The client object for the service. +- `grpc_method_name` (str): The name of the gRPC method to be invoked. +- `grpc_service_name` (str): The name of the gRPC service. Defaults to 'service'. + +###### returns: + +- The response from the get all models request. (Any) + +_Note_: Returns an exception if an error occurs during the get all models request. + + diff --git a/snet/sdk/training/training.py b/snet/sdk/training/training.py index a158c8a..c5ede80 100644 --- a/snet/sdk/training/training.py +++ b/snet/sdk/training/training.py @@ -62,15 +62,16 @@ def _invoke_model(self, service_client, msg: ModelMethodMessage): # params from AI-service: status, model_id # params pass to daemon: grpc_service_name, grpc_method_name, address_list, # description, model_name, training_data_link, is_public_accessible - def create_model(self, service_client, grpc_method_name: str, model_name: str, - description: str = '', + def create_model(self, service_client, grpc_method_name: str, + model_name: str, description: str = '', training_data_link: str = '', grpc_service_name='service', is_publicly_accessible=False, address_list: list[str] = None): if address_list is None: address_list = [] try: auth_req, channel = self._invoke_model(service_client, ModelMethodMessage.CreateModel) - model_details = self.training_pb2.ModelDetails(grpc_method_name=grpc_method_name, description=description, + model_details = self.training_pb2.ModelDetails(grpc_method_name=grpc_method_name, + description=description, training_data_link=training_data_link, grpc_service_name=grpc_service_name, model_name=model_name, address_list=address_list, @@ -89,7 +90,8 @@ def get_model_status(self, service_client, model_id: str, grpc_method_name: str, try: auth_req, channel = self._invoke_model(service_client, ModelMethodMessage.GetModelStatus) model_details = self.training_pb2.ModelDetails(grpc_method_name=grpc_method_name, - grpc_service_name=grpc_service_name, model_id=str(model_id)) + grpc_service_name=grpc_service_name, + model_id=str(model_id)) stub = self.training_pb2_grpc.ModelStub(channel) response = stub.get_model_status( self.training_pb2.ModelDetailsRequest(authorization=auth_req, model_details=model_details)) @@ -100,12 +102,13 @@ def get_model_status(self, service_client, model_id: str, grpc_method_name: str, # params from AI-service: status # params to daemon: grpc_service_name, grpc_method_name, model_id - def delete_model(self, service_client, model_id: str, grpc_method_name: str, - grpc_service_name='service'): + def delete_model(self, service_client, model_id: str, + grpc_method_name: str, grpc_service_name='service'): try: auth_req, channel = self._invoke_model(service_client, ModelMethodMessage.DeleteModel) model_details = self.training_pb2.ModelDetails(grpc_method_name=grpc_method_name, - grpc_service_name=grpc_service_name, model_id=str(model_id)) + grpc_service_name=grpc_service_name, + model_id=str(model_id)) stub = self.training_pb2_grpc.ModelStub(channel) response = stub.delete_model( self.training_pb2.UpdateModelRequest(authorization=auth_req, update_model_details=model_details)) @@ -119,13 +122,16 @@ def delete_model(self, service_client, model_id: str, grpc_method_name: str, # all params required def update_model_access(self, service_client, model_id: str, grpc_method_name: str, model_name: str, is_public: bool, - description: str, grpc_service_name: str = 'service', address_list: list[str] = None): + description: str, grpc_service_name: str = 'service', + address_list: list[str] = None): try: auth_req, channel = self._invoke_model(service_client, ModelMethodMessage.UpdateModelAccess) - model_details = self.training_pb2.ModelDetails(grpc_method_name=grpc_method_name, description=description, + model_details = self.training_pb2.ModelDetails(grpc_method_name=grpc_method_name, + description=description, grpc_service_name=grpc_service_name, address_list=address_list, - is_publicly_accessible=is_public, model_name=model_name, + is_publicly_accessible=is_public, + model_name=model_name, model_id=str(model_id)) stub = self.training_pb2_grpc.ModelStub(channel) response = stub.update_model_access( @@ -142,9 +148,10 @@ def get_all_models(self, service_client, grpc_method_name: str, grpc_service_nam auth_req, channel = self._invoke_model(service_client, ModelMethodMessage.GetAllModels) stub = self.training_pb2_grpc.ModelStub(channel) response = stub.get_all_models( - self.training_pb2.AccessibleModelsRequest(authorization=auth_req, grpc_service_name=grpc_service_name, + self.training_pb2.AccessibleModelsRequest(authorization=auth_req, + grpc_service_name=grpc_service_name, grpc_method_name=grpc_method_name)) return response except Exception as e: print("Exception: ", e) - return e \ No newline at end of file + return e From 569224496eff83c668137c35bb3882627313f23d Mon Sep 17 00:00:00 2001 From: Arondondon Date: Fri, 13 Sep 2024 16:29:50 +0300 Subject: [PATCH 50/55] Tiny fix. --- docs/snet-sdk-python-documentation.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/snet-sdk-python-documentation.md b/docs/snet-sdk-python-documentation.md index f8820a1..d7b7692 100644 --- a/docs/snet-sdk-python-documentation.md +++ b/docs/snet-sdk-python-documentation.md @@ -11,10 +11,6 @@ The SingularityNET SDK abstracts and manages state channels with service provide A getting started guide for the SNET SDK for Python is available [here](https://github.com/singnet/snet-sdk-python/blob/master/README.md). -### Package Diagram - -![Package Diagram]() - ### Modules 1. [\_\_init\_\_](main/init.md) From e6a2a9ce1e1ce53c8ebafbd0ea10ffae38c738ee Mon Sep 17 00:00:00 2001 From: Arondondon Date: Fri, 13 Sep 2024 16:46:52 +0300 Subject: [PATCH 51/55] Fixed all bugs related to links in .md files. Fixed wrong module name (now payment_strategy.py). --- docs/main/account.md | 4 ++-- docs/main/init.md | 2 +- docs/main/service_client.md | 1 - docs/metadata_provider/metadata_provider.md | 2 +- .../paidcall_payment_strategy.md | 9 ++++----- docs/payment_strategies/payment_strategy.md | 6 ++---- .../prepaid_payment_strategy.md | 8 ++++---- docs/utils/ipfs_utils.md | 8 ++++---- docs/utils/utils.md | 18 +++++++++--------- .../default_payment_strategy.py | 2 +- .../freecall_payment_strategy.py | 2 +- .../paidcall_payment_strategy.py | 2 +- ...{payment_staregy.py => payment_strategy.py} | 0 .../prepaid_payment_strategy.py | 2 +- 14 files changed, 31 insertions(+), 35 deletions(-) rename snet/sdk/payment_strategies/{payment_staregy.py => payment_strategy.py} (100%) diff --git a/docs/main/account.md b/docs/main/account.md index f2a4c63..c951c99 100644 --- a/docs/main/account.md +++ b/docs/main/account.md @@ -5,9 +5,9 @@ Entities: 1. [TransactionError](#class-transactionerror) - [\_\_init\_\_](#__init__) - - [\_\_str\_\_](#str) + - [\_\_str\_\_](#__str__) 2. [Account](#class-account) - - [\_\_init\_\_](#init-1) + - [\_\_init\_\_](#__init__-1) - [_get_nonce](#_get_nonce) - [_get_gas_price](#_get_gas_price) - [_send_signed_transaction](#_send_signed_transaction) diff --git a/docs/main/init.md b/docs/main/init.md index c576680..b6379c8 100644 --- a/docs/main/init.md +++ b/docs/main/init.md @@ -4,7 +4,7 @@ Entities: 1. [SnetSDK](#class-snetsdk) - - [\_\_init\_\_](#__init__-1) + - [\_\_init\_\_](#__init__) - [setup_config](#setup_config) - [set_session_identity](#set_session_identity) - [create_service_client](#create_service_client) diff --git a/docs/main/service_client.md b/docs/main/service_client.md index 03cb483..11df1b9 100644 --- a/docs/main/service_client.md +++ b/docs/main/service_client.md @@ -10,7 +10,6 @@ Entities: - [get_grpc_base_channel](#get_grpc_base_channel) - [_get_grpc_channel](#_get_grpc_channel) - [_get_service_call_metadata](#_get_service_call_metadata) - - [_intercept_call](#_intercept_call) - [_filter_existing_channels_from_new_payment_channels](#_filter_existing_channels_from_new_payment_channels) - [load_open_channels](#load_open_channels) - [get_current_block_number](#get_current_block_number) diff --git a/docs/metadata_provider/metadata_provider.md b/docs/metadata_provider/metadata_provider.md index 74831d2..a3118af 100644 --- a/docs/metadata_provider/metadata_provider.md +++ b/docs/metadata_provider/metadata_provider.md @@ -3,7 +3,7 @@ [Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/metadata_provider/metadata_provider.py) to GitHub Entities: -1. [MetadataProvider](#class-transactionerror) +1. [MetadataProvider](#abstract-class-metadataprovider) - [fetch_org_metadata](#fetch_org_metadata) - [fetch_service_metadata](#fetch_service_metadata) - [enhance_service_group_details](#enhance_service_group_details) diff --git a/docs/payment_strategies/paidcall_payment_strategy.md b/docs/payment_strategies/paidcall_payment_strategy.md index 1ab0830..8dff3f9 100644 --- a/docs/payment_strategies/paidcall_payment_strategy.md +++ b/docs/payment_strategies/paidcall_payment_strategy.md @@ -8,9 +8,8 @@ Entities: - [get_price](#get_price) - [get_payment_metadata](#get_payment_metadata) - [select_channel](#select_channel) - - [_has_sufficient_funds](#_has_sufficient_funds) - - [_is_valid](#_is_valid) - + - [_has_sufficient_funds](#static-_has_sufficient_funds) + - [_is_valid](#static-_is_valid) ### Class `PaidCallPaymentStrategy` @@ -26,7 +25,7 @@ expiration are checked before each call and the payment itself is made each call #### attributes -- `block_offset` (int): Current block number. +- `block_offset` (int): Block offset. - `call_allowance` (int): The amount of allowed calls. #### methods @@ -37,7 +36,7 @@ Initializes a new instance of the class. ###### args: -- `block_offset` (int): Current block number. Defaults to 240. +- `block_offset` (int): Block offset. - `call_allowance` (int): The amount of allowed calls. Defaults to 1. ###### returns: diff --git a/docs/payment_strategies/payment_strategy.md b/docs/payment_strategies/payment_strategy.md index 34c2bee..c980c92 100644 --- a/docs/payment_strategies/payment_strategy.md +++ b/docs/payment_strategies/payment_strategy.md @@ -1,8 +1,6 @@ -## module: sdk.payment_strategies.payment_staregy +## module: sdk.payment_strategies.payment_strategy - - -[Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/payment_strategies/payment_staregy.py) to GitHub +[Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/payment_strategies/payment_strategy.py) to GitHub Entities: 1. [PaymentStrategy](#class-paymentstrategy) diff --git a/docs/payment_strategies/prepaid_payment_strategy.md b/docs/payment_strategies/prepaid_payment_strategy.md index e5420bd..7e5094f 100644 --- a/docs/payment_strategies/prepaid_payment_strategy.md +++ b/docs/payment_strategies/prepaid_payment_strategy.md @@ -9,8 +9,8 @@ Entities: - [get_payment_metadata](#get_payment_metadata) - [get_concurrency_token_and_channel](#get_concurrency_token_and_channel) - [select_channel](#select_channel) - - [_has_sufficient_funds](#_has_sufficient_funds) - - [_is_valid](#_is_valid) + - [_has_sufficient_funds](#static-_has_sufficient_funds) + - [_is_valid](#static-_is_valid) ### Class `PrePaidPaymentStrategy` @@ -28,7 +28,7 @@ and then make them. #### attributes - `concurrency_manager`: The `ConcurrencyManager` instance. -- `block_offset` (int): Current block number. +- `block_offset` (int): Block offset. - `call_allowance` (int): The amount of allowed calls. #### methods @@ -40,7 +40,7 @@ Initializes a new instance of the class. ###### args: - `concurrency_manager`: The `ConcurrencyManager` instance. -- `block_offset` (int): Current block number. Defaults to 240. +- `block_offset` (int): Block offset. - `call_allowance` (int): The amount of allowed calls. Defaults to 1. ###### returns: diff --git a/docs/utils/ipfs_utils.md b/docs/utils/ipfs_utils.md index cb7fa9b..62aaaef 100644 --- a/docs/utils/ipfs_utils.md +++ b/docs/utils/ipfs_utils.md @@ -3,10 +3,10 @@ [Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/utils/ipfs_utils.py) to GitHub Entities: -1. [get_from_ipfs_and_checkhash](#get_from_ipfs_and_checkhash) -2. [bytesuri_to_hash](#bytesuri_to_hash) -3. [safe_extract_proto_from_ipfs](#safe_extract_proto_from_ipfs) -4. [get_ipfs_client](#get_ipfs_client) +1. [get_from_ipfs_and_checkhash](#function-get_from_ipfs_and_checkhash) +2. [bytesuri_to_hash](#function-bytesuri_to_hash) +3. [safe_extract_proto_from_ipfs](#function-safe_extract_proto_from_ipfs) +4. [get_ipfs_client](#function-get_ipfs_client) diff --git a/docs/utils/utils.md b/docs/utils/utils.md index f57401c..e96a045 100644 --- a/docs/utils/utils.md +++ b/docs/utils/utils.md @@ -3,15 +3,15 @@ [Link](https://github.com/singnet/snet-sdk-python/blob/master/snet/sdk/utils/utils.py) to GitHub Entities: -1. [safe_address_converter](#safe_address_converter) -2. [type_converter](#type_converter) -3. [bytes32_to_str](#bytes32_to_str) -4. [compile_proto](#compile_proto) -5. [is_valid_endpoint](#is_valid_endpoint) -6. [normalize_private_key](#normalize_private_key) -7. [get_address_from_private](#get_address_from_private) -8. [add_to_path](#add_to_path) -9. [find_file_by_keyword](#find_file_by_keyword) +1. [safe_address_converter](#function-safe_address_converter) +2. [type_converter](#function-type_converter) +3. [bytes32_to_str](#function-bytes32_to_str) +4. [compile_proto](#function-compile_proto) +5. [is_valid_endpoint](#function-is_valid_endpoint) +6. [normalize_private_key](#function-normalize_private_key) +7. [get_address_from_private](#function-get_address_from_private) +8. [add_to_path](#class-add_to_path) +9. [find_file_by_keyword](#function-find_file_by_keyword) ### Function `safe_address_converter` diff --git a/snet/sdk/payment_strategies/default_payment_strategy.py b/snet/sdk/payment_strategies/default_payment_strategy.py index 63203f6..d8582d7 100644 --- a/snet/sdk/payment_strategies/default_payment_strategy.py +++ b/snet/sdk/payment_strategies/default_payment_strategy.py @@ -2,7 +2,7 @@ from snet.sdk.payment_strategies.freecall_payment_strategy import FreeCallPaymentStrategy from snet.sdk.payment_strategies.paidcall_payment_strategy import PaidCallPaymentStrategy from snet.sdk.payment_strategies.prepaid_payment_strategy import PrePaidPaymentStrategy -from snet.sdk.payment_strategies.payment_staregy import PaymentStrategy +from snet.sdk.payment_strategies.payment_strategy import PaymentStrategy class DefaultPaymentStrategy(PaymentStrategy): diff --git a/snet/sdk/payment_strategies/freecall_payment_strategy.py b/snet/sdk/payment_strategies/freecall_payment_strategy.py index 7373019..aae70b6 100644 --- a/snet/sdk/payment_strategies/freecall_payment_strategy.py +++ b/snet/sdk/payment_strategies/freecall_payment_strategy.py @@ -6,7 +6,7 @@ from snet.sdk.resources.root_certificate import certificate from snet.sdk.utils.utils import RESOURCES_PATH, add_to_path -from snet.sdk.payment_strategies.payment_staregy import PaymentStrategy +from snet.sdk.payment_strategies.payment_strategy import PaymentStrategy class FreeCallPaymentStrategy(PaymentStrategy): diff --git a/snet/sdk/payment_strategies/paidcall_payment_strategy.py b/snet/sdk/payment_strategies/paidcall_payment_strategy.py index 5903dba..8acd44b 100644 --- a/snet/sdk/payment_strategies/paidcall_payment_strategy.py +++ b/snet/sdk/payment_strategies/paidcall_payment_strategy.py @@ -1,5 +1,5 @@ import web3 -from snet.sdk.payment_strategies.payment_staregy import PaymentStrategy +from snet.sdk.payment_strategies.payment_strategy import PaymentStrategy class PaidCallPaymentStrategy(PaymentStrategy): diff --git a/snet/sdk/payment_strategies/payment_staregy.py b/snet/sdk/payment_strategies/payment_strategy.py similarity index 100% rename from snet/sdk/payment_strategies/payment_staregy.py rename to snet/sdk/payment_strategies/payment_strategy.py diff --git a/snet/sdk/payment_strategies/prepaid_payment_strategy.py b/snet/sdk/payment_strategies/prepaid_payment_strategy.py index b786170..aaaf3b3 100644 --- a/snet/sdk/payment_strategies/prepaid_payment_strategy.py +++ b/snet/sdk/payment_strategies/prepaid_payment_strategy.py @@ -1,4 +1,4 @@ -from snet.sdk.payment_strategies.payment_staregy import PaymentStrategy +from snet.sdk.payment_strategies.payment_strategy import PaymentStrategy class PrePaidPaymentStrategy(PaymentStrategy): From 2bb8a715d53d9538abbdd939ae17acc515aa6d0d Mon Sep 17 00:00:00 2001 From: pls-github-dont-suspend-me Date: Fri, 20 Sep 2024 16:40:08 +0300 Subject: [PATCH 52/55] Got rid of local storage of client configuration. Now config is stored only in code and is not stored in .snet folder. --- snet/sdk/__init__.py | 31 +---- snet/sdk/config.py | 304 +++++-------------------------------------- 2 files changed, 32 insertions(+), 303 deletions(-) diff --git a/snet/sdk/__init__.py b/snet/sdk/__init__.py index 1856c68..c3b0ca4 100644 --- a/snet/sdk/__init__.py +++ b/snet/sdk/__init__.py @@ -45,7 +45,7 @@ class SnetSDK: """Base Snet SDK""" - def __init__(self, sdk_config, metadata_provider=None): + def __init__(self, sdk_config: Config, metadata_provider=None): self._sdk_config = sdk_config self._metadata_provider = metadata_provider @@ -66,7 +66,7 @@ def __init__(self, sdk_config, metadata_provider=None): self.mpe_contract = MPEContract(self.web3, _mpe_contract_address) # Instantiate IPFS client - ipfs_endpoint = self._sdk_config.get("default_ipfs_endpoint", "/dns/ipfs.singularitynet.io/tcp/80/") + ipfs_endpoint = self._sdk_config["ipfs_endpoint"] self.ipfs_client = ipfshttpclient.connect(ipfs_endpoint) # Get Registry contract address from config if specified; mostly for local testing @@ -78,30 +78,6 @@ def __init__(self, sdk_config, metadata_provider=None): self.account = Account(self.web3, sdk_config, self.mpe_contract) - global_config = Config(sdk_config=sdk_config) - self.setup_config(global_config) - - def setup_config(self, config: Config) -> None: - out_f = sys.stdout - network = self._sdk_config.get("network", None) - identity_name = self._sdk_config.get("identity_name", None) - # Checking for an empty network - if network and config["session"]["network"] != network: - config.set_session_network(network, out_f) - if identity_name: - self.set_session_identity(identity_name, config, out_f) - elif len(config.get_all_identities_names()) > 0: - if "identity" not in config["session"] or config["session"]["identity"] == "": - raise Exception("identity_name is not passed or selected") - - def set_session_identity(self, identity_name: str, config: Config, out_f): - if identity_name not in config.get_all_identities_names(): - identity = config.setup_identity() - config.add_identity(identity_name, identity, out_f) - config.set_session_identity(identity_name, out_f) - elif config["session"]["identity"] != identity_name: - config.set_session_identity(identity_name, out_f) - def create_service_client(self, org_id: str, service_id: str, group_name=None, payment_channel_management_strategy=None, free_call_auth_token_bin=None, @@ -110,8 +86,7 @@ def create_service_client(self, org_id: str, service_id: str, group_name=None, concurrent_calls=1): # Create and instance of the Config object, so we can create an instance of ClientLibGenerator - sdk_config_object = Config(sdk_config=self._sdk_config) - lib_generator = ClientLibGenerator(sdk_config_object, self.registry_contract, org_id, service_id) + lib_generator = ClientLibGenerator(self._sdk_config, self.registry_contract, org_id, service_id) # Download the proto file and generate stubs if needed force_update = self._sdk_config.get('force_update', False) diff --git a/snet/sdk/config.py b/snet/sdk/config.py index 4636d74..eff168f 100644 --- a/snet/sdk/config.py +++ b/snet/sdk/config.py @@ -1,283 +1,37 @@ -from configparser import ConfigParser, ExtendedInterpolation -from pathlib import Path -import sys -default_snet_folder = Path("~").expanduser().joinpath(".snet") -DEFAULT_NETWORK = "sepolia" - -class Config(ConfigParser): - def __init__(self, _snet_folder=default_snet_folder, sdk_config=None): - super(Config, self).__init__(interpolation=ExtendedInterpolation(), delimiters=("=",)) - self._config_file = _snet_folder.joinpath("config") - self.sdk_config = sdk_config - self.is_sdk = True if sdk_config else False - if self._config_file.exists(): - with open(self._config_file) as f: - self.read_file(f) - else: - self.create_default_config() - - def get_session_network_name(self): - session_network = self["session"]["network"] - self._check_section("network.%s" % session_network) - return session_network - - def safe_get_session_identity_network_names(self): - if "identity" not in self["session"]: - first_identity_message_and_exit(is_sdk=self.is_sdk) - - session_identity = self["session"]["identity"] - self._check_section("identity.%s" % session_identity) - - session_network = self.get_session_network_name() - - network = self._get_identity_section(session_identity).get("network") - if network and network != session_network: - raise Exception("Your session identity '%s' is bind to network '%s', which is different from your" - " session network '%s', please switch identity or network" % ( - session_identity, network, session_network)) - return session_identity, session_network - - def set_session_network(self, network, out_f): - self._set_session_network(network, out_f) - if "identity" in self["session"]: - session_identity = self["session"]["identity"] - identity_network = self._get_identity_section(session_identity).get("network") - if identity_network and identity_network != network: - print("Your new session network '%s' is incompatible with your current session identity '%s' " - "(which is bind to network '%s'), please switch your identity" % ( - network, session_identity, identity_network), file=out_f) - - def _set_session_network(self, network, out_f): - if network not in self.get_all_networks_names(): - raise Exception("Network %s is not in config" % network) - print("Switch to network: %s" % network, file=out_f) - self["session"]["network"] = network - self._persist() - - def set_session_identity(self, identity, out_f): - if identity not in self.get_all_identities_names(): - raise Exception('Identity "%s" is not in config' % identity) - network = self._get_identity_section(identity).get("network") - if network: - print('Identity "%s" is bind to network "%s"' % (identity, network), file=out_f) - self._set_session_network(network, out_f) - else: - print( - 'Identity "%s" is not bind to any network. You should switch network manually if you need.' % identity, - file=out_f) - print("Switch to identity: %s" % (identity), file=out_f) - self["session"]["identity"] = identity - self._persist() - - # session is the union of session.identity + session.network + default_ipfs_endpoint - # if value is presented in both session.identity and session.network we get it from session.identity (can happen only for default_eth_rpc_endpoint) - def get_session_field(self, key, exception_if_not_found=True): - session_identity, session_network = self.safe_get_session_identity_network_names() - - rez_identity = self._get_identity_section(session_identity).get(key) - rez_network = self._get_network_section(session_network).get(key) - - rez_ipfs = None - if key == "default_ipfs_endpoint": - rez_ipfs = self.get_ipfs_endpoint() - - rez = rez_identity or rez_network or rez_ipfs - if not rez and exception_if_not_found: - raise Exception("Cannot find %s in the session.identity and in the session.network" % key) - return rez - - def set_session_field(self, key, value, out_f): - if key == "default_ipfs_endpoint": - self.set_ipfs_endpoint(value) - print("set default_ipfs_endpoint=%s" % value, file=out_f) - elif key in get_session_network_keys(): - session_network = self.get_session_network_name() - self.set_network_field(session_network, key, value) - print("set {}={} for network={}".format(key, value, session_network), file=out_f) - elif key in get_session_identity_keys(): - session_identity, _ = self.safe_get_session_identity_network_names() - self.set_identity_field(session_identity, key, value) - print("set {}={} for identity={}".format(key, value, session_identity), file=out_f) - else: - all_keys = get_session_network_keys() + get_session_identity_keys() + ["default_ipfs_endpoint"] - raise Exception("key {} not in {}".format(key, all_keys)) - - def unset_session_field(self, key, out_f): - if key in get_session_network_keys_removable(): - print("unset %s from network %s" % (key, self["session"]["network"]), file=out_f) - del self._get_network_section(self["session"]["network"])[key] - self._persist() - - def session_to_dict(self): - session_identity, session_network = self.safe_get_session_identity_network_names() - - show = {"session", "network.%s" % session_network, "identity.%s" % session_identity, "ipfs"} - response = {f: dict(self[f]) for f in show} - return response - - def add_network(self, network, rpc_endpoint, default_gas_price): - network_section = "network.%s" % network - if network_section in self: - raise Exception("Network section %s already exists in config" % network) - - self[network_section] = {} - self[network_section]["default_eth_rpc_endpoint"] = str(rpc_endpoint) - # TODO: find solution with default gas price - self[network_section]["default_gas_price"] = str(default_gas_price) - self._persist() - - def set_network_field(self, network, key, value): - self._get_network_section(network)[key] = str(value) - self._persist() - - def add_identity(self, identity_name, identity, out_f=sys.stdout): - identity_section = "identity.%s" % identity_name - if identity_section in self: - raise Exception("Identity section %s already exists in config" % identity_section) - if "network" in identity and identity["network"] not in self.get_all_networks_names(): - raise Exception("Network %s is not in config" % identity["network"]) - self[identity_section] = identity - self._persist() - # switch to it, if it was the first identity - if len(self.get_all_identities_names()) == 1: - print("You've just added your first identity %s. We will automatically switch to it!" % identity_name) - self.set_session_identity(identity_name, out_f) - - def set_identity_field(self, identity, key, value): - self._get_identity_section(identity)[key] = str(value) - self._persist() - - def _get_network_section(self, network): - """ return section for network or identity """ - return self["network.%s" % network] - - def _get_identity_section(self, identity): - """ return section for the specific identity """ - return self["identity.%s" % identity] - - def get_ipfs_endpoint(self): - return self["ipfs"]["default_ipfs_endpoint"] - - def set_ipfs_endpoint(self, ipfs_endpoint): - self["ipfs"]["default_ipfs_endpoint"] = ipfs_endpoint - self._persist() - - def get_all_identities_names(self): - return [x[len("identity."):] for x in self.sections() if x.startswith("identity.")] - - def get_all_networks_names(self): - return [x[len("network."):] for x in self.sections() if x.startswith("network.")] - - def delete_identity(self, identity_name): - if identity_name not in self.get_all_identities_names(): - raise Exception("identity_name {} does not exist".format(identity_name)) - - session_identity, _ = self.safe_get_session_identity_network_names() - if identity_name == session_identity: - raise Exception("identity_name {} is in use".format(identity_name)) - self.remove_section("identity.{}".format(identity_name)) - self._persist() - - def create_default_config(self): - """ Create default configuration if config file does not exist """ - # make config directory with the minimal possible permission - self._config_file.parent.mkdir(mode=0o700, exist_ok=True) - self["network.mainnet"] = { - "default_eth_rpc_endpoint": "https://mainnet.infura.io/v3/09027f4a13e841d48dbfefc67e7685d5" - } - self["network.goerli"] = { - "default_eth_rpc_endpoint": "https://goerli.infura.io/v3/09027f4a13e841d48dbfefc67e7685d5", +class Config: + def __init__(self, + private_key, + eth_rpc_endpoint, + wallet_index=0, + ipfs_endpoint=None, + concurrency=True, + force_update=False, + mpe_contract_address=None, + token_contract_address=None, + registry_contract_address=None, + signer_private_key=None): + self.__config = { + "private_key": private_key, + "eth_rpc_endpoint": eth_rpc_endpoint, + "wallet_index": wallet_index, + "ipfs_endpoint": ipfs_endpoint if ipfs_endpoint else "/dns/ipfs.singularitynet.io/tcp/80/", + "concurrency": concurrency, + "force_update": force_update, + "mpe_contract_address": mpe_contract_address, + "token_contract_address": token_contract_address, + "registry_contract_address": registry_contract_address, + "signer_private_key": signer_private_key } - self["network.sepolia"] = { - "default_eth_rpc_endpoint": "https://sepolia.infura.io/v3/09027f4a13e841d48dbfefc67e7685d5", - } - self["ipfs"] = {"default_ipfs_endpoint": "/dns/ipfs.singularitynet.io/tcp/80/"} - network = self.get_param_from_sdk_config("network") - if network: - if network not in self.get_all_networks_names(): - raise Exception("Network '%s' is not in config" % network) - self["session"] = {"network": network} - else: - self["session"] = {"network": DEFAULT_NETWORK} - identity_name = self.get_param_from_sdk_config("identity_name") - identity_type = self.get_param_from_sdk_config("identity_type") - if identity_name and identity_type: - identity = self.setup_identity() - self.add_identity(identity_name, identity) - self._persist() - print("We've created configuration file with default values in: %s\n" % str(self._config_file)) - - def _check_section(self, s): - if s not in self: - raise Exception("Config error, section %s is absent" % s) - - def _persist(self): - with open(self._config_file, "w") as f: - self.write(f) - self._config_file.chmod(0o600) - - def get_param_from_sdk_config(self, param: str, alternative=None): - if self.sdk_config: - return self.sdk_config.get(param, alternative) - return None - - def setup_identity(self): - identity_type = self.get_param_from_sdk_config("identity_type") - private_key = self.get_param_from_sdk_config("private_key") - default_wallet_index = self.get_param_from_sdk_config("wallet_index", 0) - if not identity_type: - raise Exception("identity_type not passed") - if identity_type == "key": - identity = { - "identity_type": "key", - "private_key": private_key, - "default_wallet_index": default_wallet_index - } - # TODO: logic for other identity_type - else: - print("\nThe identity_type parameter value you passed is not supported " - "by the sdk at this time.\n") - print("The available identity types are:\n" - " - 'key' (uses a required hex-encoded private key for signing)\n\n") - exit(1) - return identity - -def first_identity_message_and_exit(is_sdk=False): - if is_sdk: - print("\nPlease create your first identity by passing the 'identity_name' " - "and 'identity_type' parameters in SDK config.\n") - print("The available identity types are:\n" - " - 'key' (uses a required hex-encoded private key for signing)\n\n") - else: - print("\nPlease create your first identity by running 'snet identity create'.\n\n") - print("The available identity types are:\n" - " - 'rpc' (yields to a required ethereum json-rpc endpoint for signing using a given wallet\n" - " index)\n" - " - 'mnemonic' (uses a required bip39 mnemonic for HDWallet/account derivation and signing\n" - " using a given wallet index)\n" - " - 'key' (uses a required hex-encoded private key for signing)\n" - " - 'ledger' (yields to a required ledger nano s device for signing using a given wallet\n" - " index)\n" - " - 'trezor' (yields to a required trezor device for signing using a given wallet index)\n" - "\n") - exit(1) + def __getitem__(self, key): + return self.__config[key] + def get(self, key, default=None): + return self.__config.get(key, default) -def get_session_identity_keys(): - return ["default_wallet_index"] - - -def get_session_network_keys(): - return ["default_gas_price", "current_registry_at", "current_multipartyescrow_at", "current_singularitynettoken_at", - "default_eth_rpc_endpoint"] - - -def get_session_network_keys_removable(): - return ["default_gas_price", "current_registry_at", "current_multipartyescrow_at", "current_singularitynettoken_at"] + def get_ipfs_endpoint(self): + return self["ipfs_endpoint"] -def get_session_keys(): - return get_session_network_keys() + get_session_identity_keys() + ["default_ipfs_endpoint"] From 4418f91db5b4e2fffaa1913fbc058d9b7f3ac254 Mon Sep 17 00:00:00 2001 From: Arondondon Date: Fri, 20 Sep 2024 18:31:49 +0300 Subject: [PATCH 53/55] Changes in documentation, examples and README files related to the config changes. --- README.md | 40 +- docs/main/config.md | 429 ++---------------- docs/main/init.md | 41 +- examples/calculator.py | 13 +- examples/console_app.py | 13 +- examples/examples_docs/calculator.md | 15 +- examples/examples_docs/console_app.md | 15 +- snet/sdk/__init__.py | 3 +- testcases/functional_tests/test_sdk_client.py | 13 +- 9 files changed, 86 insertions(+), 496 deletions(-) diff --git a/README.md b/README.md index 5f2fefc..57c5998 100644 --- a/README.md +++ b/README.md @@ -34,33 +34,31 @@ Once you have installed snet-sdk in your current environment, you can import it instance of the base sdk class: ```python from snet import sdk -config = { - "private_key": 'YOUR_PRIVATE_WALLET_KEY', - "eth_rpc_endpoint": f"https://sepolia.infura.io/v3/YOUR_INFURA_KEY", - "email": "your@email.com", - "concurrency": False, - "identity_name": "local_name_for_that_identity", - "identity_type": "key", - "network": "sepolia", - "force_update": False - } + +config = sdk.config.Config(private_key="YOUR_PRIVATE_KEY", + eth_rpc_endpoint=f"https://sepolia.infura.io/v3/YOUR_INFURA_KEY", + concurrency=False, + force_update=False) snet_sdk = sdk.SnetSDK(config) ``` -The `config` parameter is a Python dictionary. -See [test_sdk_client.py](https://github.com/singnet/snet-sdk-python/blob/master/testcases/functional_tests/test_sdk_client.py) +The `config` parameter is an instance of the `Config` class. +See [config.py](https://github.com/singnet/snet-sdk-python/blob/master/docs/main/config.md) for a reference. -##### Config options description - -- private_key: Your wallet's private key that will be used to pay for calls. Is **required** to make a call; -- eth_rpc_endpoint: RPC endpoint that is used to access the Ethereum network. Is **required** to make a call; -- email: Your email; -- identity_name: Name that will be used locally to save your wallet settings. You can check your identities in the `~/.snet/config` file; -- identity_type: Type of your wallet authentication. Note that snet-sdk currently supports only "key" identity_type; -- network: You can set the Ethereum network that will be used to make a call; -- force_update: If set to False, will reuse the existing gRPC stubs (if any) instead of downloading proto and regenerating them every time. +##### Config parameters description + +- `private_key`: Your wallet's private key that will be used to pay for calls. Is **required** in config; +- `eth_rpc_endpoint`: RPC endpoint that is used to access the Ethereum network. Is **required** in config; +- `wallet_index`: The index of the wallet that will be used to pay for calls; +- `ipfs_endpoint`: IPFS endpoint that is used to access IPFS; +- `concurrency`: If set to True, will enable concurrency for the SDK; +- `force_update`: If set to False, will reuse the existing gRPC stubs (if any) instead of downloading proto and regenerating them every time. +- `mpe_contract_address`: The address of the Multi-Party Escrow smart contract; +- `token_contract_address`: The address of the SingularityNET token smart contract; +- `registry_contract_address`: The address of the Registry smart contract; +- `signer_private_key`: The private key of the signer. Used to sign the service call. Equals to `private_key` by default. #### List organizations and their services diff --git a/docs/main/config.md b/docs/main/config.md index 89a2ed2..8954be0 100644 --- a/docs/main/config.md +++ b/docs/main/config.md @@ -5,40 +5,14 @@ Entities: 1. [Config](#class-config) - [\_\_init\_\_](#__init__) - - [get_session_network_name](#get_session_network_name) - - [safe_get_session_identity_network_names](#safe_get_session_identity_network_names) - - [set_session_network](#set_session_network) - - [_set_session_network](#_set_session_network) - - [set_session_identity](#set_session_identity) - - [get_session_field](#get_session_field) - - [set_session_field](#set_session_field) - - [unset_session_field](#unset_session_field) - - [session_to_dict](#session_to_dict) - - [add_network](#add_network) - - [set_network_field](#set_network_field) - - [add_identity](#add_identity) - - [set_identity_field](#set_identity_field) - - [_get_network_section](#_get_network_section) - - [_get_identity_section](#_get_identity_section) + - [\_\_getitem\_\_](#__getitem__) + - [get](#get) - [get_ipfs_endpoint](#get_ipfs_endpoint) - - [set_ipfs_endpoint](#set_ipfs_endpoint) - - [get_all_identities_names](#get_all_identities_names) - - [get_all_networks_names](#get_all_networks_names) - - [delete_identity](#delete_identity) - - [create_default_config](#create_default_config) - - [_check_section](#_check_section) - - [_persist](#_persist) - - [get_param_from_sdk_config](#get_param_from_sdk_config) - - [setup_identity](#setup_identity) -2. [first_identity_message_and_exit](#function-first_identity_message_and_exit) -3. [get_session_identity_keys](#function-get_session_identity_keys) -4. [get_session_network_keys](#function-get_session_network_keys) -5. [get_session_network_keys_removable](#function-get_session_network_keys_removable) -6. [get_session_keys](#function-get_session_keys) + ### Class `Config` -extends: `ConfigParser` +extends: - is extended by: - @@ -48,395 +22,76 @@ This is a configuration manager for the SDK. It is responsible for handling conf #### attributes -- `_config_file` (Path): The path to the configuration file. -- `sdk_config` (dict): The SDK configuration. -- `is_sdk` (bool): Whether the configuration is for the SDK. +- `__config` (dict): The dictionary containing: + - `private_key` (str): Your wallet's private key that will be used to pay for calls. Is **required** in config. + - `eth_rpc_endpoint` (str): RPC endpoint that is used to access the Ethereum network. Is **required** in config. + - `wallet_index` (int): The index of the wallet that will be used to pay for calls. + - `ipfs_endpoint` (str): IPFS endpoint that is used to access IPFS. Defaults to _"/dns/ipfs.singularitynet.io/tcp/80/"_. + - `concurrency` (bool): If set to True, will enable concurrency for the SDK. + - `force_update` (bool): If set to False, will reuse the existing gRPC stubs (if any) instead of downloading proto +and regenerating them every time. + - `mpe_contract_address` (str): The address of the Multi-Party Escrow smart contract. + - `token_contract_address` (str): The address of the SingularityNET token smart contract. + - `registry_contract_address` (str): The address of the Registry smart contract. + - `signer_private_key` (str): The private key of the signer. Used to sign the service call. Equals to `private_key` +by default. #### methods #### `__init__` -Initializes a new instance of the class. Reads the configuration file or creates a default one if it doesn't exist. -Initializes attributes by the passed arguments. - -###### args: - -- `_snet_folder` (Path): The path to the folder where the configuration file is located. Defaults to "~/.snet". -- `sdk_config` (dict): The SDK configuration. Defaults to _None_. - -###### returns: - -- _None_ - -#### `get_session_network_name` - -Returns the name of the session network. - -###### returns: - -- (str): The name of the session network. - -#### `safe_get_session_identity_network_names` - -Returns the names of the session network and identity. - -###### returns: - -- The names of the session network and identity. (str, str) - -###### raises: - -- Exception: If the session identity does not bind to the session network. - -#### `set_session_network` - -Sets the session network using `_set_session_network`. +Initializes a new instance of the class. Sets `__config` fields from passed arguments. ###### args: -- `network` (str): The name of the session network. -- `out_f` (TextIO): The output to write messages to. +- `private_key` (str): Your wallet's private key that will be used to pay for calls. Is **required** in config. +- `eth_rpc_endpoint` (str): RPC endpoint that is used to access the Ethereum network. Is **required** in config. +- `wallet_index` (int): The index of the wallet that will be used to pay for calls. Defaults to _0_. +- `ipfs_endpoint` (str): IPFS endpoint that is used to access IPFS. Defaults to _None_. +- `concurrency` (bool): If set to True, will enable concurrency for the SDK. Defaults to _True_. +- `force_update` (bool): If set to False, will reuse the existing gRPC stubs (if any) instead of downloading proto +and regenerating them every time. Defaults to _False_. +- `mpe_contract_address` (str): The address of the Multi-Party Escrow smart contract. Defaults to _None_. +- `token_contract_address` (str): The address of the SingularityNET token smart contract. Defaults to _None_. +- `registry_contract_address` (str): The address of the Registry smart contract. Defaults to _None_. +- `signer_private_key` (str): The private key of the signer. Used to sign the service call. Equals to `private_key` +by default. ###### returns: - _None_ -#### `_set_session_network` +#### `__getitem__` -Sets the session network. +Overrides the `__getitem__` Python special method. Returns the value associated with the given key from +the `__config` dict. ###### args: -- `network` (str): The name of the session network. -- `out_f` (TextIO): The output to write messages to. +- `key` (str): The key of the value to retrieve. ###### returns: -- _None_ - -###### raises: - -- Exception: If the network is not in the config. +- The value associated with the given key. (Any) -#### `set_session_identity` +#### `get` -Sets the session identity. +Returns the value associated with the given key from the `__config` dict or the default value if the key is not found. ###### args: -- `identity` (str): The name of the session identity. -- `out_f` (TextIO): The output to write messages to. +- `key` (str): The key of the value to retrieve. +- `default` (Any): The default value to return if the key is not found. Defaults to _None_. ###### returns: -- _None_ - -###### raises: - -- Exception: If the identity is not in the config. - -#### `get_session_field` - -Retrieves a session field based on the provided key. - -###### args: - -- `key` (str): The key of the session field to retrieve. -- `exception_if_not_found` (bool): Whether to raise an exception if the field is not found. Defaults to _True_. - -###### returns: - -- The value of the session field if found, otherwise _None_. (str | None) - -###### raises: - -- `Exception`: If the field is not found and `exception_if_not_found` is _True_. - -#### `set_session_field` - -Sets a session field based on the provided key and value. - -###### args: - -- `key` (str): The key of the session field to set. -- `value` (str): The value of the session field to set. -- `out_f` (TextIO): The output to write messages to. - -###### returns: - -- _None_ - -###### raises: - -- `Exception`: If the key is not in the config. - -#### `unset_session_field` - -Unsets a session field based on the provided key. - -###### args: - -- `key` (str): The key of the session field to unset. -- `out_f` (TextIO): The output to write messages to. - -###### returns: - -- _None_ - -#### `session_to_dict` - -Converts the session configuration to a dictionary. - -###### returns: - -- The session configuration as a dictionary. (dict) - -#### `add_network` - -Adds a new network configuration to the existing config. - -###### args: - -- `network` (str): The name of the network to add. -- `rpc_endpoint` (str): The RPC endpoint of the network. -- `default_gas_price` (str): The default gas price of the network. - -###### returns: - -- _None_ - -###### raises: - -- `Exception`: If the specified network section already exists in config. - -#### `set_network_field` - -Sets a network field based on the provided key and value. - -###### args: - -- `network` (str): The name of the network to set. -- `key` (str): The key of the network field to set. -- `value` (str): The value of the network field to set. - -###### returns: - -- _None_ - -#### `add_identity` - -Adds a new identity configuration to the existing config. - -###### args: - -- `identity_name` (str): The name of the identity to add. -- `identity` (dict): The identity configuration. -- `out_f` (TextIO): The output to write messages to. Defaults to _sys.stdout_. - -###### returns: - -- _None_ - -###### raises: - -- `Exception`: If the specified identity section already exists in config or if network of the identity is not in config. - -#### `set_identity_field` - -Sets an identity field based on the provided key and value. - -###### args: - -- `identity` (str): The name of the identity to set. -- `key` (str): The key of the identity field to set. -- `value` (str): The value of the identity field to set. - -###### returns: - -- _None_ - -#### `_get_network_section` - -Returns the config section for the specified network. - -###### args: - -- `network` (str): The name of the network. - -###### returns: - -- The config section for the specified network. (dict | SectionProxy) - -#### `_get_identity_section` - -Returns the config section for the specified identity. - -###### args: - -- `identity` (str): The name of the identity. - -###### returns: - -- The config section for the specified identity. (dict | SectionProxy) +- The value associated with the given key or the default value if the key is not found. (Any) #### `get_ipfs_endpoint` -Returns default IPFS endpoint from config. - -###### returns: - -- The default IPFS endpoint. (str) - -#### `set_ipfs_endpoint` - -Sets default IPFS endpoint in config. - -###### args: - -- `ipfs_endpoint` (str): The IPFS endpoint to set. - -###### returns: - -- _None_ - -#### `get_all_identities_names` - -Returns all identity names from config. - -###### returns: - -- The list of identity names. (list[str]) - -#### `get_all_networks_names` - -Returns all network names from config. - -###### returns: - -- The list of network names. (list[str]) - -#### `delete_identity` - -Deletes an identity from config. - -###### args: - -- `identity_name` (str): The name of the identity to delete. - -###### returns: - -- _None_ - -###### raises: - -- `Exception`: If the specified identity name does not exist in config or if the specified identity name is in use. - -#### `create_default_config` - -Creates default configuration if config file does not exist. Determines default endpoints for each network and IPFS. -Sets identity if it is provided by sdk. - -###### returns: - -- _None_ - -#### `_check_section` - -Checks if the specified section exists in config, raises an exception if it does not. - -###### args: - -- `s` (sts): The section name to check. - -###### returns: - -- _None_ - -###### raises: - -- `Exception`: If the specified section does not exist in config. - -#### `_persist` - -Persists the config to disk. - -###### returns: - -- _None_ - -#### `get_param_from_sdk_config` - -Returns the value of the specified parameter from the `sdk_config` dict. - -###### args: - -- `param` (str): The name of the parameter to get. -- `alternative` (any): The value to return if the parameter is not found in `sdk_config`. Defaults to _None_. - -###### returns: - -- The value of the parameter or `alternative` if the parameter is not found or _None_ if `sdk_config` is not set. (Any) - -#### `setup_identity` - -Sets up and returns a new identity in the config with the parameters from the `sdk_config`. - -_Note_: currently, only `key` identity type is supported. - -###### returns: - -- The identity as a dict. (dict) - -###### raises: - -- Exception: If the `identity_type` is not passed in the `sdk_config`. - -### Function `first_identity_message_and_exit` - -Prints a message instructing the user to create their first identity and then exits the program. -The message differs depending on whether the `is_sdk` parameter is True or False, indicating whether the function -is being called from an SDK or not. The message lists the available identity types and how to create an identity. - -###### args: - -- `is_sdk` (bool): Whether the function is being called from an SDK. - -###### returns: - -- _None_ - -### Function `get_session_identity_keys` - -Returns a list with the only one element "default_wallet_index". - -###### returns: - -- List with the only one element. (list[str]) - -### Function `get_session_network_keys` - -Returns a list with the elements "default_gas_price", "current_registry_at", "current_multipartyescrow_at", -"current_singularitynettoken_at" and "default_eth_rpc_endpoint". - -###### returns: - -- List with the elements. (list[str]) - -### Function `get_session_network_keys_removable` - -Returns a list with the elements "default_gas_price", "current_registry_at", "current_multipartyescrow_at" and -"current_singularitynettoken_at". - -###### returns: - -- List with the elements. (list[str]) - -### Function `get_session_keys` - -Returns a list with the elements from the `get_session_identity_keys` and `get_session_network_keys` methods, plus -the "default_ipfs_endpoint". +Returns the `ipfs_endpoint` field value from the `__config` dict. ###### returns: -- List with the elements. (list[str]) +- The IPFS endpoint. (str) diff --git a/docs/main/init.md b/docs/main/init.md index b6379c8..d97949b 100644 --- a/docs/main/init.md +++ b/docs/main/init.md @@ -5,8 +5,6 @@ Entities: 1. [SnetSDK](#class-snetsdk) - [\_\_init\_\_](#__init__) - - [setup_config](#setup_config) - - [set_session_identity](#set_session_identity) - [create_service_client](#create_service_client) - [get_service_stub](#get_service_stub) - [get_path_to_pb_files](#get_path_to_pb_files) @@ -31,7 +29,7 @@ It provides methods for creating service clients, managing identities, and confi #### attributes -- `_sdk_config` (dict): The SDK configuration. +- `_sdk_config` (Config): An instance of the `Config` class. - `_metadata_provider` (MetadataProvider): An instance of the `MetadataProvider` class. _Note_: There is currently only one implementation of `MetadataProvider` which is `IPFSMetadataProvider`, so this attribute can only be initialized to `IPFSMetadataProvider` at this time. @@ -52,51 +50,16 @@ Instantiates the MPE contract with the specified contract address if provided, o Instantiates the IPFS client with the specified IPFS endpoint if provided, otherwise uses the default IPFS endpoint. Instantiates the Registry contract with the specified contract address if provided, otherwise uses the default Registry contract. Instantiates the Account object with the specified Web3 client, SDK configuration, and MPE contract. -Creates an instance of the "Config" class, passing `_sdk_config` to it, and calls the `setup_config` method with this -instance as an argument. ###### args: -- `sdk_config` (dict): A dictionary containing the SDK configuration. +- `sdk_config` (Config): A `Config` object containing the SDK configuration. - `metadata_provider` (MetadataProvider): A `MetadataProvider` object. Defaults to _None_. ###### returns: - _None_ -#### `setup_config` - -Sets up the configuration for the SnetSDK instance. This function checks the network and identity_name in -the configuration and sets up the configuration accordingly. If the network is specified, and it is different from -the current session network, it sets the session network. If the identity_name is specified, it sets the session identity. -If there are no identities in the configuration, it checks if the identity is selected and raises an exception if not. - -###### args: - -- config (Config): The `snet.cli.config.Congig` instance. - -###### returns: - -- _None_ - -###### raises: - -- Exception: If the identity name is not passed or selected. - -#### `set_session_identity` - -Sets the session identity in the given config. - -###### args: - -- `identity_name` (str): The name of the identity to set. -- `config` (Config): The `snet.cli.config.Congig` object to modify. -- `out_f` (TextIO): The output to write messages to. - -###### returns: - -- _None_ - #### `create_service_client` If `force_update` is True or if there are no gRPC stubs for the given service, the proto files are loaded diff --git a/examples/calculator.py b/examples/calculator.py index 18d0924..6e3f5e8 100644 --- a/examples/calculator.py +++ b/examples/calculator.py @@ -1,14 +1,9 @@ from snet import sdk -config = { - "private_key": 'APP_PROVIDER_PRIVATE_KEY', - "eth_rpc_endpoint": f"https://sepolia.infura.io/v3/INFURE_API_KEY", - "concurrency": False, - "identity_name": "NAME", - "identity_type": "key", - "network": "sepolia", - "force_update": False -} +config = sdk.config.Config(private_key="YOUR_PRIVATE_KEY", + eth_rpc_endpoint=f"https://sepolia.infura.io/v3/YOUR_INFURA_KEY", + concurrency=False, + force_update=False) operators = { "+": "add", diff --git a/examples/console_app.py b/examples/console_app.py index 9015c5f..588bdd3 100644 --- a/examples/console_app.py +++ b/examples/console_app.py @@ -304,15 +304,10 @@ def extend_expiration(): SDK configuration that is configured by the application provider. To run the application you need to change the 'private_key', 'eth_rpc_endpoint' and 'identity_name' values. """ -config = { - "private_key": 'APP_PROVIDER_PRIVATE_KEY', - "eth_rpc_endpoint": f"https://sepolia.infura.io/v3/APP_PROVIDER_INFURA_API_KEY", - "concurrency": False, - "identity_name": "NAME", - "identity_type": "key", - "network": "sepolia", - "force_update": False -} +config = sdk.config.Config(private_key="YOUR_PRIVATE_KEY", + eth_rpc_endpoint=f"https://sepolia.infura.io/v3/YOUR_INFURA_KEY", + concurrency=False, + force_update=False) snet_sdk = sdk.SnetSDK(config) # the 'SnetSDK' instance initialized_services = [] # the list of initialized service clients diff --git a/examples/examples_docs/calculator.md b/examples/examples_docs/calculator.md index 937baaa..87fec3e 100644 --- a/examples/examples_docs/calculator.md +++ b/examples/examples_docs/calculator.md @@ -43,20 +43,15 @@ _Note:_ don't forget to import `snet.sdk` package. ```python from snet import sdk -config = { - "private_key": 'APP_PROVIDER_PRIVATE_KEY', - "eth_rpc_endpoint": f"https://sepolia.infura.io/v3/INFURE_API_KEY", - "concurrency": False, - "identity_name": "NAME", - "identity_type": "key", - "network": "sepolia", - "force_update": False -} +config = sdk.config.Config(private_key="YOUR_PRIVATE_KEY", + eth_rpc_endpoint=f"https://sepolia.infura.io/v3/YOUR_INFURA_KEY", + concurrency=False, + force_update=False) snet_sdk = sdk.SnetSDK(config) ``` -Here you need to set private values: `private_key`, `eth_rpc_endpoint`, `identity_name` and possibly change some others. +Here you need to set private values: `private_key`, `eth_rpc_endpoint`and possibly change some others. Calculator service is deployed on the sepolia network. To create a client of this service we need to pass `org_id`, `service_id` and `group_name` to `create_service_client` method: diff --git a/examples/examples_docs/console_app.md b/examples/examples_docs/console_app.md index 6ba28de..01970d5 100644 --- a/examples/examples_docs/console_app.md +++ b/examples/examples_docs/console_app.md @@ -79,20 +79,15 @@ from snet import sdk SDK configuration that is configured by the application provider. To run the application you need to change the 'private_key', 'eth_rpc_endpoint' and 'identity_name' values. """ -config = { - "private_key": 'APP_PROVIDER_PRIVATE_KEY', - "eth_rpc_endpoint": f"https://sepolia.infura.io/v3/APP_PROVIDER_INFURA_API_KEY", - "concurrency": False, - "identity_name": "NAME", - "identity_type": "key", - "network": "sepolia", - "force_update": False -} +config = sdk.config.Config(private_key="YOUR_PRIVATE_KEY", + eth_rpc_endpoint=f"https://sepolia.infura.io/v3/YOUR_INFURA_KEY", + concurrency=False, + force_update=False) snet_sdk = sdk.SnetSDK(config) # the 'SnetSDK' instance ``` -Here you need to set private values: `private_key`, `eth_rpc_endpoint`, `identity_name` and possibly change some others. +Here you need to set private values: `private_key`, `eth_rpc_endpoint` and possibly change some others. #### Global variables diff --git a/snet/sdk/__init__.py b/snet/sdk/__init__.py index c3b0ca4..fd52bf2 100644 --- a/snet/sdk/__init__.py +++ b/snet/sdk/__init__.py @@ -50,8 +50,7 @@ def __init__(self, sdk_config: Config, metadata_provider=None): self._metadata_provider = metadata_provider # Instantiate Ethereum client - eth_rpc_endpoint = self._sdk_config.get("eth_rpc_endpoint", - "https://mainnet.infura.io/v3/e7732e1f679e461b9bb4da5653ac3fc2") + eth_rpc_endpoint = self._sdk_config["eth_rpc_endpoint"] eth_rpc_request_kwargs = self._sdk_config.get("eth_rpc_request_kwargs") provider = web3.HTTPProvider(endpoint_uri=eth_rpc_endpoint, request_kwargs=eth_rpc_request_kwargs) diff --git a/testcases/functional_tests/test_sdk_client.py b/testcases/functional_tests/test_sdk_client.py index 6a3069d..4028ab9 100644 --- a/testcases/functional_tests/test_sdk_client.py +++ b/testcases/functional_tests/test_sdk_client.py @@ -23,15 +23,10 @@ def tearDown(self): def get_test_service_data(): - config = { - "private_key": os.environ['SNET_TEST_WALLET_PRIVATE_KEY'], - "eth_rpc_endpoint": f"https://sepolia.infura.io/v3/{os.environ['SNET_TEST_INFURA_KEY']}", - "concurrency": False, - "identity_name": "test", - "identity_type": "key", - "network": "sepolia", - "force_update": False - } + config = sdk.config.Config(private_key=os.environ['SNET_TEST_WALLET_PRIVATE_KEY'], + eth_rpc_endpoint=f"https://sepolia.infura.io/v3/{os.environ['SNET_TEST_INFURA_KEY']}", + concurrency=False, + force_update=False) snet_sdk = sdk.SnetSDK(config) From ea97b5f74040c834a65fb3bd1facd9de0e1476fb Mon Sep 17 00:00:00 2001 From: Arondondon Date: Thu, 26 Sep 2024 11:57:36 +0300 Subject: [PATCH 54/55] Fixed issues: grammatical in service_metadata.md and logical in calculator.py --- docs/metadata_provider/service_metadata.md | 4 ++-- examples/calculator.py | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/metadata_provider/service_metadata.md b/docs/metadata_provider/service_metadata.md index 2b7d346..d06f1c2 100644 --- a/docs/metadata_provider/service_metadata.md +++ b/docs/metadata_provider/service_metadata.md @@ -104,7 +104,7 @@ This class represents the service metadata. #### `__init__` -Initializes a new instance of the class. Initializes the `m` dict with empty of default values. +Initializes a new instance of the class. Initializes the `m` dict with empty or default values. ###### returns: @@ -272,7 +272,7 @@ If the asset type is not supported, an exception is raised. #### `add_endpoint_to_group` Checks the endpoint is valid and adds it to the `endpoints` field of the specified payment group in the `m` dict. -If the endpoint is not valid of if the group does not exist or if the endpoint is already present, an exception is +If the endpoint is not valid or if the group does not exist or if the endpoint is already present, an exception is raised. ###### args: diff --git a/examples/calculator.py b/examples/calculator.py index 6e3f5e8..d9404ce 100644 --- a/examples/calculator.py +++ b/examples/calculator.py @@ -22,11 +22,12 @@ def parse_expression(expression): if len(elements) != 3: raise Exception(f"Invalid expression '{expression}'. Three items required.") - a = int(elements[0]) - b = int(elements[2]) if elements[1] not in ["+", "-", "*", "/"]: - raise Exception(f"Invalid expression '{expression}'. Operation must be '+' or '-' or '*' or '/'") - elif not isinstance(a, (float, int)) or not isinstance(b, (float, int)): + raise Exception(f"Invalid expression '{expression}'. Operation must be '+' or '-' or '*' or '/'.") + try: + a = float(elements[0]) + b = float(elements[2]) + except ValueError: raise Exception(f"Invalid expression '{expression}'. Operands must be integers or floating point numbers.") op = elements[1] From 2312606c10300a5bae7849b47206bc4eaedc968c Mon Sep 17 00:00:00 2001 From: Kirill <65371121+kiruxaspb@users.noreply.github.com> Date: Thu, 26 Sep 2024 16:32:46 +0300 Subject: [PATCH 55/55] upgrade version to 3.5.0 --- version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.py b/version.py index a5cfdf5..dcbfb52 100644 --- a/version.py +++ b/version.py @@ -1 +1 @@ -__version__ = "3.4.1" +__version__ = "3.5.0"