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.sc import Amount
from symbolchain.symbol.FeeCalculator import calculate_transaction_fee
from symbolchain.symbol.IdGenerator import generate_namespace_id
from symbolchain.symbol.Network import Address, NetworkTimestamp

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

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 current network time
    time_path = '/node/time'
    print(f'Fetching current network time from {time_path}')
    with urllib.request.urlopen(f'{NODE_URL}{time_path}') as response:
        response_json = json.loads(response.read().decode())
        receive_timestamp = (
            response_json['communicationTimestamps']['receiveTimestamp'])
        timestamp = NetworkTimestamp(int(receive_timestamp))
        print(f'  Network time: {timestamp.timestamp} ms since nemesis')

    # 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.transaction_factory.create({
        'type': 'namespace_registration_transaction_v1',
        'signer_public_key': signer_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        'registration_type': 'child',
        'parent_id': parent_id,
        'name': subnamespace_name
    })
    transaction.fee = Amount(
        calculate_transaction_fee(transaction, fee_multiplier))

    # 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
    print('Announcing namespace registration to /transactions')
    request = urllib.request.Request(
        f'{NODE_URL}/transactions',
        data=json_payload.encode(),
        headers={'Content-Type': 'application/json'},
        method='PUT'
    )
    with urllib.request.urlopen(request) as response:
        print(f'  Response: {response.read().decode()}')

    # Wait for confirmation
    print('Waiting for namespace registration confirmation...')
    for attempt in range(60):
        time.sleep(1)
        try:
            status_url = (
                f'{NODE_URL}/transactionStatus/{transaction_hash}')
            with urllib.request.urlopen(status_url) as response:
                status = json.loads(response.read().decode())
                print(f"  Transaction status: {status['group']}")
            if status['group'] == 'confirmed':
                print('Namespace registration confirmed in',
                    attempt, 'seconds')
                break
            if status['group'] == 'failed':
                raise RuntimeError('Namespace registration failed:',
                    status['code'])
        except urllib.error.HTTPError:
            print('  Transaction status: unknown')

    # 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,
    NetworkTimestamp,
    SymbolFacade,
    calculateTransactionFee,
    generateNamespaceId,
    models
} from 'symbol-sdk/symbol';

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

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 current network time
    const timePath = '/node/time';
    console.log('Fetching current network time from', timePath);
    const timeResponse = await fetch(`${NODE_URL}${timePath}`);
    const timeJSON = await timeResponse.json();
    const receiveTimestamp =
        timeJSON.communicationTimestamps.receiveTimestamp;
    const timestamp = new NetworkTimestamp(receiveTimestamp);
    console.log('  Network time:', timestamp.timestamp,
        'ms since nemesis');

    // 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.transactionFactory.create({
        type: 'namespace_registration_transaction_v1',
        signerPublicKey: signerKeyPair.publicKey.toString(),
        deadline: timestamp.addHours(2).timestamp,
        registrationType: 'child',
        parentId,
        name: subnamespaceName
    });
    transaction.fee = new models.Amount(
        calculateTransactionFee(transaction, feeMultiplier));

    // 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
    console.log('Announcing namespace registration to /transactions');
    const announceResponse = await fetch(`${NODE_URL}/transactions`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: jsonPayload
    });
    console.log('  Response:', await announceResponse.text());

    // Wait for confirmation
    console.log('Waiting for namespace registration confirmation...');
    const statusPath = `/transactionStatus/${transactionHash}`;
    for (let attempt = 1; 60 >= attempt; ++attempt) {
        await new Promise(resolve => { setTimeout(resolve, 1000); });
        const response = await fetch(`${NODE_URL}${statusPath}`);

        if (response.ok) {
            const status = await response.json();
            console.log('  Transaction status:', status.group);
            if ('confirmed' === status.group) {
                console.log('Transaction confirmed in', attempt,
                    'seconds');
                break;
            }
            if ('failed' === status.group) {
                console.log('Transaction failed:', status.code);
                break;
            }
        } else {
            console.log('  Transaction status: unknown | Cause:',
                response.status
            );
        }
        if (60 === attempt)
            console.warn('Confirmation took too long.');
    }

    // 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

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 network time and 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}`);

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.transaction_factory.create({
        'type': 'namespace_registration_transaction_v1',
        'signer_public_key': signer_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        'registration_type': 'child',
        'parent_id': parent_id,
        'name': subnamespace_name
    })
    transaction.fee = Amount(
        calculate_transaction_fee(transaction, fee_multiplier))
    // Build the transaction
    const transaction = facade.transactionFactory.create({
        type: 'namespace_registration_transaction_v1',
        signerPublicKey: signerKeyPair.publicKey.toString(),
        deadline: timestamp.addHours(2).timestamp,
        registrationType: 'child',
        parentId,
        name: subnamespaceName
    });
    transaction.fee = new models.Amount(
        calculateTransactionFee(transaction, feeMultiplier));

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']}")
    // 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);

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 current network time from /node/time
  Network time: 99282637399 ms since nemesis
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 7): The subnamespace ns_root.sub_1766533103 shows the full name.

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

  • Fee (line 16): 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 19, 21): 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 30): Shows both decimal and hexadecimal representations to match the id field on line 19.

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

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

  • Depth (line 36): 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 37-38): 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 39-40): 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: