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.
importjsonimportosimporturllib.requestNODE_URL=os.environ.get('NODE_URL','https://001-sai-dual.symboltest.net:3001')print(f'Using node {NODE_URL}')defget_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:withurllib.request.urlopen(f'{NODE_URL}{account_path}')asresponse:account_info=json.loads(response.read().decode())returnaccount_info['account']excepturllib.error.HTTPErrorase:ife.status==404:print(f'Address does not exist: {e}')elife.status==409:print(f'Address is not properly formatted: {e}')else:print(f'Unexpected error: {e}')raiseSystemExit(1)defget_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}'formosaic_idinmosaic_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'})withurllib.request.urlopen(request)asresponse:names_info=json.loads(response.read().decode())# Build a dictionary mapping mosaic IDs to their namesnames_map={}forentryinnames_info['mosaicNames']:mosaic_id=int(entry['mosaicId'],16)names_map[mosaic_id]=entry['names']returnnames_mapdefget_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}'formosaic_idinmosaic_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'})withurllib.request.urlopen(request)asresponse:mosaics_info=json.loads(response.read().decode())# Build a dictionary mapping mosaic IDs to their propertiesmosaics_map={}forentryinmosaics_info:mosaic_id=int(entry['mosaic']['id'],16)mosaics_map[mosaic_id]=entry['mosaic']returnmosaics_mapdefformat_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 """ifdivisibility==0:returnstr(amount)whole_part=amount//(10**divisibility)fractional_part=amount%(10**divisibility)returnf'{whole_part}.{fractional_part:0{divisibility}d}'# The account address to queryADDRESS=os.environ.get('ADDRESS','TBIL6D6RURP45YQRWV6Q7YVWIIPLQGLZQFHWFEQ')print(f'Fetching account information from {ADDRESS}')# Get account informationaccount=get_account_info(ADDRESS)# Display balances for all mosaics the account holdsaccount_mosaics=account['mosaics']ifnotaccount_mosaics:print('Account holds no mosaics')else:print(f'Account holds {len(account_mosaics)} mosaic(s):')# Fetch mosaic properties and names for all mosaicsmosaic_ids=[int(m['id'],16)forminaccount_mosaics]mosaic_names=get_mosaic_names(mosaic_ids)mosaics_info=get_mosaics_info(mosaic_ids)formosaic_entryinaccount_mosaics:mosaic_id=int(mosaic_entry['id'],16)balance=int(mosaic_entry['amount'])# Get mosaic propertiesinfo=mosaics_info[mosaic_id]divisibility=info['divisibility']# Format and display the balanceformatted_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,[])ifnames: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}')
constNODE_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 */asyncfunctiongetAccountInfo(accountIdentifier){constaccountPath=`/accounts/${accountIdentifier}`;constaccountResponse=awaitfetch(`${NODE_URL}${accountPath}`);if(!accountResponse.ok){if(accountResponse.status===404){console.error('Address does not exist:',accountResponse.statusText);}elseif(accountResponse.status===409){console.error('Address is not properly formatted:',accountResponse.statusText);}else{console.error('Unexpected error:',accountResponse.statusText);}process.exit(1);}constaccountInfo=awaitaccountResponse.json();returnaccountInfo.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 */asyncfunctiongetMosaicNames(mosaicIds){constmosaicIdsHex=mosaicIds.map(id=>id.toString(16).toUpperCase().padStart(16,'0'));constresponse=awaitfetch(`${NODE_URL}/namespaces/mosaic/names`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({mosaicIds:mosaicIdsHex})});constnamesInfo=awaitresponse.json();// Build a map from mosaic IDs to their namesconstnamesMap=newMap();for(constentryofnamesInfo.mosaicNames){constmosaicId=BigInt(`0x${entry.mosaicId}`);namesMap.set(mosaicId,entry.names);}returnnamesMap;}/** * 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 */asyncfunctiongetMosaicsInfo(mosaicIds){constmosaicIdsHex=mosaicIds.map(id=>id.toString(16).toUpperCase().padStart(16,'0'));constresponse=awaitfetch(`${NODE_URL}/mosaics`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({mosaicIds:mosaicIdsHex})});constmosaicsInfo=awaitresponse.json();// Build a map from mosaic IDs to their propertiesconstmosaicsMap=newMap();for(constentryofmosaicsInfo){constmosaicId=BigInt(`0x${entry.mosaic.id}`);mosaicsMap.set(mosaicId,entry.mosaic);}returnmosaicsMap;}/** * 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 */functionformatAmount(amount,divisibility){if(divisibility===0){returnamount.toString();}constdivisor=10n**BigInt(divisibility);constwholePart=amount/divisor;constfractionalPart=amount%divisor;constfractionalStr=fractionalPart.toString().padStart(divisibility,'0');return`${wholePart}.${fractionalStr}`;}// The account address to queryconstADDRESS=process.env.ADDRESS||'TBIL6D6RURP45YQRWV6Q7YVWIIPLQGLZQFHWFEQ';console.log('Fetching account information from',ADDRESS);// Get account informationconstaccount=awaitgetAccountInfo(ADDRESS);// Display balances for all mosaics the account holdsconstaccountMosaics=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 mosaicsconstmosaicIds=accountMosaics.map(m=>BigInt(`0x${m.id}`));constmosaicNames=awaitgetMosaicNames(mosaicIds);constmosaicsInfo=awaitgetMosaicsInfo(mosaicIds);for(constmosaicEntryofaccountMosaics){constmosaicId=BigInt(`0x${mosaicEntry.id}`);constbalance=BigInt(mosaicEntry.amount);// Get mosaic propertiesconstinfo=mosaicsInfo.get(mosaicId);constdivisibility=info.divisibility;// Format and display the balanceconstformattedBalance=formatAmount(balance,divisibility);constmosaicIdHex=`0x${mosaicId.toString(16).toUpperCase().padStart(16,'0')}`;// Display mosaic ID and names (if available)constnames=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}`);}}
defget_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:withurllib.request.urlopen(f'{NODE_URL}{account_path}')asresponse:account_info=json.loads(response.read().decode())returnaccount_info['account']excepturllib.error.HTTPErrorase:ife.status==404:print(f'Address does not exist: {e}')elife.status==409:print(f'Address is not properly formatted: {e}')else:print(f'Unexpected error: {e}')raiseSystemExit(1)
/** * Fetch account information by address or public key. * * @param {string} accountIdentifier - Account address or public key * @returns {Promise<Object>} The account information */asyncfunctiongetAccountInfo(accountIdentifier){constaccountPath=`/accounts/${accountIdentifier}`;constaccountResponse=awaitfetch(`${NODE_URL}${accountPath}`);if(!accountResponse.ok){if(accountResponse.status===404){console.error('Address does not exist:',accountResponse.statusText);}elseif(accountResponse.status===409){console.error('Address is not properly formatted:',accountResponse.statusText);}else{console.error('Unexpected error:',accountResponse.statusText);}process.exit(1);}constaccountInfo=awaitaccountResponse.json();returnaccountInfo.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.
defget_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}'formosaic_idinmosaic_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'})withurllib.request.urlopen(request)asresponse:names_info=json.loads(response.read().decode())# Build a dictionary mapping mosaic IDs to their namesnames_map={}forentryinnames_info['mosaicNames']:mosaic_id=int(entry['mosaicId'],16)names_map[mosaic_id]=entry['names']returnnames_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 */asyncfunctiongetMosaicNames(mosaicIds){constmosaicIdsHex=mosaicIds.map(id=>id.toString(16).toUpperCase().padStart(16,'0'));constresponse=awaitfetch(`${NODE_URL}/namespaces/mosaic/names`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({mosaicIds:mosaicIdsHex})});constnamesInfo=awaitresponse.json();// Build a map from mosaic IDs to their namesconstnamesMap=newMap();for(constentryofnamesInfo.mosaicNames){constmosaicId=BigInt(`0x${entry.mosaicId}`);namesMap.set(mosaicId,entry.names);}returnnamesMap;}
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/namesPOST 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.
defget_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}'formosaic_idinmosaic_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'})withurllib.request.urlopen(request)asresponse:mosaics_info=json.loads(response.read().decode())# Build a dictionary mapping mosaic IDs to their propertiesmosaics_map={}forentryinmosaics_info:mosaic_id=int(entry['mosaic']['id'],16)mosaics_map[mosaic_id]=entry['mosaic']returnmosaics_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 */asyncfunctiongetMosaicsInfo(mosaicIds){constmosaicIdsHex=mosaicIds.map(id=>id.toString(16).toUpperCase().padStart(16,'0'));constresponse=awaitfetch(`${NODE_URL}/mosaics`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({mosaicIds:mosaicIdsHex})});constmosaicsInfo=awaitresponse.json();// Build a map from mosaic IDs to their propertiesconstmosaicsMap=newMap();for(constentryofmosaicsInfo){constmosaicId=BigInt(`0x${entry.mosaic.id}`);mosaicsMap.set(mosaicId,entry.mosaic);}returnmosaicsMap;}
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 /mosaicsPOST endpoint, which accepts multiple mosaic IDs in a single request
and returns detailed information about each mosaic, including its divisibility.
defformat_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 """ifdivisibility==0:returnstr(amount)whole_part=amount//(10**divisibility)fractional_part=amount%(10**divisibility)returnf'{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 */functionformatAmount(amount,divisibility){if(divisibility===0){returnamount.toString();}constdivisor=10n**BigInt(divisibility);constwholePart=amount/divisor;constfractionalPart=amount%divisor;constfractionalStr=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.
# The account address to queryADDRESS=os.environ.get('ADDRESS','TBIL6D6RURP45YQRWV6Q7YVWIIPLQGLZQFHWFEQ')print(f'Fetching account information from {ADDRESS}')# Get account informationaccount=get_account_info(ADDRESS)# Display balances for all mosaics the account holdsaccount_mosaics=account['mosaics']ifnotaccount_mosaics:print('Account holds no mosaics')else:print(f'Account holds {len(account_mosaics)} mosaic(s):')# Fetch mosaic properties and names for all mosaicsmosaic_ids=[int(m['id'],16)forminaccount_mosaics]mosaic_names=get_mosaic_names(mosaic_ids)mosaics_info=get_mosaics_info(mosaic_ids)formosaic_entryinaccount_mosaics:mosaic_id=int(mosaic_entry['id'],16)balance=int(mosaic_entry['amount'])# Get mosaic propertiesinfo=mosaics_info[mosaic_id]divisibility=info['divisibility']# Format and display the balanceformatted_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,[])ifnames: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 queryconstADDRESS=process.env.ADDRESS||'TBIL6D6RURP45YQRWV6Q7YVWIIPLQGLZQFHWFEQ';console.log('Fetching account information from',ADDRESS);// Get account informationconstaccount=awaitgetAccountInfo(ADDRESS);// Display balances for all mosaics the account holdsconstaccountMosaics=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 mosaicsconstmosaicIds=accountMosaics.map(m=>BigInt(`0x${m.id}`));constmosaicNames=awaitgetMosaicNames(mosaicIds);constmosaicsInfo=awaitgetMosaicsInfo(mosaicIds);for(constmosaicEntryofaccountMosaics){constmosaicId=BigInt(`0x${mosaicEntry.id}`);constbalance=BigInt(mosaicEntry.amount);// Get mosaic propertiesconstinfo=mosaicsInfo.get(mosaicId);constdivisibility=info.divisibility;// Format and display the balanceconstformattedBalance=formatAmount(balance,divisibility);constmosaicIdHex=`0x${mosaicId.toString(16).toUpperCase().padStart(16,'0')}`;// Display mosaic ID and names (if available)constnames=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:
Retrieve the account information.
Extract the mosaic IDs from the account.
Fetch mosaic properties and namespace names for all mosaics.
Iterate through each mosaic and format the balance with the appropriate decimal places.
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).