Skip to content

Querying an Account Balance⚓︎

Accounts on Symbol can hold mosaics (fungible tokens), including the native currency XYM.

This tutorial shows how to query an account's mosaic balances and display them with the appropriate number of decimal places. If the mosaic ID has a linked namespace, it is also retrieved and displayed as the mosaic's friendly name.

Prerequisites⚓︎

This tutorial uses the Symbol REST API without requiring an SDK. You only need a way to make HTTP requests.

Full Code⚓︎

import json
import os
import urllib.request

NODE_URL = os.environ.get(
    'NODE_URL', 'https://001-sai-dual.symboltest.net:3001')
print(f'Using node {NODE_URL}')


def get_account_info(account_identifier):
    """
    Fetch account information by address or public key.

    Args:
        account_identifier: The account address or public key

    Returns:
        Dictionary containing the account information
    """
    account_path = f'/accounts/{account_identifier}'
    try:
        with urllib.request.urlopen(
                f'{NODE_URL}{account_path}') as response:
            account_info = json.loads(response.read().decode())
            return account_info['account']
    except urllib.error.HTTPError as e:
        if e.status == 404:
            print(f'Address does not exist: {e}')
        elif e.status == 409:
            print(f'Address is not properly formatted: {e}')
        else:
            print(f'Unexpected error: {e}')
        raise SystemExit(1)


def get_mosaic_names(mosaic_ids):
    """
    Fetch friendly names for a set of mosaics.

    Args:
        mosaic_ids: List of mosaic IDs as integers

    Returns:
        Dictionary mapping mosaic IDs to their namespace names
    """
    mosaic_ids_hex = [f'{mosaic_id:016X}' for mosaic_id in mosaic_ids]
    request_body = json.dumps({'mosaicIds': mosaic_ids_hex}).encode()
    request = urllib.request.Request(
        f'{NODE_URL}/namespaces/mosaic/names',
        data=request_body,
        headers={'Content-Type': 'application/json'}
    )
    with urllib.request.urlopen(request) as response:
        names_info = json.loads(response.read().decode())
        # Build a dictionary mapping mosaic IDs to their names
        names_map = {}
        for entry in names_info['mosaicNames']:
            mosaic_id = int(entry['mosaicId'], 16)
            names_map[mosaic_id] = entry['names']
        return names_map


def get_mosaics_info(mosaic_ids):
    """
    Fetch information for multiple mosaics in a single request.

    Args:
        mosaic_ids: List of mosaic IDs as integers

    Returns:
        Dictionary mapping mosaic IDs to their properties
    """
    mosaic_ids_hex = [f'{mosaic_id:016X}' for mosaic_id in mosaic_ids]
    request_body = json.dumps({'mosaicIds': mosaic_ids_hex}).encode()
    request = urllib.request.Request(
        f'{NODE_URL}/mosaics',
        data=request_body,
        headers={'Content-Type': 'application/json'}
    )
    with urllib.request.urlopen(request) as response:
        mosaics_info = json.loads(response.read().decode())
        # Build a dictionary mapping mosaic IDs to their properties
        mosaics_map = {}
        for entry in mosaics_info:
            mosaic_id = int(entry['mosaic']['id'], 16)
            mosaics_map[mosaic_id] = entry['mosaic']
        return mosaics_map


def format_amount(amount, divisibility):
    """
    Format an atomic amount with decimal places.

    Args:
        amount: The atomic amount as an integer
        divisibility: Number of decimal places

    Returns:
        Formatted amount as a string
    """
    if divisibility == 0:
        return str(amount)
    whole_part = amount // (10 ** divisibility)
    fractional_part = amount % (10 ** divisibility)
    return f'{whole_part}.{fractional_part:0{divisibility}d}'


# The account address to query
ADDRESS = os.environ.get(
    'ADDRESS', 'TBIL6D6RURP45YQRWV6Q7YVWIIPLQGLZQFHWFEQ')
print(f'Fetching account information from {ADDRESS}')

# Get account information
account = get_account_info(ADDRESS)

# Display balances for all mosaics the account holds
account_mosaics = account['mosaics']
if not account_mosaics:
    print('Account holds no mosaics')
else:
    print(f'Account holds {len(account_mosaics)} mosaic(s):')

# Fetch mosaic properties and names for all mosaics
mosaic_ids = [int(m['id'], 16) for m in account_mosaics]
mosaic_names = get_mosaic_names(mosaic_ids)
mosaics_info = get_mosaics_info(mosaic_ids)

for mosaic_entry in account_mosaics:
    mosaic_id = int(mosaic_entry['id'], 16)
    balance = int(mosaic_entry['amount'])

    # Get mosaic properties
    info = mosaics_info[mosaic_id]
    divisibility = info['divisibility']

    # Format and display the balance
    formatted_balance = format_amount(balance, divisibility)
    mosaic_id_hex = f'0x{mosaic_id:016X}'

    # Display mosaic ID and names (if available)
    names = mosaic_names.get(mosaic_id, [])
    if names:
        names_str = ', '.join(names)
        print(f'- Mosaic {mosaic_id_hex} ({names_str})')
    else:
        print(f'- Mosaic {mosaic_id_hex}')

    print(f'  Balance: {formatted_balance}')
    print(f'  Balance (atomic): {balance}')
    print(f'  Divisibility: {divisibility}')

Download source

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


/**
 * Fetch account information by address or public key.
 *
 * @param {string} accountIdentifier - Account address or public key
 * @returns {Promise<Object>} The account information
 */
async function getAccountInfo(accountIdentifier) {
    const accountPath = `/accounts/${accountIdentifier}`;
    const accountResponse = await fetch(`${NODE_URL}${accountPath}`);
    if (!accountResponse.ok) {
        if (accountResponse.status === 404) {
            console.error(
                'Address does not exist:', accountResponse.statusText);
        } else if (accountResponse.status === 409) {
            console.error(
                'Address is not properly formatted:',
                accountResponse.statusText);
        }
        else {
            console.error(
                'Unexpected error:', accountResponse.statusText);
        }
        process.exit(1);
    }
    const accountInfo = await accountResponse.json();
    return accountInfo.account;
}

/**
 * Fetch friendly names for a set of mosaics.
 *
 * @param {bigint[]} mosaicIds - Array of mosaic IDs
 * @returns {Promise<Map>} Map of mosaic IDs to their namespace names
 */
async function getMosaicNames(mosaicIds) {
    const mosaicIdsHex = mosaicIds.map(
        id => id.toString(16).toUpperCase().padStart(16, '0')
    );
    const response = await fetch(`${NODE_URL}/namespaces/mosaic/names`, {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({mosaicIds: mosaicIdsHex})
    });
    const namesInfo = await response.json();
    // Build a map from mosaic IDs to their names
    const namesMap = new Map();
    for (const entry of namesInfo.mosaicNames) {
        const mosaicId = BigInt(`0x${entry.mosaicId}`);
        namesMap.set(mosaicId, entry.names);
    }
    return namesMap;
}

/**
 * Fetch information for multiple mosaics in a single request.
 *
 * @param {bigint[]} mosaicIds - Array of mosaic IDs
 * @returns {Promise<Map>} Map of mosaic IDs to their properties
 */
async function getMosaicsInfo(mosaicIds) {
    const mosaicIdsHex = mosaicIds.map(
        id => id.toString(16).toUpperCase().padStart(16, '0')
    );
    const response = await fetch(`${NODE_URL}/mosaics`, {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({mosaicIds: mosaicIdsHex})
    });
    const mosaicsInfo = await response.json();
    // Build a map from mosaic IDs to their properties
    const mosaicsMap = new Map();
    for (const entry of mosaicsInfo) {
        const mosaicId = BigInt(`0x${entry.mosaic.id}`);
        mosaicsMap.set(mosaicId, entry.mosaic);
    }
    return mosaicsMap;
}

/**
 * Format an atomic amount with decimal places.
 *
 * @param {bigint} amount - The atomic amount
 * @param {number} divisibility - Number of decimal places
 * @returns {string} The formatted amount
 */
function formatAmount(amount, divisibility) {
    if (divisibility === 0) {
        return amount.toString();
    }
    const divisor = 10n ** BigInt(divisibility);
    const wholePart = amount / divisor;
    const fractionalPart = amount % divisor;
    const fractionalStr = fractionalPart.toString()
        .padStart(divisibility, '0');
    return `${wholePart}.${fractionalStr}`;
}

// The account address to query
const ADDRESS = process.env.ADDRESS ||
    'TBIL6D6RURP45YQRWV6Q7YVWIIPLQGLZQFHWFEQ';
console.log('Fetching account information from', ADDRESS);

// Get account information
const account = await getAccountInfo(ADDRESS);

// Display balances for all mosaics the account holds
const accountMosaics = account.mosaics;
if (accountMosaics.length === 0) {
    console.log('Account holds no mosaics');
} else {
    console.log(`Account holds ${accountMosaics.length} mosaic(s):`);

    // Fetch mosaic properties and names for all mosaics
    const mosaicIds = accountMosaics.map(m => BigInt(`0x${m.id}`));
    const mosaicNames = await getMosaicNames(mosaicIds);
    const mosaicsInfo = await getMosaicsInfo(mosaicIds);

    for (const mosaicEntry of accountMosaics) {
        const mosaicId = BigInt(`0x${mosaicEntry.id}`);
        const balance = BigInt(mosaicEntry.amount);

        // Get mosaic properties
        const info = mosaicsInfo.get(mosaicId);
        const divisibility = info.divisibility;

        // Format and display the balance
        const formattedBalance = formatAmount(balance, divisibility);
        const mosaicIdHex =
            `0x${mosaicId.toString(16).toUpperCase().padStart(16, '0')}`;

        // Display mosaic ID and names (if available)
        const names = mosaicNames.get(mosaicId) || [];
        if (names.length > 0) {
            console.log(`- Mosaic ${mosaicIdHex} (${names.join(', ')})`);
        } else {
            console.log(`- Mosaic ${mosaicIdHex}`);
        }

        console.log(`  Balance: ${formattedBalance}`);
        console.log(`  Balance (atomic): ${balance.toString()}`);
        console.log(`  Divisibility: ${divisibility}`);
    }
}

Download source

The snippet uses the NODE_URL environment variable to set the Symbol API node. If no value is provided, a default one is used.

The tutorial defines the following functions:

  • get_account_info(): Fetches account state by address or public key.
  • get_mosaic_names(): Fetches namespace aliases for mosaics.
  • get_mosaics_info(): Fetches properties for multiple mosaics in a single request.
  • format_amount(): Formats amounts with the appropriate number of decimal places, according to their divisibility.

Code Explanation⚓︎

Fetching Account Information⚓︎

def get_account_info(account_identifier):
    """
    Fetch account information by address or public key.

    Args:
        account_identifier: The account address or public key

    Returns:
        Dictionary containing the account information
    """
    account_path = f'/accounts/{account_identifier}'
    try:
        with urllib.request.urlopen(
                f'{NODE_URL}{account_path}') as response:
            account_info = json.loads(response.read().decode())
            return account_info['account']
    except urllib.error.HTTPError as e:
        if e.status == 404:
            print(f'Address does not exist: {e}')
        elif e.status == 409:
            print(f'Address is not properly formatted: {e}')
        else:
            print(f'Unexpected error: {e}')
        raise SystemExit(1)
/**
 * Fetch account information by address or public key.
 *
 * @param {string} accountIdentifier - Account address or public key
 * @returns {Promise<Object>} The account information
 */
async function getAccountInfo(accountIdentifier) {
    const accountPath = `/accounts/${accountIdentifier}`;
    const accountResponse = await fetch(`${NODE_URL}${accountPath}`);
    if (!accountResponse.ok) {
        if (accountResponse.status === 404) {
            console.error(
                'Address does not exist:', accountResponse.statusText);
        } else if (accountResponse.status === 409) {
            console.error(
                'Address is not properly formatted:',
                accountResponse.statusText);
        }
        else {
            console.error(
                'Unexpected error:', accountResponse.statusText);
        }
        process.exit(1);
    }
    const accountInfo = await accountResponse.json();
    return accountInfo.account;
}

The /accounts/{accountId} GET endpoint retrieves the state of an account, including all the mosaics it holds.

You can query an account using either its address or its public key.

Fetching Mosaic Names⚓︎

def get_mosaic_names(mosaic_ids):
    """
    Fetch friendly names for a set of mosaics.

    Args:
        mosaic_ids: List of mosaic IDs as integers

    Returns:
        Dictionary mapping mosaic IDs to their namespace names
    """
    mosaic_ids_hex = [f'{mosaic_id:016X}' for mosaic_id in mosaic_ids]
    request_body = json.dumps({'mosaicIds': mosaic_ids_hex}).encode()
    request = urllib.request.Request(
        f'{NODE_URL}/namespaces/mosaic/names',
        data=request_body,
        headers={'Content-Type': 'application/json'}
    )
    with urllib.request.urlopen(request) as response:
        names_info = json.loads(response.read().decode())
        # Build a dictionary mapping mosaic IDs to their names
        names_map = {}
        for entry in names_info['mosaicNames']:
            mosaic_id = int(entry['mosaicId'], 16)
            names_map[mosaic_id] = entry['names']
        return names_map
/**
 * Fetch friendly names for a set of mosaics.
 *
 * @param {bigint[]} mosaicIds - Array of mosaic IDs
 * @returns {Promise<Map>} Map of mosaic IDs to their namespace names
 */
async function getMosaicNames(mosaicIds) {
    const mosaicIdsHex = mosaicIds.map(
        id => id.toString(16).toUpperCase().padStart(16, '0')
    );
    const response = await fetch(`${NODE_URL}/namespaces/mosaic/names`, {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({mosaicIds: mosaicIdsHex})
    });
    const namesInfo = await response.json();
    // Build a map from mosaic IDs to their names
    const namesMap = new Map();
    for (const entry of namesInfo.mosaicNames) {
        const mosaicId = BigInt(`0x${entry.mosaicId}`);
        namesMap.set(mosaicId, entry.names);
    }
    return namesMap;
}

Mosaics are identified by 64-bit numeric IDs, which can be hard to read and remember. To improve usability, mosaics can be linked to human-readable namespace aliases.

The /namespaces/mosaic/names POST endpoint accepts multiple mosaic IDs and returns any namespace names that are currently linked to them. If a mosaic has no linked namespace, it will not appear in the response.

A mosaic can have multiple namespace aliases (different namespaces can link to the same mosaic). When this happens, the code displays all aliases separated by commas.

Fetching Mosaic Properties⚓︎

def get_mosaics_info(mosaic_ids):
    """
    Fetch information for multiple mosaics in a single request.

    Args:
        mosaic_ids: List of mosaic IDs as integers

    Returns:
        Dictionary mapping mosaic IDs to their properties
    """
    mosaic_ids_hex = [f'{mosaic_id:016X}' for mosaic_id in mosaic_ids]
    request_body = json.dumps({'mosaicIds': mosaic_ids_hex}).encode()
    request = urllib.request.Request(
        f'{NODE_URL}/mosaics',
        data=request_body,
        headers={'Content-Type': 'application/json'}
    )
    with urllib.request.urlopen(request) as response:
        mosaics_info = json.loads(response.read().decode())
        # Build a dictionary mapping mosaic IDs to their properties
        mosaics_map = {}
        for entry in mosaics_info:
            mosaic_id = int(entry['mosaic']['id'], 16)
            mosaics_map[mosaic_id] = entry['mosaic']
        return mosaics_map
/**
 * Fetch information for multiple mosaics in a single request.
 *
 * @param {bigint[]} mosaicIds - Array of mosaic IDs
 * @returns {Promise<Map>} Map of mosaic IDs to their properties
 */
async function getMosaicsInfo(mosaicIds) {
    const mosaicIdsHex = mosaicIds.map(
        id => id.toString(16).toUpperCase().padStart(16, '0')
    );
    const response = await fetch(`${NODE_URL}/mosaics`, {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({mosaicIds: mosaicIdsHex})
    });
    const mosaicsInfo = await response.json();
    // Build a map from mosaic IDs to their properties
    const mosaicsMap = new Map();
    for (const entry of mosaicsInfo) {
        const mosaicId = BigInt(`0x${entry.mosaic.id}`);
        mosaicsMap.set(mosaicId, entry.mosaic);
    }
    return mosaicsMap;
}

To format mosaic balances correctly, the snippet fetches their properties from the network. The key property required is divisibility, which defines how many decimal places a mosaic supports.

You can retrieve mosaic properties using the /mosaics/{mosaicId} GET endpoint for individual mosaics. The option used in this snippet is the /mosaics POST endpoint, which accepts multiple mosaic IDs in a single request and returns detailed information about each mosaic, including its divisibility.

Formatting Amounts⚓︎

def format_amount(amount, divisibility):
    """
    Format an atomic amount with decimal places.

    Args:
        amount: The atomic amount as an integer
        divisibility: Number of decimal places

    Returns:
        Formatted amount as a string
    """
    if divisibility == 0:
        return str(amount)
    whole_part = amount // (10 ** divisibility)
    fractional_part = amount % (10 ** divisibility)
    return f'{whole_part}.{fractional_part:0{divisibility}d}'
/**
 * Format an atomic amount with decimal places.
 *
 * @param {bigint} amount - The atomic amount
 * @param {number} divisibility - Number of decimal places
 * @returns {string} The formatted amount
 */
function formatAmount(amount, divisibility) {
    if (divisibility === 0) {
        return amount.toString();
    }
    const divisor = 10n ** BigInt(divisibility);
    const wholePart = amount / divisor;
    const fractionalPart = amount % divisor;
    const fractionalStr = fractionalPart.toString()
        .padStart(divisibility, '0');
    return `${wholePart}.${fractionalStr}`;
}

This utility function converts atomic amounts into human-friendly representations:

  • Atomic amount: The raw value stored on the blockchain, expressed as an integer.
  • Formatted amount: The display format with decimal places determined by the mosaic's divisibility.

The formatting splits the atomic amount into whole and fractional parts by dividing and taking the remainder with respect to \(10^{\text{divisibility}}\). The fractional part is then zero-padded to ensure it always displays the correct number of decimal places.

Putting It All Together⚓︎

# The account address to query
ADDRESS = os.environ.get(
    'ADDRESS', 'TBIL6D6RURP45YQRWV6Q7YVWIIPLQGLZQFHWFEQ')
print(f'Fetching account information from {ADDRESS}')

# Get account information
account = get_account_info(ADDRESS)

# Display balances for all mosaics the account holds
account_mosaics = account['mosaics']
if not account_mosaics:
    print('Account holds no mosaics')
else:
    print(f'Account holds {len(account_mosaics)} mosaic(s):')

# Fetch mosaic properties and names for all mosaics
mosaic_ids = [int(m['id'], 16) for m in account_mosaics]
mosaic_names = get_mosaic_names(mosaic_ids)
mosaics_info = get_mosaics_info(mosaic_ids)

for mosaic_entry in account_mosaics:
    mosaic_id = int(mosaic_entry['id'], 16)
    balance = int(mosaic_entry['amount'])

    # Get mosaic properties
    info = mosaics_info[mosaic_id]
    divisibility = info['divisibility']

    # Format and display the balance
    formatted_balance = format_amount(balance, divisibility)
    mosaic_id_hex = f'0x{mosaic_id:016X}'

    # Display mosaic ID and names (if available)
    names = mosaic_names.get(mosaic_id, [])
    if names:
        names_str = ', '.join(names)
        print(f'- Mosaic {mosaic_id_hex} ({names_str})')
    else:
        print(f'- Mosaic {mosaic_id_hex}')

    print(f'  Balance: {formatted_balance}')
    print(f'  Balance (atomic): {balance}')
    print(f'  Divisibility: {divisibility}')
// The account address to query
const ADDRESS = process.env.ADDRESS ||
    'TBIL6D6RURP45YQRWV6Q7YVWIIPLQGLZQFHWFEQ';
console.log('Fetching account information from', ADDRESS);

// Get account information
const account = await getAccountInfo(ADDRESS);

// Display balances for all mosaics the account holds
const accountMosaics = account.mosaics;
if (accountMosaics.length === 0) {
    console.log('Account holds no mosaics');
} else {
    console.log(`Account holds ${accountMosaics.length} mosaic(s):`);

    // Fetch mosaic properties and names for all mosaics
    const mosaicIds = accountMosaics.map(m => BigInt(`0x${m.id}`));
    const mosaicNames = await getMosaicNames(mosaicIds);
    const mosaicsInfo = await getMosaicsInfo(mosaicIds);

    for (const mosaicEntry of accountMosaics) {
        const mosaicId = BigInt(`0x${mosaicEntry.id}`);
        const balance = BigInt(mosaicEntry.amount);

        // Get mosaic properties
        const info = mosaicsInfo.get(mosaicId);
        const divisibility = info.divisibility;

        // Format and display the balance
        const formattedBalance = formatAmount(balance, divisibility);
        const mosaicIdHex =
            `0x${mosaicId.toString(16).toUpperCase().padStart(16, '0')}`;

        // Display mosaic ID and names (if available)
        const names = mosaicNames.get(mosaicId) || [];
        if (names.length > 0) {
            console.log(`- Mosaic ${mosaicIdHex} (${names.join(', ')})`);
        } else {
            console.log(`- Mosaic ${mosaicIdHex}`);
        }

        console.log(`  Balance: ${formattedBalance}`);
        console.log(`  Balance (atomic): ${balance.toString()}`);
        console.log(`  Divisibility: ${divisibility}`);
    }
}

The main code reads the ADDRESS environment variable to determine which account to query. If no value is provided, it uses a default sample address.

It orchestrates the helper functions to:

  1. Retrieve the account information.
  2. Extract the mosaic IDs from the account.
  3. Fetch mosaic properties and namespace names for all mosaics.
  4. Iterate through each mosaic and format the balance with the appropriate decimal places.

Output⚓︎

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

Using node https://001-sai-dual.symboltest.net:3001
Fetching account information from TBIL6D6RURP45YQRWV6Q7YVWIIPLQGLZQFHWFEQ
Account holds 3 mosaic(s):
- Mosaic 0x72C0212E67A08BCE (symbol.xym)
  Balance: 592.589332
  Balance (atomic): 592589332
  Divisibility: 6
- Mosaic 0x0D8B2E6D54A936E6
  Balance: 999999
  Balance (atomic): 999999
  Divisibility: 0
- Mosaic 0x42248928982939ED
  Balance: 0.01
  Balance (atomic): 1
  Divisibility: 2

The output displays all mosaics the account holds. Notice how different mosaics have different divisibility values:

  • The first mosaic has divisibility 6, showing six decimal places (592.589332). It also has a friendly name (symbol.xym), which identifies it as the network's native currency.
  • The second has divisibility 0, showing no decimal places (999999).
  • The third has divisibility 2, showing two decimal places (0.01).

Conclusion⚓︎

This tutorial showed how to:

Step Related documentation
Fetch account state /accounts/{accountId} GET
Fetch mosaic names /namespaces/mosaic/names POST
Fetch mosaic properties /mosaics/{mosaicId} GET
/mosaics POST