Topology updater for cardano node on Windows

Hi developers,

i needs some detailed support please. I created some native asset on Cardano, using Cardano cli on Windows. Wanted then to withdrawal some of the token to another address. That worked so far. Once the token were sent, my node status was at 99.99% and showed invalid blocks error and couldn’t get over that percentage until i restarted the node.

Then i did a little bit of research and noticed that i might need to update my mainnet-topology.json file which still just points to IOHK relay node, to fix that node status issue:

{
  "Producers": [
    {
      "addr": "relays-new.cardano-mainnet.iohk.io",
      "port": 3001,
      "valency": 2
    }
  ]
}

So i found on this Github here some mainnet topology updater script, which didnt work initially so i did some modifications with ChatGPT. Code below.

when i run the topologyUpdater.py i assume my mainnet-topology.json file should get modified, but it doesnt.

Am i on the correct path here?

Thank you

PS C:\Users\username\AppData\Roaming\Cardano> python .\topologyUpdater.py

############################################################
# Reminder:                                                #
# - Ensure VPN is disabled when running this script        #
# - Make sure topologyUpdater.json is correctly configured #
# - Cardano node must be running                           #
############################################################

{ "resultcode": "201", "datetime":"2024-02-01 19:12:50", "clientIp": "my external IP address", "iptype": 4, "msg": "nice to meet you" }

topologyUpdater.py@01.02.2024 20:12:52: got bad resultcode = 402 for fetch command
PS C:\Users\username\AppData\Roaming\Cardano>

topologyUpdater.py

#!/usr/bin/python3
#
# topologyUpdater as python variant

import os
import sys
import argparse
import requests
import json
import subprocess
import time
import smtplib
import email.utils
from email.mime.text import MIMEText
import socket
import requests.packages.urllib3.util.connection as urllib3_cn
from datetime import datetime

version = "0.8"

# define some default values upfront

cnode_valency_default = "1"
ipVersion_default = "4"             # other possible values are "6" or "mix"
max_peers_default = "15"
nwmagic_default = "764824073"       # network magic for mainnet - if you want a testnet then change the value in config file instead
metricsURL_default = "http://localhost:12798/metrics"

def timestamp4log():
    return datetime.now().strftime("%d.%m.%Y %H:%M:%S");

def sendmail(email_config,message):
    ts_message = os.path.basename(sys.argv[0]) + "@" + timestamp4log() + ": " + message
    if not bool(email_config):
        print(ts_message)
    else:
        msg = MIMEText(ts_message)
        msg['To'] = email.utils.formataddr(('Admin', email_config['msgTo']))
        msg['From'] = email.utils.formataddr(('Monitoring', email_config['msgFrom']))
        msg['Subject'] = email_config['msgSubject']
        server = smtplib.SMTP(email_config['smtpServer'], email_config['smtpPort'])
        #server.set_debuglevel(True) # show communication with the server
        try:
            server.sendmail(email_config['msgFrom'], [email_config['msgTo']], msg.as_string())
        finally:
            server.quit()
    return;

def exception_handler(exception_type, exception, traceback):
    # All your trace are belong to us!
    # your format
    sendmail(ec, exception_type.__name__ + ", " + str(exception))

def allowed_gai_family() -> socket.AddressFamily:
    family = socket.AF_INET
    return family;

def getconfig(configfile):
    with open(configfile) as f:
        data = json.load(f)
    return data;

def requestmetric(url):
    try:
        response = requests.get(url)
        response.raise_for_status()
    except requests.exceptions.HTTPError as errh:
        sendmail(ec, "Http Error: " + repr(errh))
        sys.exit()
    except requests.exceptions.ConnectionError as errc:
        sendmail(ec,+ "Error Connecting: " + repr(errc))
        sys.exit()
    except requests.exceptions.Timeout as errt:
        sendmail(ec, + "Timeout Error: " + repr(errt))
        sys.exit()
    except requests.exceptions.RequestException as err:
        sendmail(ec, + "OOps: Something Else " + repr(err))
        sys.exit()
    finally:
        return response;
        
def get_current_block_number():
    try:
        # Set the environment variable for CARDANO_NODE_SOCKET_PATH
        os.environ['CARDANO_NODE_SOCKET_PATH'] = '\\\\.\\pipe\\cardano-node'

        # Execute the cardano-cli command and capture the output
        cardano_cli_command = ['cardano-cli', 'query', 'tip', '--mainnet']
        result = subprocess.run(cardano_cli_command, capture_output=True, text=True, check=True)
        data = json.loads(result.stdout)
        return str(data['block'])
    except subprocess.CalledProcessError as e:
        sendmail(ec, "Error executing cardano-cli: " + str(e))
        return "0"  # Return a default value or handle the error appropriately
    except Exception as e:
        sendmail(ec, "General error: " + str(e))
        return "0"

def print_startup_message():
    print("\n############################################################")
    print("# Reminder:                                                #")
    print("# - Ensure VPN is disabled when running this script        #")
    print("# - Make sure topologyUpdater.json is correctly configured #")
    print("# - Cardano node must be running                           #")
    print("############################################################\n")

# main
if __name__ == "__main__":
    print_startup_message()

    ec = {}
    sys.excepthook = exception_handler

    # start with parsing arguments
    parser = argparse.ArgumentParser()
    parser.add_argument("-f", "--fetch", help="only fetch of a fresh topology file", action="store_true")
    parser.add_argument("-p", "--push", help="only node alive push to Topology Updater API", action="store_true")
    parser.add_argument("-v", "--version", help="displays version and exits", action="store_true")
    parser.add_argument("-c", "--config", help="path to config file in json format", default="topologyUpdater.json")

    args = parser.parse_args()
    myname = os.path.basename(sys.argv[0])

    if args.version:
        print(myname + " version " + version)
        sys.exit()

    if not args.push and not args.fetch:
        do_both = True
    else:
        do_both = False

    if args.config:
        my_config = getconfig(args.config)
        if 'email' in my_config:
           ec = my_config['email'] 
        if 'hostname' in my_config:
            cnode_hostname = my_config['hostname']
        else:
            sendmail(ec,"relay hostname parameter is missing in configuration")
            sys.exit()
        if 'port' in my_config:
            cnode_port = my_config['port']
        else:
            sendmail(ec,"relay port parameter is missing in configuration")
            sys.exit()
        if 'valency' in my_config:
            cnode_valency = my_config['valency']
        else:
            cnode_valency = cnode_valency_default
        if 'ipVersion' in my_config:
            ipVersion = my_config['ipVersion']
        else:
            ipVersion = ipVersion_default
        if 'maxPeers' in my_config:
            max_peers = my_config['maxPeers']
        else:
            max_peers = max_peers_default
        if 'metricsURL' in my_config:
            metricsURL = my_config['metricsURL']
        else:
            metricsURL = metricsURL_default
        if 'destinationTopologyFile' in my_config:
            destination_topology_file = my_config['destinationTopologyFile']
        else:
            sendmail(ec,"destination for topology file is missing in configuration")
            sys.exit()
        if 'logfile' in my_config:
            logfile = my_config['logfile']
        else:
            sendmail(ec,"path for logfile is missing in configuration")
            sys.exit()
        if 'networkMagic' in my_config:
            nwmagic = my_config['networkMagic']
        else:
            nwmagic = nwmagic_default

    # force to use ipv4 for version 4 configuration

    if ipVersion == "4":
        urllib3_cn.allowed_gai_family = allowed_gai_family

    if args.push or do_both:
        # first get last block number
        response = requestmetric(metricsURL)

        response_list = response.text.splitlines(True)
        metrics = {}
        for element in response_list:
            metric = element.rstrip('\n').split()
            metrics[metric[0]] = metric[1]

        # Get the current block number using cardano-cli
        block_number = get_current_block_number()

        # now register with central

        url = "https://api.clio.one/htopology/v1/?port=" + cnode_port + "&blockNo=" + block_number + "&valency=" + cnode_valency 
        url = url + "&magic=" + nwmagic + "&hostname=" + cnode_hostname 

        response = requests.get(url)
        print(response.text)
        log = open(logfile, "a")
        n = log.write(response.text)
        log.close()

        # check resultcode

        ok_responses = [ '201', '203', '204' ]
        parsed_response = json.loads(response.text)
        if (parsed_response['resultcode'] not in ok_responses):        # add 201 and 203 because those are starting codes
            sendmail(ec, "got bad resultcode = " + parsed_response['resultcode'] + " for push command")

    if args.fetch or do_both:
        url = "https://api.clio.one/htopology/v1/fetch/?max=" + max_peers + "&magic=" + nwmagic + "&ipv=" + ipVersion
        response = requests.get(url)
        parsed_response = json.loads(response.text)
        if (parsed_response['resultcode'] != '201'):
            sendmail(ec, "got bad resultcode = " + parsed_response['resultcode'] + " for fetch command")
            sys.exit()

        # Add custom peers
        for peer in my_config['customPeers']:
            parsed_response['Producers'].append(peer)

        # Write topology to file
        topology_file = open(destination_topology_file, "wt")
        n = topology_file.write(json.dumps(parsed_response, indent=4))
        topology_file.close()

topologyUpdater.json config:

{
  "maxPeers": "15",
  "metricsURL": "http://localhost:12798/metrics",
  "destinationTopologyFile": "C:\\Users\\username\\AppData\\Roaming\\Cardano\\configuration\\cardano\\mainnet-topology.json",
  "networkMagic": "764824073",
  "customPeers": [
    {
      "addr": "relays-new.cardano-mainnet.iohk.io",
      "port": 3001,
      "valency": 1
    }
  ],
  "hostname": "my external IP address",
  "port": "3001",
  "logfile": "C:\\Users\\username\\AppData\\Roaming\\Cardano\\configuration\\cardano\\topologyUpdater.log"
}

Below the python script source code to withdrawal the token.

sendFromWallet.py:

import subprocess
import os

def query_utxos(wallet_address):
    socket_path = "\\\\.\\pipe\\cardano-node"
    os.environ['CARDANO_NODE_SOCKET_PATH'] = socket_path
    cmd = f"cardano-cli query utxo --address {wallet_address} --mainnet"
    result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
    if result.returncode != 0:
        print("Error querying UTXOs. Make sure cardano-cli is installed and the wallet address is correct.")
        print("Error message:", result.stderr)
        exit(1)
    return result.stdout

def parse_utxos(utxo_data):
    utxos = {}
    for line in utxo_data.splitlines()[2:]:  # Skipping headers
        parts = line.split()
        tx_hash, tx_ix, *rest = parts
        utxos[(tx_hash, tx_ix)] = ' '.join(rest)
    return utxos

def extract_token_amount(token_string, full_token_identifier):
    # Split the token string at spaces and search for the full token identifier
    parts = token_string.split()
    for i, part in enumerate(parts):
        if full_token_identifier in part:
            # The token amount should be the part just before the full token identifier
            return int(parts[i - 1])
    return 0

def build_transaction(tx_hash, tx_ix, source_address, target_address, token_amount, full_token_identifier, lovelace_amount, total_source_lovelace, fee, total_source_token_amount):
    change_lovelace = total_source_lovelace - lovelace_amount - fee
    change_token_amount = total_source_token_amount - token_amount

    tx_out_change = f"{source_address}+{change_lovelace}"
    if change_token_amount > 0:
        tx_out_change += f'+"{change_token_amount} {full_token_identifier}"'

    tx_out_recipient = f"{target_address}+{lovelace_amount}"
    if token_amount > 0:
        tx_out_recipient += f'+"{token_amount} {full_token_identifier}"'

    tx_command = f'cardano-cli transaction build-raw --tx-in {tx_hash}#{tx_ix} --tx-out {tx_out_change} --tx-out {tx_out_recipient} --fee {fee} --out-file tx.raw'
    return tx_command

def calculate_fee(tx_draft_file):
    cmd = f"cardano-cli transaction calculate-min-fee --tx-body-file {tx_draft_file} --tx-in-count 1 --tx-out-count 2 --witness-count 1 --mainnet --protocol-params-file protocol.json"
    result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
    if result.returncode != 0:
        print("Error calculating fee. Make sure cardano-cli is installed and the parameters are correct.")
        print("Error message:", result.stderr)
        exit(1)
    fee = result.stdout.split()[0]
    return int(fee)

def sign_transaction(signing_key_file):
    cmd = f"cardano-cli transaction sign --tx-body-file tx.raw --signing-key-file {signing_key_file} --mainnet --out-file tx.signed"
    result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
    if result.returncode != 0:
        print("Error signing the transaction. Make sure the signing key file is correct.")
        print("Error message:", result.stderr)
        exit(1)

def submit_transaction():
    cmd = "cardano-cli transaction submit --tx-file tx.signed --mainnet"
    result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
    if result.returncode != 0:
        print("Error submitting the transaction.")
        print("Error message:", result.stderr)
        exit(1)

def main():
    wallet_address = "sourceAddress"
    target_address = "targetAddress"
    policy_id = "policyID"
    token_name = "tokenName"
    full_token_identifier = f"{policy_id}.{token_name}"
    token_amount = 10
    lovelace_amount = 2000000

    utxo_data = query_utxos(wallet_address)
    utxos = parse_utxos(utxo_data)

    print("\nAvailable UTXOs:")
    for (tx_hash, tx_ix), details in utxos.items():
        print(f"{tx_hash}#{tx_ix} : {details}")

    found_utxo = False
    for (tx_hash, tx_ix), details in utxos.items():
        details_parts = details.split('+')
        total_source_lovelace = int(details_parts[0].split()[0])
        total_source_token_amount = 0
        if full_token_identifier and len(details_parts) > 1:
            total_source_token_amount = extract_token_amount(details_parts[1], full_token_identifier)
            found_utxo = True

        if found_utxo:
            draft_tx_command = build_transaction(tx_hash, tx_ix, wallet_address, target_address, token_amount, full_token_identifier, lovelace_amount, total_source_lovelace, 0, total_source_token_amount)
            subprocess.run(draft_tx_command, shell=True)
            fee = calculate_fee("tx.raw")
            final_tx_command = build_transaction(tx_hash, tx_ix, wallet_address, target_address, token_amount, full_token_identifier, lovelace_amount, total_source_lovelace - fee, fee, total_source_token_amount)
            subprocess.run(final_tx_command, shell=True)
            print(f"\nFinal Transaction Command: \n{final_tx_command}")
            signing_key_file = "wallets/payment.skey"
            sign_transaction(signing_key_file)
            submit_transaction()
            print("Transaction submitted successfully.")
            break

    if not found_utxo:
        print("\nNo suitable UTXO found.")

if __name__ == "__main__":
    main()

Do you have the seed phrase for the wallets/addresses you are trying to use? You can just put those into a wallet such as Daedalus and interact with the addresses and tokens that way. That avoids having your relay/node setup.

Otherwise, can you post your gLiveView output? Do you have outgoing connections?

Unless you are running a stake pool, you don’t really need the topology updater. Relays can now run on p2p as well.

Thanks for your answer. Looks like my script had some error. When i am running the cli commands directly, it seems to work so far, and node status remains at 100% and console does not generate any errors.

Cant use Dedalus here. Plan is to create some automated User Funds management backend system. So everything has to run via Python scripts.

1 Like