Skip to content

Registering a Subnamespace⚓︎

INTERMEDIATE

Subnamespaces (also called "child" namespaces) extend the hierarchical structure of namespaces.

This tutorial shows how to register a subnamespace under an existing root namespace.

Once registered, additional steps are required to link the namespace to a mosaic or account, as explained in Next Steps.

Prerequisites⚓︎

Before you start, make sure to:

Additionally, review the Transfer transaction tutorial to understand how transactions are announced and confirmed.

Full Code⚓︎

The following is the complete code listing for this tutorial. A detailed, step-by-step explanation follows in the next section.

import json
import os
import time
import urllib.request

from symbolchain.CryptoTypes import PrivateKey
from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.symbol.IdGenerator import generate_namespace_id
from symbolchain.symbol.Network import Address

NODE_URL = os.getenv('NODE_URL', 'https://reference.symboltest.net:3001')
print(f'Using node {NODE_URL}')


# Helper function to announce a transaction
def announce_transaction(payload, label):
    print(f'Announcing {label} to /transactions')
    request = urllib.request.Request(
        f'{NODE_URL}/transactions',
        data=payload.encode(),
        headers={'Content-Type': 'application/json'},
        method='PUT'
    )
    with urllib.request.urlopen(request) as announce_response:
        print(f'  Response: {announce_response.read().decode()}')


# Helper function to wait for transaction confirmation
def wait_for_confirmation(tx_hash, label):
    print(f'Waiting for {label} confirmation...')
    for attempt in range(60):
        time.sleep(1)
        try:
            url = f'{NODE_URL}/transactionStatus/{tx_hash}'
            with urllib.request.urlopen(url) as confirm_response:
                status = json.loads(confirm_response.read().decode())
                print(f'  Transaction status: {status["group"]}')
                if status['group'] == 'confirmed':
                    print(f'{label} confirmed in {attempt} seconds')
                    return
                if status['group'] == 'failed':
                    raise RuntimeError(
                        f'{label} failed: {status["code"]}')
        except urllib.error.HTTPError:
            print('  Transaction status: unknown')
    raise TimeoutError(f'{label} not confirmed after 60 seconds')


SIGNER_PRIVATE_KEY = os.getenv(
    'SIGNER_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000000')
signer_key_pair = SymbolFacade.KeyPair(PrivateKey(SIGNER_PRIVATE_KEY))

facade = SymbolFacade('testnet')
signer_address = facade.network.public_key_to_address(
    signer_key_pair.public_key)
print(f'Signer address: {signer_address}')

try:
    # Fetch recommended fees
    fee_path = '/network/fees/transaction'
    print(f'Fetching recommended fees from {fee_path}')
    with urllib.request.urlopen(f'{NODE_URL}{fee_path}') as response:
        response_json = json.loads(response.read().decode())
        median_multiplier = response_json['medianFeeMultiplier']
        minimum_multiplier = response_json['minFeeMultiplier']
        fee_multiplier = max(median_multiplier, minimum_multiplier)
        print(f'  Fee multiplier: {fee_multiplier}')

    # Build the subnamespace name
    root_namespace_name = os.getenv('ROOT_NAMESPACE', 'ns_root')
    subnamespace_name = os.getenv(
        'SUBNAMESPACE', f'sub_{int(time.time())}')
    full_namespace_name = (
        f'{root_namespace_name}.{subnamespace_name}')
    print(f'Creating subnamespace: {full_namespace_name}')

    # Generate the parent namespace ID from the root namespace name
    parent_id = generate_namespace_id(root_namespace_name)
    print(f'  Parent namespace ID: 0x{parent_id:016X}')

    # Build the transaction
    transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'namespace_registration_transaction_v1',
            'registration_type': 'child',
            'parent_id': parent_id,
            'name': subnamespace_name
        },
        signer_key_pair.public_key,
        fee_multiplier,
        2 * 60 * 60)

    # Sign transaction and generate final payload
    signature = facade.sign_transaction(signer_key_pair, transaction)
    json_payload = facade.transaction_factory.attach_signature(
        transaction, signature)
    print('Built transaction:')
    print(json.dumps(transaction.to_json(), indent=2))

    transaction_hash = facade.hash_transaction(transaction)
    print(f'Transaction hash: {transaction_hash}')

    # Announce transaction
    announce_transaction(json_payload, 'namespace registration')

    # Wait for confirmation
    wait_for_confirmation(transaction_hash, 'namespace registration')

    # Retrieve the namespace
    namespace_id = generate_namespace_id(
        subnamespace_name, parent_id)
    print(f'Child namespace ID: {namespace_id} (0x{namespace_id:016X})')

    namespace_path = f'/namespaces/{namespace_id:016X}'
    print(f'Fetching namespace information from {namespace_path}')
    with urllib.request.urlopen(
        f'{NODE_URL}{namespace_path}'
    ) as response:
        response_json = json.loads(response.read().decode())
        namespace_info = response_json['namespace']
        print('Namespace information:')
        reg_type = namespace_info['registrationType']
        print(f'  Registration type: {reg_type}')
        owner_address = Address.from_decoded_address_hex_string(
            namespace_info['ownerAddress'])
        print(f'  Owner address: {owner_address}')
        print(f"  Parent ID: {namespace_info['parentId']}")
        print(f"  Depth: {namespace_info['depth']}")
        print(f"  Level 0: {namespace_info['level0']}")
        if int(namespace_info['depth']) >= 1:
            print(f"  Level 1: {namespace_info['level1']}")
        if int(namespace_info['depth']) >= 2:
            if 'level2' in namespace_info:
                print(f"  Level 2: {namespace_info['level2']}")
        print(f"  Start height: {namespace_info['startHeight']}")
        print(f"  End height: {namespace_info['endHeight']}")

except Exception as e:
    print(e)

Download source

import { PrivateKey } from 'symbol-sdk';
import {
    Address,
    SymbolFacade,
    descriptors,
    generateNamespaceId,
    models
} from 'symbol-sdk/symbol';

const NODE_URL = process.env.NODE_URL ||
    'https://reference.symboltest.net:3001';
console.log('Using node', NODE_URL);

// Helper function to announce a transaction
async function announceTransaction(payload, label) {
    console.log(`Announcing ${label} to /transactions`);
    const response = await fetch(`${NODE_URL}/transactions`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: payload
    });
    console.log('  Response:', await response.text());
}

// Helper function to wait for transaction confirmation
async function waitForConfirmation(transactionHash, label) {
    console.log(`Waiting for ${label} confirmation...`);
    for (let attempt = 0; 60 > attempt; attempt++) {
        await new Promise(resolve => { setTimeout(resolve, 1000); });
        const response = await fetch(
            `${NODE_URL}/transactionStatus/${transactionHash}`);
        if (!response.ok) {
            if (404 === response.status) {
                console.log('  Transaction status: unknown');
                continue;
            }
            throw new Error(`HTTP ${response.status}`);
        }
        const status = await response.json();
        console.log('  Transaction status:', status.group);
        if ('confirmed' === status.group) {
            console.log(`${label} confirmed in`, attempt, 'seconds');
            return;
        }
        if ('failed' === status.group)
            throw new Error(`${label} failed: ${status.code}`);
    }
    throw new Error(`${label} not confirmed after 60 seconds`);
}

const SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY ||
    '0000000000000000000000000000000000000000000000000000000000000000';
const signerKeyPair = new SymbolFacade.KeyPair(
    new PrivateKey(SIGNER_PRIVATE_KEY));

const facade = new SymbolFacade('testnet');
const signerAddress = facade.network.publicKeyToAddress(
    signerKeyPair.publicKey);
console.log('Signer address:', signerAddress.toString());

try {
    // Fetch recommended fees
    const feePath = '/network/fees/transaction';
    console.log('Fetching recommended fees from', feePath);
    const feeResponse = await fetch(`${NODE_URL}${feePath}`);
    const feeJSON = await feeResponse.json();
    const medianMultiplier = feeJSON.medianFeeMultiplier;
    const minimumMultiplier = feeJSON.minFeeMultiplier;
    const feeMultiplier = Math.max(medianMultiplier, minimumMultiplier);
    console.log('  Fee multiplier:', feeMultiplier);

    // Build the subnamespace name
    const rootNamespaceName = process.env.ROOT_NAMESPACE || 'ns_root';
    const subnamespaceName =
        process.env.SUBNAMESPACE || `sub_${Date.now()}`;
    const fullNamespaceName =
        `${rootNamespaceName}.${subnamespaceName}`;
    console.log('Creating subnamespace:', fullNamespaceName);

    // Generate the parent namespace ID from the root name
    const parentId = generateNamespaceId(rootNamespaceName);
    const parentIdHex = parentId.toString(16)
        .toUpperCase().padStart(16, '0');
    console.log('Parent namespace ID:', `0x${parentIdHex}`);

    // Build the transaction
    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.NamespaceRegistrationTransactionV1Descriptor(
            new models.NamespaceId(0n),
            models.NamespaceRegistrationType.CHILD,
            undefined,
            new models.NamespaceId(parentId),
            subnamespaceName),
        signerKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60);

    // Sign transaction and generate final payload
    const signature = facade.signTransaction(
        signerKeyPair, transaction);
    const jsonPayload = facade.transactionFactory.static.attachSignature(
        transaction, signature);
    console.log('Built transaction:');
    console.dir(transaction.toJson(), { colors: true });

    const transactionHash =
        facade.hashTransaction(transaction).toString();
    console.log('Transaction hash:', transactionHash);

    // Announce transaction
    await announceTransaction(jsonPayload, 'namespace registration');

    // Wait for confirmation
    await waitForConfirmation(transactionHash, 'namespace registration');

    // Retrieve the namespace
    const namespaceId = generateNamespaceId(
        subnamespaceName, parentId);
    const namespaceIdHex = namespaceId.toString(16)
        .toUpperCase().padStart(16, '0');
    console.log(
        'Child namespace ID:',
        `${namespaceId} (0x${namespaceIdHex})`);

    const namespacePath = `/namespaces/${namespaceIdHex}`;
    console.log(
        'Fetching namespace information from', namespacePath);
    const namespaceResponse = await fetch(`${NODE_URL}${namespacePath}`);
    const namespaceJSON = await namespaceResponse.json();
    const namespaceInfo = namespaceJSON.namespace;
    console.log('Namespace information:');
    console.log(
        '  Registration type:', namespaceInfo.registrationType);
    const ownerAddress = Address.fromDecodedAddressHexString(
        namespaceInfo.ownerAddress);
    console.log('  Owner address:', ownerAddress.toString());
    console.log('  Parent ID:', namespaceInfo.parentId);
    console.log('  Depth:', namespaceInfo.depth);
    console.log('  Level 0:', namespaceInfo.level0);
    if (1 <= namespaceInfo.depth)
        console.log('  Level 1:', namespaceInfo.level1);
    if (2 <= namespaceInfo.depth && namespaceInfo.level2)
        console.log('  Level 2:', namespaceInfo.level2);
    console.log('  Start height:', namespaceInfo.startHeight);
    console.log('  End height:', namespaceInfo.endHeight);
} catch (e) {
    console.error(e.message);
}

Download source

//JAVA 21+
//DEPS org.symbol:symbol-sdk:3.3.1

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.charset.StandardCharsets;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.symbol.sdk.CryptoTypes;
import org.symbol.sdk.facade.SymbolFacade;
import org.symbol.sdk.symbol.Address;
import org.symbol.sdk.symbol.IdGenerator;
import org.symbol.sdk.symbol.KeyPair;
import org.symbol.sdk.symbol.SymbolTransactionFactory;
import org.symbol.sdk.symbol.descriptors.*;
import org.symbol.sdk.symbol.models.*;

public final class RegisterSubnamespace {
    private static final ObjectMapper JSON_MAPPER = new ObjectMapper();

    private static final HttpClient HTTP_CLIENT =
        HttpClient.newHttpClient();

    private final String nodeUrl = System.getenv().getOrDefault(
        "NODE_URL", "https://reference.symboltest.net:3001");

    private final SymbolFacade facade = new SymbolFacade("testnet");

    private void announceTransaction(
        final String payload,
        final String label
    ) throws IOException, InterruptedException {
        System.out.printf("Announcing %s to /transactions%n", label);
        final HttpRequest request = HttpRequest.newBuilder(
            URI.create(nodeUrl + "/transactions"))
            .header("Content-Type", "application/json")
            .PUT(HttpRequest.BodyPublishers.ofString(payload))
            .build();
        final HttpResponse<String> response = HTTP_CLIENT.send(
            request, BodyHandlers.ofString());
        System.out.printf("  Response: %s%n", response.body());
    }

    private void waitForConfirmation(
        final String transactionHash,
        final String label
    ) throws IOException, InterruptedException {
        System.out.printf("Waiting for %s confirmation...%n", label);
        for (int attempt = 0; 60 > attempt; ++attempt) {
            Thread.sleep(1000);
            final String statusPath =
                "/transactionStatus/" + transactionHash;
            final HttpRequest statusRequest = HttpRequest.newBuilder(
                URI.create(nodeUrl + statusPath)).GET().build();
            final HttpResponse<String> statusResponse = HTTP_CLIENT
                .send(statusRequest, BodyHandlers.ofString());
            if (404 == statusResponse.statusCode()) {
                System.out.println("  Transaction status: unknown");
                continue;
            }
            if (2 != statusResponse.statusCode() / 100)
                throw new IOException(
                    "HTTP " + statusResponse.statusCode());

            final JsonNode status =
                JSON_MAPPER.readTree(statusResponse.body());
            final String group = status.get("group").asText();
            System.out.printf("  Transaction status: %s%n", group);
            if ("confirmed".equals(group)) {
                System.out.printf("%s confirmed in %d seconds%n",
                    label, attempt);
                return;
            }
            if ("failed".equals(group))
                throw new IOException(String.format("%s failed: %s",
                    label, status.get("code").asText()));
        }
        throw new IOException(String.format(
            "%s not confirmed after 60 seconds", label));
    }

    public static void main(final String[] args) {
        try {
            new RegisterSubnamespace().run();
        } catch (final Exception ex) {
            System.out.println(null == ex.getMessage()
                ? ex.toString()
                : ex.getMessage());
        }
    }

    private void run() throws IOException, InterruptedException {
        System.out.printf("Using node %s%n", nodeUrl);

        final String signerPrivateKey = System.getenv().getOrDefault(
            "SIGNER_PRIVATE_KEY", "0".repeat(64));
        final KeyPair signerKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(signerPrivateKey));

        final Address signerAddress = facade.network.publicKeyToAddress(
            signerKeyPair.getPublicKey());
        System.out.printf("Signer address: %s%n", signerAddress);

        // Fetch recommended fees
        final String feePath = "/network/fees/transaction";
        System.out.printf("Fetching recommended fees from %s%n", feePath);
        final JsonNode feeJson = getJson(feePath);
        final long feeMultiplier = Math.max(
            feeJson.get("medianFeeMultiplier").asLong(),
            feeJson.get("minFeeMultiplier").asLong());
        System.out.printf("  Fee multiplier: %d%n", feeMultiplier);

        // Build the subnamespace name
        final String rootNamespaceName = System.getenv().getOrDefault(
            "ROOT_NAMESPACE", "ns_root");
        final String subnamespaceName = System.getenv().getOrDefault(
            "SUBNAMESPACE", "sub_" + System.currentTimeMillis());
        final String fullNamespaceName =
            rootNamespaceName + "." + subnamespaceName;
        System.out.printf("Creating subnamespace: %s%n",
            fullNamespaceName);

        // Generate the parent namespace ID from the root name
        final long parentId = IdGenerator.generateNamespaceId(
            rootNamespaceName);
        System.out.printf("Parent namespace ID: 0x%016X%n", parentId);

        // Build the transaction
        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new NamespaceRegistrationTransactionV1Descriptor(
                    new NamespaceId(0),
                    NamespaceRegistrationType.CHILD,
                    null,
                    new NamespaceId(parentId),
                    subnamespaceName.getBytes(StandardCharsets.UTF_8)),
                signerKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60);

        // Sign transaction and generate final payload
        final CryptoTypes.Signature signature = facade.signTransaction(
            signerKeyPair, transaction);
        final String jsonPayload = SymbolTransactionFactory
            .attachSignature(transaction, signature);
        System.out.println("Built transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));

        final String transactionHash =
            facade.hashTransaction(transaction).toString();
        System.out.printf("Transaction hash: %s%n", transactionHash);

        // Announce transaction
        announceTransaction(jsonPayload, "namespace registration");

        // Wait for confirmation
        waitForConfirmation(transactionHash, "namespace registration");

        // Retrieve the namespace
        final long namespaceId = IdGenerator.generateNamespaceId(
            subnamespaceName, parentId);
        System.out.printf("Child namespace ID: %s (0x%016X)%n",
            Long.toUnsignedString(namespaceId), namespaceId);

        final String namespacePath = "/namespaces/%016X".formatted(
            namespaceId);
        System.out.printf("Fetching namespace information from %s%n",
            namespacePath);
        final JsonNode namespaceInfo = getJson(namespacePath)
            .get("namespace");
        System.out.println("Namespace information:");
        System.out.printf("  Registration type: %s%n",
            namespaceInfo.get("registrationType").asText());
        final Address ownerAddress = Address
            .fromDecodedAddressHexString(
                namespaceInfo.get("ownerAddress").asText());
        System.out.printf("  Owner address: %s%n", ownerAddress);
        System.out.printf("  Parent ID: %s%n",
            namespaceInfo.get("parentId").asText());
        System.out.printf("  Depth: %s%n",
            namespaceInfo.get("depth").asText());
        System.out.printf("  Level 0: %s%n",
            namespaceInfo.get("level0").asText());
        if (1 <= namespaceInfo.get("depth").asInt())
            System.out.printf("  Level 1: %s%n",
                namespaceInfo.get("level1").asText());
        if (2 <= namespaceInfo.get("depth").asInt()
            && namespaceInfo.has("level2"))
            System.out.printf("  Level 2: %s%n",
                namespaceInfo.get("level2").asText());
        System.out.printf("  Start height: %s%n",
            namespaceInfo.get("startHeight").asText());
        System.out.printf("  End height: %s%n",
            namespaceInfo.get("endHeight").asText());
    }

    private JsonNode getJson(final String path)
        throws IOException, InterruptedException {
        final HttpRequest request = HttpRequest.newBuilder(
            URI.create(nodeUrl + path)).GET().build();
        final HttpResponse<String> response = HTTP_CLIENT.send(
            request, BodyHandlers.ofString());
        return JSON_MAPPER.readTree(response.body());
    }

}

Download source

Code Explanation⚓︎

The code follows the same pattern as the Registering a Root Namespace tutorial. This section focuses only on the key differences.

For detailed explanations of the common steps (setting up the account, fetching recommended fees, and announcing) and the transaction descriptor fields shared with a root namespace, see Registering a Root Namespace.

Choosing the Subnamespace Name⚓︎

    # Build the subnamespace name
    root_namespace_name = os.getenv('ROOT_NAMESPACE', 'ns_root')
    subnamespace_name = os.getenv(
        'SUBNAMESPACE', f'sub_{int(time.time())}')
    full_namespace_name = (
        f'{root_namespace_name}.{subnamespace_name}')
    print(f'Creating subnamespace: {full_namespace_name}')

    # Generate the parent namespace ID from the root namespace name
    parent_id = generate_namespace_id(root_namespace_name)
    print(f'  Parent namespace ID: 0x{parent_id:016X}')
    // Build the subnamespace name
    const rootNamespaceName = process.env.ROOT_NAMESPACE || 'ns_root';
    const subnamespaceName =
        process.env.SUBNAMESPACE || `sub_${Date.now()}`;
    const fullNamespaceName =
        `${rootNamespaceName}.${subnamespaceName}`;
    console.log('Creating subnamespace:', fullNamespaceName);

    // Generate the parent namespace ID from the root name
    const parentId = generateNamespaceId(rootNamespaceName);
    const parentIdHex = parentId.toString(16)
        .toUpperCase().padStart(16, '0');
    console.log('Parent namespace ID:', `0x${parentIdHex}`);
        // Build the subnamespace name
        final String rootNamespaceName = System.getenv().getOrDefault(
            "ROOT_NAMESPACE", "ns_root");
        final String subnamespaceName = System.getenv().getOrDefault(
            "SUBNAMESPACE", "sub_" + System.currentTimeMillis());
        final String fullNamespaceName =
            rootNamespaceName + "." + subnamespaceName;
        System.out.printf("Creating subnamespace: %s%n",
            fullNamespaceName);

        // Generate the parent namespace ID from the root name
        final long parentId = IdGenerator.generateNamespaceId(
            rootNamespaceName);
        System.out.printf("Parent namespace ID: 0x%016X%n", parentId);

A subnamespace is identified by its full name, which joins the parent namespace name and the child name with a dot, such as company.product. See Name in the Textbook for the naming rules.

To avoid collisions across multiple runs of the tutorial, a timestamp is added to the child name. In practice, however, programs would use a fixed name for their subnamespaces. You can force the tutorial to use fixed names through the ROOT_NAMESPACE and SUBNAMESPACE environment variables.

The parent namespace ID is derived from the parent name using . A parent namespace is referenced by this ID rather than by its name.

Use a parent namespace owned by the signer

By default, the code uses the test account referenced by SIGNER_PRIVATE_KEY and a parent namespace named ns_root.

If you come from the Registering a Root Namespace tutorial, set the SIGNER_PRIVATE_KEY and ROOT_NAMESPACE environment variables to match the account and namespace you created there, or any other namespace that the signer owns.

Building the Transaction⚓︎

    # Build the transaction
    transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'namespace_registration_transaction_v1',
            'registration_type': 'child',
            'parent_id': parent_id,
            'name': subnamespace_name
        },
        signer_key_pair.public_key,
        fee_multiplier,
        2 * 60 * 60)
    // Build the transaction
    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.NamespaceRegistrationTransactionV1Descriptor(
            new models.NamespaceId(0n),
            models.NamespaceRegistrationType.CHILD,
            undefined,
            new models.NamespaceId(parentId),
            subnamespaceName),
        signerKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60);
        // Build the transaction
        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new NamespaceRegistrationTransactionV1Descriptor(
                    new NamespaceId(0),
                    NamespaceRegistrationType.CHILD,
                    null,
                    new NamespaceId(parentId),
                    subnamespaceName.getBytes(StandardCharsets.UTF_8)),
                signerKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60);

The main difference when registering a subnamespace is in the transaction descriptor:

  • : The value child indicates a subnamespace is being created. Use root to register a root namespace instead.

  • : Instead of specifying a duration, you provide the namespace ID of the parent namespace, derived in the previous step. It can be a root namespace or another subnamespace.

  • : The name of the subnamespace, chosen in the previous step.

    Note that this is just the name of the subnamespace, not the full path. For example, to create company.product, where company is the root, you would set name: 'product' and .

Subnamespace lease fees

In addition to the standard transaction fee, registering a subnamespace requires a lease fee.

Unlike the transaction fee, the lease fee is not included in the transaction request.

For subnamespaces, this fee is fixed regardless of duration. The network deducts the lease fee automatically when the transaction is confirmed, so you do not need to specify it in the transaction.

The transaction is then signed, announced, and confirmed following the same process as in Registering a Root Namespace.

Retrieving the Subnamespace⚓︎

    # Retrieve the namespace
    namespace_id = generate_namespace_id(
        subnamespace_name, parent_id)
    print(f'Child namespace ID: {namespace_id} (0x{namespace_id:016X})')

    namespace_path = f'/namespaces/{namespace_id:016X}'
    print(f'Fetching namespace information from {namespace_path}')
    with urllib.request.urlopen(
        f'{NODE_URL}{namespace_path}'
    ) as response:
        response_json = json.loads(response.read().decode())
        namespace_info = response_json['namespace']
        print('Namespace information:')
        reg_type = namespace_info['registrationType']
        print(f'  Registration type: {reg_type}')
        owner_address = Address.from_decoded_address_hex_string(
            namespace_info['ownerAddress'])
        print(f'  Owner address: {owner_address}')
        print(f"  Parent ID: {namespace_info['parentId']}")
        print(f"  Depth: {namespace_info['depth']}")
        print(f"  Level 0: {namespace_info['level0']}")
        if int(namespace_info['depth']) >= 1:
            print(f"  Level 1: {namespace_info['level1']}")
        if int(namespace_info['depth']) >= 2:
            if 'level2' in namespace_info:
                print(f"  Level 2: {namespace_info['level2']}")
        print(f"  Start height: {namespace_info['startHeight']}")
        print(f"  End height: {namespace_info['endHeight']}")
    // Retrieve the namespace
    const namespaceId = generateNamespaceId(
        subnamespaceName, parentId);
    const namespaceIdHex = namespaceId.toString(16)
        .toUpperCase().padStart(16, '0');
    console.log(
        'Child namespace ID:',
        `${namespaceId} (0x${namespaceIdHex})`);

    const namespacePath = `/namespaces/${namespaceIdHex}`;
    console.log(
        'Fetching namespace information from', namespacePath);
    const namespaceResponse = await fetch(`${NODE_URL}${namespacePath}`);
    const namespaceJSON = await namespaceResponse.json();
    const namespaceInfo = namespaceJSON.namespace;
    console.log('Namespace information:');
    console.log(
        '  Registration type:', namespaceInfo.registrationType);
    const ownerAddress = Address.fromDecodedAddressHexString(
        namespaceInfo.ownerAddress);
    console.log('  Owner address:', ownerAddress.toString());
    console.log('  Parent ID:', namespaceInfo.parentId);
    console.log('  Depth:', namespaceInfo.depth);
    console.log('  Level 0:', namespaceInfo.level0);
    if (1 <= namespaceInfo.depth)
        console.log('  Level 1:', namespaceInfo.level1);
    if (2 <= namespaceInfo.depth && namespaceInfo.level2)
        console.log('  Level 2:', namespaceInfo.level2);
    console.log('  Start height:', namespaceInfo.startHeight);
    console.log('  End height:', namespaceInfo.endHeight);
        // Retrieve the namespace
        final long namespaceId = IdGenerator.generateNamespaceId(
            subnamespaceName, parentId);
        System.out.printf("Child namespace ID: %s (0x%016X)%n",
            Long.toUnsignedString(namespaceId), namespaceId);

        final String namespacePath = "/namespaces/%016X".formatted(
            namespaceId);
        System.out.printf("Fetching namespace information from %s%n",
            namespacePath);
        final JsonNode namespaceInfo = getJson(namespacePath)
            .get("namespace");
        System.out.println("Namespace information:");
        System.out.printf("  Registration type: %s%n",
            namespaceInfo.get("registrationType").asText());
        final Address ownerAddress = Address
            .fromDecodedAddressHexString(
                namespaceInfo.get("ownerAddress").asText());
        System.out.printf("  Owner address: %s%n", ownerAddress);
        System.out.printf("  Parent ID: %s%n",
            namespaceInfo.get("parentId").asText());
        System.out.printf("  Depth: %s%n",
            namespaceInfo.get("depth").asText());
        System.out.printf("  Level 0: %s%n",
            namespaceInfo.get("level0").asText());
        if (1 <= namespaceInfo.get("depth").asInt())
            System.out.printf("  Level 1: %s%n",
                namespaceInfo.get("level1").asText());
        if (2 <= namespaceInfo.get("depth").asInt()
            && namespaceInfo.has("level2"))
            System.out.printf("  Level 2: %s%n",
                namespaceInfo.get("level2").asText());
        System.out.printf("  Start height: %s%n",
            namespaceInfo.get("startHeight").asText());
        System.out.printf("  End height: %s%n",
            namespaceInfo.get("endHeight").asText());

To verify the subnamespace was registered, the code retrieves it from the network using the /namespaces/{namespaceId} GET endpoint and displays its properties.

The subnamespace ID is computed using . This function takes both the subnamespace name and the parent ID, applying a deterministic hashing algorithm to produce the subnamespace ID.

A successful response confirms the subnamespace was registered and is active on the network.

Subnamespace registered but not linked yet

A subnamespace becomes useful when it serves as an alias for a mosaic or an account. Link the subnamespace to an identifier using the guides in Next Steps.

Output⚓︎

The output shown below corresponds to a typical run of the program.

Using node https://reference.symboltest.net:3001
Signer address: TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
Fetching recommended fees from /network/fees/transaction
  Fee multiplier: 100
Creating subnamespace: ns_root.sub_1766533103
Parent namespace ID: 0xB0786316D5C1D9DD
Built transaction:
{
  "signature": "9E91F08855D1911BE7208BB3441ACF8AE18EBEB529867D6DAC8552A837E17AA2F05BEBAE510950D5194E0837C4A6E780138CCF29A065F55C05A76CEF6AFA610E",
  "signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
  "version": 1,
  "network": 152,
  "type": 16718,
  "fee": "16000",
  "deadline": "99289837399",
  "parent_id": "12716022497607277021",
  "id": "11305240440531381773",
  "registration_type": 1,
  "name": "7375625f31373636353333313033"
}
Transaction hash: 43962A040342198E485F5E91364CA0270F737C56E58909FE03FE3F02AD97CA56
Announcing namespace registration to /transactions
  Response: {"message":"packet 9 was pushed to the network via /transactions"}
Waiting for namespace registration confirmation...
  Transaction status: unconfirmed
  Transaction status: confirmed
namespace registration confirmed in 19 seconds
Child namespace ID: 11305240440531381773 (0x9CE448486E40EE0D)
Fetching namespace information from /namespaces/9CE448486E40EE0D
Namespace information:
  Registration type: 1
  Owner address: TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
  Parent ID: B0786316D5C1D9DD
  Depth: 2
  Level 0: B0786316D5C1D9DD
  Level 1: 9CE448486E40EE0D
  Start height: 2956766
  End height: 3046046

Some highlights from the output:

  • Full namespace path (line 5): The subnamespace ns_root.sub_1766533103 shows the full name.

  • Parent namespace ID (lines 6, 33): The parent ID 0xB0786316D5C1D9DD links this subnamespace to its root.

  • Fee (line 14): The transaction fee of 0.016 XYM is calculated as the transaction size multiplied by the fee multiplier. The lease fee is deducted separately by the network when the transaction is confirmed.

  • ID and name (lines 17, 19): The id field shows the subnamespace ID as a decimal number, while name contains only the child portion encoded as hexadecimal (for example, 7375625f... decodes to sub_1...).

  • Subnamespace ID (line 28): Shows both decimal and hexadecimal representations to match the id field on line 17.

  • Registration type (line 31): The value 1 indicates a subnamespace (versus 0 for root namespaces).

  • Owner address (line 32): The account that registered the namespace, which must be the same as the root namespace owner.

  • Depth (line 34): The depth of 2 indicates there are 2 levels in the namespace hierarchy. Level 0 is the root namespace, and level 1 is this subnamespace.

  • Levels (lines 35-36): The full hierarchical path. level0 contains the root namespace ID, and level1 contains the child ID. If depth were 3, level2 would contain the grandchild ID.

  • Start and end heights (lines 37-38): These values are inherited from the root namespace, not set independently.

The transaction hash printed in the output can also be used to search for the transaction in the Symbol Testnet Explorer.

Conclusion⚓︎

This tutorial showed how to:

Step Related documentation
Generate namespace ID
Build a subnamespace registration transaction , NamespaceRegistrationTransactionV1
Retrieve the subnamespace /namespaces/{namespaceId} GET

Next Steps⚓︎

Now that you have a subnamespace, you can: