Skip to content

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:

powershell
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:

sh
pip install snet.sdk

Configuration

Firstly, we need to configure the SDK and service client. We create a configuration dictionary and initialize the SDK instance with it.

Note: Don't forget to import the snet.sdk package.

python
from snet import sdk

"""
SDK configuration provided by the application provider.
To run the application, replace 'private_key' and 'eth_rpc_endpoint' with your values.
"""
config = sdk.config.Config(
    private_key="YOUR_PRIVATE_KEY",  # Replace with your Ethereum private key
    eth_rpc_endpoint="https://eth-sepolia.g.alchemy.com/v2/YOUR_ALCHEMY_API_KEY",  # Replace with your Alchemy API key
    concurrency=False, 
    force_update=False 
)

# Initialize the SnetSDK instance
snet_sdk = sdk.SnetSDK(config)

What to Replace

  • private_key: Your Ethereum account private key for signing transactions.
  • eth_rpc_endpoint: Use the Alchemy RPC endpoint with your API key:
    https://eth-sepolia.g.alchemy.com/v2/YOUR_ALCHEMY_API_KEY

To get your Alchemy API Key, follow this guide.

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 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 to GitHub.