Skip to content

Resolving a Namespace from a Receipt⚓︎

Namespace aliases can change over time, so the current value of a namespace may not match the value it had when a transaction was confirmed.

Every time a transaction uses an alias instead of a raw address or mosaic ID, the network stores a receipt with the actual value the alias pointed to at confirmation time. The type of receipt that captures namespace resolutions is called a resolution statement.

This tutorial shows how to query resolution statements to find the real address and mosaic ID behind a namespace alias used in a confirmed transaction.

Prerequisites⚓︎

Before you start, set up your development environment.

This tutorial only reads data from the network. It does not submit any transaction, so no account or XYM balance is needed.

Full Code⚓︎

import json
import os
import urllib.request

from symbolchain.symbol.Network import Address

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

# Hash of a confirmed tx that used a namespace alias
TX_HASH = os.getenv('TRANSACTION_HASH',
    'BA0C65DB752A3BF1B25285540642537ECE8C2CA716577EDF8BF0F8597A85ADC4')
print(f'Transaction hash: {TX_HASH}')

try:
    # Retrieve the confirmed transaction
    tx_path = f'/transactions/confirmed/{TX_HASH}'
    print(f'Fetching transaction from {tx_path}')
    with urllib.request.urlopen(f'{NODE_URL}{tx_path}') as response:
        tx_data = json.loads(response.read().decode())

    block_height = tx_data['meta']['height']
    print(f'  Block height: {block_height}')

    # primaryId is 1-based, meta.index is 0-based
    tx_index = int(tx_data['meta']['index'])
    tx_primary = tx_index + 1
    print(f'  Transaction index: {tx_index} (primaryId: {tx_primary})')

    recipient_hex = tx_data['transaction']['recipientAddress']
    recipient_bytes = bytes.fromhex(recipient_hex)
    is_address_alias = (recipient_bytes[0] & 0x01) == 1
    print(f'  Recipient: {recipient_hex}')
    print(f'  Is address alias: {is_address_alias}')

    aliased_mosaics = set()
    mosaics = tx_data['transaction']['mosaics']
    for mosaic in mosaics:
        mosaic_id = int(mosaic['id'], 16)
        is_mosaic_alias = (mosaic_id >> 63) & 1 == 1
        if is_mosaic_alias:
            aliased_mosaics.add(mosaic['id'])
        print(f'  Mosaic: {mosaic["id"]}')
        print(f'  Is mosaic alias: {is_mosaic_alias}')

    # Query address resolution statements
    if is_address_alias:
        address_path = ('/statements/resolutions/address'
            f'?height={block_height}')
        print(f'\nFetching address resolutions from {address_path}')
        with urllib.request.urlopen(
                f'{NODE_URL}{address_path}') as response:
            address_data = json.loads(response.read().decode())

        address_statements = address_data['data']
        print(f'  Found {len(address_statements)}'
            + ' resolution statement(s)')

        for item in address_statements:
            statement = item['statement']
            if statement['unresolved'] != recipient_hex:
                continue
            resolved = None
            for entry in statement['resolutionEntries']:
                source = entry['source']
                if source['primaryId'] <= tx_primary:
                    resolved = entry['resolved']
            if resolved:
                address = Address(bytes.fromhex(resolved))
                print('\nAddress resolution:')
                print(f'  Unresolved:  {statement["unresolved"]}')
                print(f'  Resolved:   {address}')

    # Query mosaic resolution statements
    if len(aliased_mosaics):
        mosaic_path = ('/statements/resolutions/mosaic'
            f'?height={block_height}')
        print(f'\nFetching mosaic resolutions from {mosaic_path}')
        with urllib.request.urlopen(
                f'{NODE_URL}{mosaic_path}') as response:
            mosaic_data = json.loads(response.read().decode())

        mosaic_statements = mosaic_data['data']
        print(f'  Found {len(mosaic_statements)} resolution statement(s)')

        for item in mosaic_statements:
            statement = item['statement']
            if statement['unresolved'] not in aliased_mosaics:
                continue
            resolved = None
            for entry in statement['resolutionEntries']:
                source = entry['source']
                if source['primaryId'] <= tx_primary:
                    resolved = entry['resolved']
            if resolved:
                print('\nMosaic resolution:')
                print(f'  Unresolved: {statement["unresolved"]}')
                print(f'  Resolved:   {resolved}')

except Exception as e:
    print(e)

Download source

import { SymbolFacade } from 'symbol-sdk/symbol';

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

// Hash of a confirmed tx that used a namespace alias
const TX_HASH = process.env.TRANSACTION_HASH ||
    'BA0C65DB752A3BF1B25285540642537ECE8C2CA716577EDF8BF0F8597A85ADC4';
console.log('Transaction hash:', TX_HASH);

try {
    // Retrieve the confirmed transaction
    const txPath = `/transactions/confirmed/${TX_HASH}`;
    console.log('Fetching transaction from', txPath);
    const txResponse = await fetch(`${NODE_URL}${txPath}`);
    const txData = await txResponse.json();

    const blockHeight = txData.meta.height;
    console.log('  Block height:', blockHeight);

    // primaryId is 1-based, meta.index is 0-based
    const txIndex = txData.meta.index;
    const txPrimary = txIndex + 1;
    console.log(
        `  Transaction index: ${txIndex} (primaryId: ${txPrimary})`);

    const recipientHex = txData.transaction.recipientAddress;
    const recipientBytes = Buffer.from(recipientHex, 'hex');
    const isAddressAlias = (recipientBytes[0] & 0x01) === 1;
    console.log('  Recipient:', recipientHex);
    console.log('  Is address alias:', isAddressAlias);

    const aliasedMosaics = new Set();
    const mosaics = txData.transaction.mosaics;
    for (const mosaic of mosaics) {
        const mosaicId = BigInt(`0x${mosaic.id}`);
        const isMosaicAlias =
            (mosaicId >> 63n & 1n) === 1n;
        if (isMosaicAlias) aliasedMosaics.add(mosaic.id);
        console.log(`  Mosaic: ${mosaic.id}`);
        console.log(`  Is mosaic alias: ${isMosaicAlias}`);
    }

    // Query address resolution statements
    if (isAddressAlias) {
        const addressPath =
            '/statements/resolutions/address'
            + `?height=${blockHeight}`;
        console.log('\nFetching address resolutions from', addressPath);
        const addressResponse =
            await fetch(`${NODE_URL}${addressPath}`);
        const addressData = await addressResponse.json();

        const addressStatements = addressData.data;
        console.log(`  Found ${addressStatements.length}`
            + ' resolution statement(s)');

        for (const item of addressStatements) {
            const statement = item.statement;
            if (statement.unresolved !== recipientHex)
                continue;
            let resolved = null;
            for (const entry of
                statement.resolutionEntries) {
                if (entry.source.primaryId <= txPrimary)
                    resolved = entry.resolved;
            }
            if (resolved) {
                const bytes = Uint8Array.from(
                    Buffer.from(resolved, 'hex'));
                const address = new SymbolFacade.Address(bytes);
                console.log('\nAddress resolution:');
                console.log('  Unresolved:', statement.unresolved);
                console.log(`  Resolved:   ${address}`);
            }
        }
    }

    // Query mosaic resolution statements
    if (aliasedMosaics.size) {
        const mosaicPath =
            '/statements/resolutions/mosaic'
            + `?height=${blockHeight}`;
        console.log('\nFetching mosaic resolutions from', mosaicPath);
        const mosaicResponse = await fetch(`${NODE_URL}${mosaicPath}`);
        const mosaicData = await mosaicResponse.json();

        const mosaicStatements = mosaicData.data;
        console.log(
            `  Found ${mosaicStatements.length}`
            + ' resolution statement(s)');

        for (const item of mosaicStatements) {
            const statement = item.statement;
            if (!aliasedMosaics.has(statement.unresolved))
                continue;
            let resolved = null;
            for (const entry of
                statement.resolutionEntries) {
                if (entry.source.primaryId <= txPrimary)
                    resolved = entry.resolved;
            }
            if (resolved) {
                console.log('\nMosaic resolution:');
                console.log('  Unresolved:', statement.unresolved);
                console.log(`  Resolved:   ${resolved}`);
            }
        }
    }

} catch (e) {
    console.error(e.message);
}

Download source

Code Explanation⚓︎

Defining the Transaction Hash⚓︎

# Hash of a confirmed tx that used a namespace alias
TX_HASH = os.getenv('TRANSACTION_HASH',
    'BA0C65DB752A3BF1B25285540642537ECE8C2CA716577EDF8BF0F8597A85ADC4')
print(f'Transaction hash: {TX_HASH}')
// Hash of a confirmed tx that used a namespace alias
const TX_HASH = process.env.TRANSACTION_HASH ||
    'BA0C65DB752A3BF1B25285540642537ECE8C2CA716577EDF8BF0F8597A85ADC4';
console.log('Transaction hash:', TX_HASH);

The code defines the hash of a confirmed transaction that used namespace aliases instead of a plain address and mosaic ID. This hash is read from the TRANSACTION_HASH environment variable, which defaults to a known transaction on the Symbol testnet.

The provided default transaction hash might result in a Not Found error if the testnet is reset. You can use TRANSACTION_HASH to analyze a different transaction.

Retrieving the Confirmed Transaction⚓︎

    # Retrieve the confirmed transaction
    tx_path = f'/transactions/confirmed/{TX_HASH}'
    print(f'Fetching transaction from {tx_path}')
    with urllib.request.urlopen(f'{NODE_URL}{tx_path}') as response:
        tx_data = json.loads(response.read().decode())

    block_height = tx_data['meta']['height']
    print(f'  Block height: {block_height}')

    # primaryId is 1-based, meta.index is 0-based
    tx_index = int(tx_data['meta']['index'])
    tx_primary = tx_index + 1
    print(f'  Transaction index: {tx_index} (primaryId: {tx_primary})')
    // Retrieve the confirmed transaction
    const txPath = `/transactions/confirmed/${TX_HASH}`;
    console.log('Fetching transaction from', txPath);
    const txResponse = await fetch(`${NODE_URL}${txPath}`);
    const txData = await txResponse.json();

    const blockHeight = txData.meta.height;
    console.log('  Block height:', blockHeight);

    // primaryId is 1-based, meta.index is 0-based
    const txIndex = txData.meta.index;
    const txPrimary = txIndex + 1;
    console.log(
        `  Transaction index: ${txIndex} (primaryId: ${txPrimary})`);

The confirmed transaction is fetched from the /transactions/confirmed/{transactionId} GET endpoint using its hash.

The response includes a meta object with the block height where the transaction was confirmed. This height is needed to query the resolution statements for that specific block.

The transaction metadata also includes the index field, which is the 0-based position of the transaction within the block. The code converts it to a 1-based primaryId (index + 1) to match against the resolution entries later.

    recipient_hex = tx_data['transaction']['recipientAddress']
    recipient_bytes = bytes.fromhex(recipient_hex)
    is_address_alias = (recipient_bytes[0] & 0x01) == 1
    print(f'  Recipient: {recipient_hex}')
    print(f'  Is address alias: {is_address_alias}')
    const recipientHex = txData.transaction.recipientAddress;
    const recipientBytes = Buffer.from(recipientHex, 'hex');
    const isAddressAlias = (recipientBytes[0] & 0x01) === 1;
    console.log('  Recipient:', recipientHex);
    console.log('  Is address alias:', isAddressAlias);

The retrieved transaction contains the recipientAddress field. If the transaction used a namespace alias as recipient, this field holds the unresolved value (the encoded namespace ID) instead of a real address.

The code checks whether the recipient is an alias by inspecting the lowest bit of the first byte:

  • If the bit is 1, the address is an encoded namespace alias, as described in the Linking Namespaces to Addresses tutorial.
  • If the bit is 0, the recipient is a regular address and no resolution is needed.
    aliased_mosaics = set()
    mosaics = tx_data['transaction']['mosaics']
    for mosaic in mosaics:
        mosaic_id = int(mosaic['id'], 16)
        is_mosaic_alias = (mosaic_id >> 63) & 1 == 1
        if is_mosaic_alias:
            aliased_mosaics.add(mosaic['id'])
        print(f'  Mosaic: {mosaic["id"]}')
        print(f'  Is mosaic alias: {is_mosaic_alias}')
    const aliasedMosaics = new Set();
    const mosaics = txData.transaction.mosaics;
    for (const mosaic of mosaics) {
        const mosaicId = BigInt(`0x${mosaic.id}`);
        const isMosaicAlias =
            (mosaicId >> 63n & 1n) === 1n;
        if (isMosaicAlias) aliasedMosaics.add(mosaic.id);
        console.log(`  Mosaic: ${mosaic.id}`);
        console.log(`  Is mosaic alias: ${isMosaicAlias}`);
    }

The code iterates through the transaction's mosaics array and checks whether each mosaic ID is a namespace alias by inspecting bit 63 (the highest bit) of its 64-bit value:

  • If the bit is 1, the value is a namespace ID used as a mosaic alias, as described in the Linking Namespaces to Mosaics tutorial.
  • If the bit is 0, the value is a regular mosaic ID and no resolution is needed.

Querying Address Resolution Statements⚓︎

    if is_address_alias:
        address_path = ('/statements/resolutions/address'
            f'?height={block_height}')
        print(f'\nFetching address resolutions from {address_path}')
        with urllib.request.urlopen(
                f'{NODE_URL}{address_path}') as response:
            address_data = json.loads(response.read().decode())

        address_statements = address_data['data']
        print(f'  Found {len(address_statements)}'
            + ' resolution statement(s)')
    if (isAddressAlias) {
        const addressPath =
            '/statements/resolutions/address'
            + `?height=${blockHeight}`;
        console.log('\nFetching address resolutions from', addressPath);
        const addressResponse =
            await fetch(`${NODE_URL}${addressPath}`);
        const addressData = await addressResponse.json();

        const addressStatements = addressData.data;
        console.log(`  Found ${addressStatements.length}`
            + ' resolution statement(s)');

Using the block height from the transaction metadata, the code queries the /statements/resolutions/address GET endpoint to retrieve the address resolution statements for that block.

Each resolution statement contains:

  • Height: The block where the resolution occurred.
  • Unresolved: The namespace alias (encoded as an address) that was used in the transaction.
  • Resolution entries: An array mapping the alias to the actual address at the time of confirmation.
        for item in address_statements:
            statement = item['statement']
            if statement['unresolved'] != recipient_hex:
                continue
            resolved = None
            for entry in statement['resolutionEntries']:
                source = entry['source']
                if source['primaryId'] <= tx_primary:
                    resolved = entry['resolved']
            if resolved:
                address = Address(bytes.fromhex(resolved))
                print('\nAddress resolution:')
                print(f'  Unresolved:  {statement["unresolved"]}')
                print(f'  Resolved:   {address}')
        for (const item of addressStatements) {
            const statement = item.statement;
            if (statement.unresolved !== recipientHex)
                continue;
            let resolved = null;
            for (const entry of
                statement.resolutionEntries) {
                if (entry.source.primaryId <= txPrimary)
                    resolved = entry.resolved;
            }
            if (resolved) {
                const bytes = Uint8Array.from(
                    Buffer.from(resolved, 'hex'));
                const address = new SymbolFacade.Address(bytes);
                console.log('\nAddress resolution:');
                console.log('  Unresolved:', statement.unresolved);
                console.log(`  Resolved:   ${address}`);
            }
        }
    }

The endpoint returns all address resolution statements for the block. The code skips any statement whose unresolved field does not match the transaction's recipientAddress, since a single block can contain multiple resolution statements if different namespace aliases were used.

Each statement can also have multiple entries if the alias was re-linked to a different value between transactions in the same block. If consecutive transactions resolve to the same value, only a single entry is stored.

Each resolution entry contains:

  • Source: Indicates from which transaction the resolved value applies, using a primaryId (1-based index of the transaction in the block) and secondaryId (1-based index within an aggregate transaction, or 0 for standalone transactions).
  • Resolved: The actual address the alias pointed to from the indicated source onward.

To determine the resolved value for the analyzed transaction, the code finds the last entry whose primaryId is less than or equal to the transaction's primaryId. This is the value the network used for that specific transaction, regardless of what the alias currently points to, even if the alias was defined multiple times in the same block.

Querying Mosaic Resolution Statements⚓︎

    if len(aliased_mosaics):
        mosaic_path = ('/statements/resolutions/mosaic'
            f'?height={block_height}')
        print(f'\nFetching mosaic resolutions from {mosaic_path}')
        with urllib.request.urlopen(
                f'{NODE_URL}{mosaic_path}') as response:
            mosaic_data = json.loads(response.read().decode())

        mosaic_statements = mosaic_data['data']
        print(f'  Found {len(mosaic_statements)} resolution statement(s)')

        for item in mosaic_statements:
            statement = item['statement']
            if statement['unresolved'] not in aliased_mosaics:
                continue
            resolved = None
            for entry in statement['resolutionEntries']:
                source = entry['source']
                if source['primaryId'] <= tx_primary:
                    resolved = entry['resolved']
            if resolved:
                print('\nMosaic resolution:')
                print(f'  Unresolved: {statement["unresolved"]}')
                print(f'  Resolved:   {resolved}')
    if (aliasedMosaics.size) {
        const mosaicPath =
            '/statements/resolutions/mosaic'
            + `?height=${blockHeight}`;
        console.log('\nFetching mosaic resolutions from', mosaicPath);
        const mosaicResponse = await fetch(`${NODE_URL}${mosaicPath}`);
        const mosaicData = await mosaicResponse.json();

        const mosaicStatements = mosaicData.data;
        console.log(
            `  Found ${mosaicStatements.length}`
            + ' resolution statement(s)');

        for (const item of mosaicStatements) {
            const statement = item.statement;
            if (!aliasedMosaics.has(statement.unresolved))
                continue;
            let resolved = null;
            for (const entry of
                statement.resolutionEntries) {
                if (entry.source.primaryId <= txPrimary)
                    resolved = entry.resolved;
            }
            if (resolved) {
                console.log('\nMosaic resolution:');
                console.log('  Unresolved:', statement.unresolved);
                console.log(`  Resolved:   ${resolved}`);
            }
        }
    }

The same transaction also used symbol.xym as a mosaic alias instead of a raw mosaic ID. The code queries the /statements/resolutions/mosaic GET endpoint with the same block height to retrieve the mosaic resolution statements.

As with the address resolution, the code skips statements whose unresolved field does not match the aliased mosaic IDs from the transaction.

The response follows the same structure as the address resolution, but the resolved field contains a mosaic ID instead of an address. The same matching logic applies: the last entry whose primaryId is less than or equal to the transaction's primaryId gives the resolved mosaic ID.

Output⚓︎

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

Using node https://reference.symboltest.net:3001
Transaction hash: BA0C65DB752A3BF1B25285540642537ECE8C2CA716577EDF8BF0F8597A85ADC4
Fetching transaction from /transactions/confirmed/BA0C65DB752A3BF1B25285540642537ECE8C2CA716577EDF8BF0F8597A85ADC4
  Block height: 3125903
  Transaction index: 0 (primaryId: 1)
  Recipient: 99EC0888813BE133E6000000000000000000000000000000
  Is address alias: True
  Mosaic: E74B99BA41F4AFEE
  Is mosaic alias: True

Fetching address resolutions from /statements/resolutions/address?height=3125903
  Found 1 resolution statement(s)

Address resolution:
  Unresolved: 99EC0888813BE133E6000000000000000000000000000000
  Resolved:   TCWYXKVYBMO4NBCUF3AXKJMXCGVSYQOS7ZG2TLI

Fetching mosaic resolutions from /statements/resolutions/mosaic?height=3125903
  Found 1 resolution statement(s)

Mosaic resolution:
  Unresolved: E74B99BA41F4AFEE
  Resolved:   72C0212E67A08BCE

Some highlights from the output:

  • Transaction hash (line 2): The hash of the confirmed transaction being analyzed.

  • Block height (line 4): The block where the transaction was confirmed, used to query resolution statements.

  • Transaction index (line 5): The 0-based position of the transaction in the block, converted to a 1-based primaryId for matching against resolution entries.

  • Recipient and alias checks (lines 6-9): The recipient field contains the encoded namespace alias, and the alias flag check confirms it is a namespace alias rather than a regular address. The mosaic alias check confirms that E74B99BA41F4AFEE is also a namespace alias.

  • Resolved address (lines 15-16): The address resolution shows that the namespace alias resolved to TCWYXKVYBMO4NBCUF3AXKJMXCGVSYQOS7ZG2TLI for this transaction.

  • Resolved mosaic ID (lines 22-23): The mosaic resolution shows that the unresolved namespace ID E74B99BA41F4AFEE resolved to mosaic ID 72C0212E67A08BCE for this transaction.

    Resolution statements only contain numeric IDs

    Resolution statements contain mosaic IDs, not human-readable names like symbol.xym. To retrieve the readable name from an ID, use /namespaces/names POST.

Conclusion⚓︎

This tutorial showed how to:

Step Related documentation
Retrieve a confirmed transaction /transactions/confirmed/{transactionId} GET
Query address resolutions /statements/resolutions/address GET
Query mosaic resolutions /statements/resolutions/mosaic GET