コンテンツにスキップ
初級

アカウント残高の照会⚓︎

Symbol上の アカウント は、ネットワークのネイティブ通貨である XYM を含む モザイク(代替可能トークン)を保持できます。

このチュートリアルでは、アカウントのモザイク残高を照会し、適切な小数点以下の桁数で表示する方法を説明します。モザイクID にリンクされた ネームスペース がある場合、それも取得してモザイクのフレンドリーネームとして表示します。

前提条件⚓︎

このチュートリアルでは、SDKを必要とせずに Symbol REST API を使用します。 HTTPリクエストを行う方法さえあれば実行可能です。

完全なコード⚓︎

このチュートリアルの完全なコード一覧を以下に示します。 詳細な手順ごとの説明は次のセクションで行います。

import json
import os
import urllib.request

NODE_URL = os.getenv(
    'NODE_URL', 'https://reference.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.getenv(
    '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://reference.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

このスニペットでは、NODE_URL 環境変数を使用して Symbol API ノードを設定します。 値が指定されない場合は、デフォルト値が使用されます。

このチュートリアルでは、以下の関数を定義しています。

  • get_account_info(): アドレスまたは公開鍵によって アカウント の状態を取得します。
  • get_mosaic_names(): モザイクの ネームスペース エイリアスを取得します。
  • get_mosaics_info(): 1回のリクエストで複数の モザイク のプロパティを取得します。
  • format_amount(): モザイクの 可分性 に基づいて、適切な小数点以下の桁数で金額をフォーマットします。

コード解説⚓︎

アカウント情報の取得⚓︎

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;
}

/accounts/{accountId} GET エンドポイントは、保持しているすべてのモザイクを含むアカウントの状態を取得します。

アドレス または 公開鍵 のいずれかを使用してアカウントを照会できます。

モザイク名の取得⚓︎

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;
}

モザイクは64ビットの数値である モザイク ID によって識別されますが、これは読みにくく覚えにくい場合があります。 利便性を高めるために、モザイクを人間が読み取り可能な ネームスペース エイリアスにリンクさせることができます。

/namespaces/mosaic/names POST エンドポイントは、複数のモザイクIDを受け取り、現在それらにリンクされているネームスペース名を返します。 モザイクにリンクされたネームスペースがない場合、そのモザイクはレスポンスに含まれません。 1つのモザイクに複数のネームスペースエイリアスが存在する場合もあります(異なるネームスペースが同じモザイクにリンクできるため)。 この場合、コードではすべてのエイリアスをカンマで区切って表示します。

モザイクプロパティの取得⚓︎

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;
}

モザイク残高を正しくフォーマットするために、スニペットはネットワークからプロパティを取得します。 必要な主要プロパティは 可分性 であり、これはモザイクがサポートする小数点以下の桁数を定義します。

個別のモザイクについては /mosaics/{mosaicId} GET エンドポイントを使用してプロパティを取得できます。 このスニペットで使用されているオプションは /mosaics POST エンドポイントで、1回のリクエストで複数のモザイクIDを受け取り、可分性を含む各モザイクの詳細情報を返します。

金額のフォーマット⚓︎

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}`;
}

このユーティリティ関数は、絶対量 (atomic amount) を人間にとってわかりやすい表現に変換します。

  • 絶対量 (Atomic amount): ブロックチェーン上に保存される生の数値で、整数として表されます。
  • フォーマット済みの金額 (Formatted amount): モザイクの可分性によって決定される小数点以下の桁数を持つ表示形式。

フォーマット処理では、\(10^{\text{可分性}}\) で除算し、その余りを取ることで、絶対量を整数部分と小数部分に分割します。 小数部分は、常に正しい小数点以下の桁数が表示されるようにゼロパディングされます。

すべてを組み合わせる⚓︎

# The account address to query
ADDRESS = os.getenv(
    '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}`);
    }
}

メインコードは ADDRESS 環境変数を読み取って、どのアカウントを照会するかを決定します。 値が指定されない場合は、デフォルトのサンプルアドレスを使用します。

ヘルパー関数を以下のように連携させます。

  1. アカウント情報を取得する。
  2. アカウントからモザイクIDを抽出する。
  3. すべてのモザイクのプロパティとネームスペース名を取得する。
  4. 各モザイクをループし、適切な小数点以下の桁数で残高をフォーマットする。

出力⚓︎

以下に示す出力は、プログラムの典型的な実行結果に対応しています。

Using node https://reference.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

出力には、アカウントが保持するすべてのモザイクが表示されます。モザイクごとに可分性の値が異なることに注目してください。

  • 1つ目のモザイクは可分性が6で、小数点以下6桁が表示されています (592.589332)。 また、フレンドリーネーム (symbol.xym) を持っており、これがネットワークのネイティブ通貨であることを示しています。
  • 2つ目は可分性が0で、小数点以下が表示されていません (999999)。
  • 3つ目は可分性が2で、小数点以下2桁が表示されています (0.01)。

結論⚓︎

このチュートリアルでは、以下の方法を説明しました。

ステップ 関連ドキュメント
アカウント状態の取得 /accounts/{accountId} GET
モザイク名の取得 /namespaces/mosaic/names POST
モザイクプロパティの取得 /mosaics/{mosaicId} GET