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

レシートからのネームスペース解決⚓︎

ネームスペース のエイリアスは 時間の経過とともに変更される 可能性があるため、ネームスペースの現在の値が トランザクション 承認時の値と一致しない場合があります。

生のアドレスや モザイク ID の代わりにエイリアスを使用するたびに、ネットワークは承認時にエイリアスが指していた実際の値を記録した レシート を保存します。ネームスペースの解決を記録するレシートの種類は 解決ステートメント と呼ばれます。

このチュートリアルでは、解決ステートメントを照会して、承認済みトランザクションで使用されたネームスペースエイリアスの背後にある実際のアドレスとモザイクIDを見つける方法を説明します。

前提条件⚓︎

開始する前に、 開発環境をセットアップ してください。

このチュートリアルではネットワークからのデータの読み取りのみを行います。トランザクションの送信は行わないため、 アカウントXYM の残高は必要ありません。

完全なコード⚓︎

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

import json
import os
import urllib.request

from symbolchain.symbol.IdGenerator import is_mosaic_alias
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_alias = is_mosaic_alias(mosaic_id)
        if is_alias:
            aliased_mosaics.add(mosaic['id'])
        print(f'  Mosaic: {mosaic["id"]}')
        print(f'  Is mosaic alias: {is_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.from_decoded_address_hex_string(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 { Address, isMosaicAlias } 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 isAlias = isMosaicAlias(mosaicId);
        if (isAlias) aliasedMosaics.add(mosaic.id);
        console.log(`  Mosaic: ${mosaic.id}`);
        console.log(`  Is mosaic alias: ${isAlias}`);
    }

    // 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 address =
                    Address.fromDecodedAddressHexString(resolved);
                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

コード解説⚓︎

トランザクションハッシュの定義⚓︎

# 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);

コードは、生のアドレスやモザイクIDの代わりにネームスペースエイリアスを使用した、承認済みトランザクションのハッシュを定義します。この ハッシュは TRANSACTION_HASH 環境変数から読み取られ、デフォルトでは Symbol テストネット 上の既知のトランザクションが使用されます。

承認済みトランザクションの取得⚓︎

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

承認済みトランザクションは、そのハッシュを使用して /transactions/confirmed/{transactionId} GET エンドポイントから取得されます。

レスポンスには、トランザクションが承認された ブロック の高さ( height )を含む meta オブジェクトが含まれます。この高さは、その特定のブロックの解決ステートメントを照会するために必要です。

トランザクションのメタデータには index フィールドも含まれており、これはブロック内でのトランザクションの0から始まる位置を示します。コードは、後の解決エントリとの照合のために、これを1から始まる primaryIdindex + 1 )に変換します。

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

取得されたトランザクションには recipientAddress フィールドが含まれます。トランザクションが受信者としてネームスペースエイリアスを使用した場合、このフィールドには実際のアドレスではなく、未解決の値(エンコードされたネームスペースID)が保持されます。

コードは、最初のバイトの最下位ビットを調べることで、受信者がエイリアスであるかどうかを確認します:

  • ビットが 1 の場合、アドレスは ネームスペースのアドレスへのリンク チュートリアルで説明されているように、エンコードされたネームスペースエイリアスです。
  • ビットが 0 の場合、受信者は通常のアドレスであり、解決は不要です。
    aliased_mosaics = set()
    mosaics = tx_data['transaction']['mosaics']
    for mosaic in mosaics:
        mosaic_id = int(mosaic['id'], 16)
        is_alias = is_mosaic_alias(mosaic_id)
        if is_alias:
            aliased_mosaics.add(mosaic['id'])
        print(f'  Mosaic: {mosaic["id"]}')
        print(f'  Is mosaic alias: {is_alias}')
    const aliasedMosaics = new Set();
    const mosaics = txData.transaction.mosaics;
    for (const mosaic of mosaics) {
        const mosaicId = BigInt(`0x${mosaic.id}`);
        const isAlias = isMosaicAlias(mosaicId);
        if (isAlias) aliasedMosaics.add(mosaic.id);
        console.log(`  Mosaic: ${mosaic.id}`);
        console.log(`  Is mosaic alias: ${isAlias}`);
    }

コードはトランザクションの mosaics 配列を反復処理し、SDK の 関数を使用して、各モザイクIDがネームスペースエイリアスかどうかを確認します。この関数は64ビット値のビット63(最上位ビット)を検査します:

  • ビットが 1 の場合、値は ネームスペースのモザイクへのリンク チュートリアルで説明されているように、モザイクエイリアスとして使用されるネームスペースIDです。
  • ビットが 0 の場合、値は通常のモザイクIDであり、解決は不要です。

アドレス解決ステートメントの照会⚓︎

    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)');

トランザクションメタデータのブロック高を使用して、コードは /statements/resolutions/address GET エンドポイントを照会し、そのブロックのアドレス解決ステートメントを取得します。

各解決ステートメントには以下が含まれます:

  • Height: 解決が発生したブロック。
  • Unresolved: トランザクションで使用された(アドレスとしてエンコードされた)ネームスペースエイリアス。
  • Resolution entries: 承認時点でのエイリアスと実際のアドレスをマッピングする配列。
        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.from_decoded_address_hex_string(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 address =
                    Address.fromDecodedAddressHexString(resolved);
                console.log('\nAddress resolution:');
                console.log('  Unresolved:', statement.unresolved);
                console.log(`  Resolved:   ${address}`);
            }
        }
    }

エンドポイントはブロックのすべてのアドレス解決ステートメントを返します。異なるネームスペースエイリアスが使用された場合、単一のブロックに複数の解決ステートメントが含まれる可能性があるため、コードは unresolved フィールドがトランザクションの recipientAddress と一致しないステートメントをスキップします。

同じブロック内のトランザクション間でエイリアスが別の値に再リンクされた場合、各ステートメントは複数のエントリを持つことができます。連続するトランザクションが同じ値に解決される場合、保存されるエントリは1つだけです。

各解決エントリには以下が含まれます:

  • Source: primaryId (ブロック内のトランザクションの1から始まるインデックス)および secondaryIdアグリゲートトランザクション 内の1から始まるインデックス、またはスタンドアロントランザクションの場合は 0 )を使用して、解決された値がどのトランザクションから適用されるかを示します。
  • Resolved: 指定されたソース以降、エイリアスが指していた実際のアドレス。

分析対象のトランザクションの解決された値を特定するために、コードは primaryId がトランザクションの primaryId 以下である最後のエントリを見つけます。これが、エイリアスが現在何を指しているかに関係なく、また同じブロック内でエイリアスが複数回定義されたとしても、ネットワークがその特定のトランザクションに使用した値です。

モザイク解決ステートメントの照会⚓︎

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

同じトランザクションでは、生のモザイクIDの代わりに symbol.xym をモザイクエイリアスとしても使用しています。コードは同じブロック高で /statements/resolutions/mosaic GET エンドポイントを照会し、モザイク解決ステートメントを取得します。

アドレス解決と同様に、 unresolved フィールドがトランザクションのエイリアス化されたモザイクIDと一致しないステートメントはスキップされます。

レスポンスはアドレス解決と同じ構造に従いますが、 resolved フィールドにはアドレスではなくモザイクIDが含まれます。同じ照合ロジックが適用され、 primaryId がトランザクションの primaryId 以下である最後のエントリが解決されたモザイクIDを与えます。

出力⚓︎

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

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

出力の主なポイント:

  • トランザクションハッシュ(2行目): 分析対象の承認済みトランザクションのハッシュ。
  • ブロック高(4行目): トランザクションが承認されたブロック。解決ステートメントの照会に使用されます。
  • トランザクションインデックス(5行目): ブロック内のトランザクションの0から始まる位置。解決エントリとの照合のために1から始まる primaryId に変換されています。
  • 受信者とエイリアスの確認(6-9行目): 受信者フィールドにはエンコードされたネームスペースエイリアスが含まれており、エイリアスフラグの確認により、それが通常のアドレスではなくネームスペースエイリアスであることが確認されます。モザイクエイリアスの確認により、 E74B99BA41F4AFEE もネームスペースエイリアスであることが確認されます。
  • 解決されたアドレス(15-16行目): アドレス解決により、このトランザクションでネームスペースエイリアスが TCWYXKVYBMO4NBCUF3AXKJMXCGVSYQOS7ZG2TLI に解決されたことが示されます。
  • 解決されたモザイクID(22-23行目): モザイク解決により、このトランザクションで未解決のネームスペースID E74B99BA41F4AFEE がモザイクID 72C0212E67A08BCE に解決されたことが示されます。

    解決ステートメントには数値IDのみが含まれます

    解決ステートメントにはモザイクIDが含まれ、 symbol.xym のような人間が読み取り可能な名前は含まれません。IDから読み取り可能な名前を取得するには、 /namespaces/names POST を使用してください。

結論⚓︎

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

ステップ 関連ドキュメント
承認済みトランザクションの取得 /transactions/confirmed/{transactionId} GET
アドレス解決の照会 /statements/resolutions/address GET
モザイク解決の照会 /statements/resolutions/mosaic GET