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

トランザクションフローのリスニング⚓︎

Symbolは、特定のアカウント に対するトランザクション が承認プロセスを進む際に、リアルタイムの通知を送信するWebSocketチャネルを提供しています。 /transactionStatus/{hash} GET エンドポイントをポーリングする場合と比較して、WebSocketはAPI呼び出しを繰り返すオーバーヘッドなしに、更新が発生した瞬間にプッシュします。

このチュートリアルでは、トランザクションチャネルをサブスクライブし、最小限の転送トランザクションをアナウンスし、WebSocketを使用してその承認を待つ方法を説明します。

メモ

ポーリングベースのアプローチについては、トランザクションステータスの監視 チュートリアルを参照してください。

前提条件⚓︎

開始する前に、以下を確認してください。

さらに、言語固有のWebSocketライブラリをインストールしてください。

websockets ライブラリをインストールします。

pip install websockets

このチュートリアルでは、Node.js 22以降で利用可能なネイティブの WebSocket APIを使用します。 追加のパッケージは必要ありません。

完全なコード⚓︎

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

import asyncio
import json
import os
import urllib.request

from symbolchain.CryptoTypes import PrivateKey
from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.symbol.Network import NetworkTimestamp
from symbolchain.sc import Amount
from websockets import connect

NODE_URL = os.getenv('NODE_URL', 'https://reference.symboltest.net:3001')
WS_URL = NODE_URL.replace('http', 'ws', 1) + '/ws'
print(f'Using node {NODE_URL}')

MONITOR_ADDRESS = os.getenv(
    'MONITOR_ADDRESS',
    'TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I'
)
print(f'Monitoring address: {MONITOR_ADDRESS}')

SIGNER_PRIVATE_KEY = os.getenv(
    'SIGNER_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000000'
)
facade = SymbolFacade('testnet')
signer_key_pair = SymbolFacade.KeyPair(PrivateKey(SIGNER_PRIVATE_KEY))


async def main():
    async with connect(WS_URL) as websocket:
        # Connect to WebSocket
        response = json.loads(await websocket.recv())
        uid = response['uid']
        print(f'Connected to {WS_URL} with uid {uid}')

        # Subscribe to transaction channels
        channels = [
            f'unconfirmedAdded/{MONITOR_ADDRESS}',
            f'unconfirmedRemoved/{MONITOR_ADDRESS}',
            f'confirmedAdded/{MONITOR_ADDRESS}',
        ]
        for channel in channels:
            await websocket.send(json.dumps(
                {'uid': uid, 'subscribe': channel}
            ))
            name = channel.split('/')[0]
            print(f'Subscribed to {name} channel')

        # Build and announce a transfer transaction
        with urllib.request.urlopen(f'{NODE_URL}/node/time') as resp:
            time_json = json.loads(resp.read().decode())
            timestamp = NetworkTimestamp(int(
                time_json['communicationTimestamps']['receiveTimestamp']))

        with urllib.request.urlopen(
            f'{NODE_URL}/network/fees/transaction'
        ) as resp:
            fee_json = json.loads(resp.read().decode())
            fee_mult = max(
                fee_json['medianFeeMultiplier'],
                fee_json['minFeeMultiplier'])

        transaction = facade.transaction_factory.create({
            'type': 'transfer_transaction_v1',
            'signer_public_key': signer_key_pair.public_key,
            'deadline': timestamp.add_hours(2).timestamp,
            'recipient_address': MONITOR_ADDRESS,
        })
        transaction.fee = Amount(fee_mult * transaction.size)

        signature = facade.sign_transaction(signer_key_pair, transaction)
        json_payload = facade.transaction_factory.attach_signature(
            transaction, signature)
        transaction_hash = str(facade.hash_transaction(transaction))

        announce_request = urllib.request.Request(
            f'{NODE_URL}/transactions',
            data=json_payload.encode(),
            headers={'Content-Type': 'application/json'},
            method='PUT'
        )
        with urllib.request.urlopen(announce_request) as resp:
            resp.read()
        print(f'Announced transaction {transaction_hash[:16]}...')

        # Wait for confirmation via WebSocket
        async for raw_message in websocket:
            message = json.loads(raw_message)
            topic = message['topic']
            message_hash = message['data']['meta']['hash']
            name = topic.split('/')[0]
            print(f'{name}: hash={message_hash[:16]}...')

            if (name == 'confirmedAdded'
                    and message_hash == transaction_hash):
                print(f'Transaction {transaction_hash[:16]}... confirmed')
                break

        # Unsubscribe before closing
        for channel in channels:
            await websocket.send(json.dumps({
                'uid': uid,
                'unsubscribe': channel
            }))
        print('Unsubscribed from all channels')


try:
    asyncio.run(main())
except Exception as error:
    print(error)

Download source

import { PrivateKey } from 'symbol-sdk';
import { SymbolFacade, NetworkTimestamp, models } from 'symbol-sdk/symbol';

const NODE_URL = process.env.NODE_URL
    || 'https://reference.symboltest.net:3001';
const WS_URL = NODE_URL.replace('http', 'ws') + '/ws';
console.log(`Using node ${NODE_URL}`);

const MONITOR_ADDRESS = process.env.MONITOR_ADDRESS
    || 'TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I';
console.log(`Monitoring address: ${MONITOR_ADDRESS}`);

const SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY
    || '0000000000000000000000000000000000000000000000000000000000000000';
const facade = new SymbolFacade('testnet');
const signerKeyPair = new SymbolFacade.KeyPair(
    new PrivateKey(SIGNER_PRIVATE_KEY));

try {
    // Connect to WebSocket
    const websocket = new WebSocket(WS_URL);
    const uid = await new Promise((resolve) => {
        websocket.addEventListener('message', (event) => {
            const message = JSON.parse(event.data);
            resolve(message.uid);
        }, { once: true });
    });
    console.log(`Connected to ${WS_URL} with uid ${uid}`);

    // Subscribe to transaction channels
    const channels = [
        `unconfirmedAdded/${MONITOR_ADDRESS}`,
        `unconfirmedRemoved/${MONITOR_ADDRESS}`,
        `confirmedAdded/${MONITOR_ADDRESS}`,
    ];
    for (const channel of channels) {
        websocket.send(JSON.stringify({ uid, subscribe: channel }));
        const name = channel.split('/')[0];
        console.log(`Subscribed to ${name} channel`);
    }

    // Build and announce a transfer transaction
    const timeResponse = await fetch(`${NODE_URL}/node/time`);
    const timeJSON = await timeResponse.json();
    const timestamp = new NetworkTimestamp(
        timeJSON.communicationTimestamps.receiveTimestamp);

    const feeResponse = await fetch(
        `${NODE_URL}/network/fees/transaction`);
    const feeJSON = await feeResponse.json();
    const feeMult = Math.max(
        feeJSON.medianFeeMultiplier, feeJSON.minFeeMultiplier);

    const transaction = facade.transactionFactory.create({
        type: 'transfer_transaction_v1',
        signerPublicKey: signerKeyPair.publicKey.toString(),
        deadline: timestamp.addHours(2).timestamp,
        recipientAddress: MONITOR_ADDRESS,
    });
    transaction.fee = new models.Amount(feeMult * transaction.size);

    const signature = facade.signTransaction(signerKeyPair, transaction);
    const jsonPayload = facade.transactionFactory.static.attachSignature(
        transaction, signature);
    const transactionHash = facade.hashTransaction(transaction).toString();

    const confirmed = new Promise((resolve) => {
        websocket.addEventListener('message', (event) => {
            const message = JSON.parse(event.data);
            const topic = message.topic;
            const messageHash = message.data.meta.hash;
            const name = topic.split('/')[0];
            console.log(
                `${name}: hash=${messageHash.substring(0, 16)}...`);

            if (name === 'confirmedAdded'
                && messageHash === transactionHash) {
                    console.log(
                        `Transaction ${transactionHash.substring(0, 16)}`
                        + '... confirmed');
                    resolve();
            }
        });
    });
    await fetch(`${NODE_URL}/transactions`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: jsonPayload
    });
    console.log(
        'Announced transaction '
        + `${transactionHash.substring(0, 16)}...`);

    // Wait for confirmation via WebSocket
    await confirmed;

    // Unsubscribe before closing
    for (const channel of channels) {
        websocket.send(JSON.stringify({ uid, unsubscribe: channel }));
    }
    console.log('Unsubscribed from all channels');
    websocket.close();
} catch (error) {
    console.error(error);
}

Download source

このスニペットでは、 NODE_URL 環境変数を使用してSymbol APIノード を設定します。 値が指定されない場合は、デフォルト値が使用されます。 WebSocket URLは、HTTPプロトコルをWebSocketプロトコルに置き換え、 /ws を追加することで NODE_URL から派生します。

コード解説⚓︎

監視対象アドレスと署名者の設定⚓︎

MONITOR_ADDRESS = os.getenv(
    'MONITOR_ADDRESS',
    'TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I'
)
print(f'Monitoring address: {MONITOR_ADDRESS}')

SIGNER_PRIVATE_KEY = os.getenv(
    'SIGNER_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000000'
)
facade = SymbolFacade('testnet')
signer_key_pair = SymbolFacade.KeyPair(PrivateKey(SIGNER_PRIVATE_KEY))
const MONITOR_ADDRESS = process.env.MONITOR_ADDRESS
    || 'TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I';
console.log(`Monitoring address: ${MONITOR_ADDRESS}`);

const SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY
    || '0000000000000000000000000000000000000000000000000000000000000000';
const facade = new SymbolFacade('testnet');
const signerKeyPair = new SymbolFacade.KeyPair(
    new PrivateKey(SIGNER_PRIVATE_KEY));

各トランザクションWebSocketチャネルは、特定のアドレスをスコープとします。 MONITOR_ADDRESS 環境変数は、監視するアドレスを設定します。 このチャネルは、送信者、受信者、またはトランザクションの内容から派生したその他の役割(例えば、アグリゲートトランザクション 内の埋め込みトランザクションの署名者など)を問わず、このアドレスがトランザクションに関与するたびに通知を送信します。

通知をトリガーするために、このチュートリアルでは監視対象アドレスに転送トランザクションを送信します。 送信者の秘密鍵は SIGNER_PRIVATE_KEY から読み取られます。

これらの環境変数のいずれかが提供されない場合、チュートリアルは同じアカウントに対応するデフォルト値を提供します。

WebSocketへの接続⚓︎

    async with connect(WS_URL) as websocket:
        # Connect to WebSocket
        response = json.loads(await websocket.recv())
        uid = response['uid']
        print(f'Connected to {WS_URL} with uid {uid}')
    // Connect to WebSocket
    const websocket = new WebSocket(WS_URL);
    const uid = await new Promise((resolve) => {
        websocket.addEventListener('message', (event) => {
            const message = JSON.parse(event.data);
            resolve(message.uid);
        }, { once: true });
    });
    console.log(`Connected to ${WS_URL} with uid ${uid}`);

コードは、ノードの /ws エンドポイントへのWebSocket接続を開きます。 接続すると、サーバーは以降のすべてのサブスクリプションリクエストに含める必要がある一意の識別子( uid )を含むメッセージを送信します。

接続プロトコルの詳細については、WebSocket リファレンス を参照してください。

チャネルのサブスクライブ⚓︎

        # Subscribe to transaction channels
        channels = [
            f'unconfirmedAdded/{MONITOR_ADDRESS}',
            f'unconfirmedRemoved/{MONITOR_ADDRESS}',
            f'confirmedAdded/{MONITOR_ADDRESS}',
        ]
        for channel in channels:
            await websocket.send(json.dumps(
                {'uid': uid, 'subscribe': channel}
            ))
            name = channel.split('/')[0]
            print(f'Subscribed to {name} channel')
    // Subscribe to transaction channels
    const channels = [
        `unconfirmedAdded/${MONITOR_ADDRESS}`,
        `unconfirmedRemoved/${MONITOR_ADDRESS}`,
        `confirmedAdded/${MONITOR_ADDRESS}`,
    ];
    for (const channel of channels) {
        websocket.send(JSON.stringify({ uid, subscribe: channel }));
        const name = channel.split('/')[0];
        console.log(`Subscribed to ${name} channel`);
    }

コードは、監視対象アドレスを各チャネル名に追加して、アドレスをスコープとする3つのチャネルをサブスクライブします。

各サブスクリプションメッセージには、接続ステップで受信した uid と、監視対象アドレスを含む完全なチャネル名が含まれます。

転送トランザクションの構築と署名⚓︎

        # Build and announce a transfer transaction
        with urllib.request.urlopen(f'{NODE_URL}/node/time') as resp:
            time_json = json.loads(resp.read().decode())
            timestamp = NetworkTimestamp(int(
                time_json['communicationTimestamps']['receiveTimestamp']))

        with urllib.request.urlopen(
            f'{NODE_URL}/network/fees/transaction'
        ) as resp:
            fee_json = json.loads(resp.read().decode())
            fee_mult = max(
                fee_json['medianFeeMultiplier'],
                fee_json['minFeeMultiplier'])

        transaction = facade.transaction_factory.create({
            'type': 'transfer_transaction_v1',
            'signer_public_key': signer_key_pair.public_key,
            'deadline': timestamp.add_hours(2).timestamp,
            'recipient_address': MONITOR_ADDRESS,
        })
        transaction.fee = Amount(fee_mult * transaction.size)

        signature = facade.sign_transaction(signer_key_pair, transaction)
        json_payload = facade.transaction_factory.attach_signature(
            transaction, signature)
        transaction_hash = str(facade.hash_transaction(transaction))
    // Build and announce a transfer transaction
    const timeResponse = await fetch(`${NODE_URL}/node/time`);
    const timeJSON = await timeResponse.json();
    const timestamp = new NetworkTimestamp(
        timeJSON.communicationTimestamps.receiveTimestamp);

    const feeResponse = await fetch(
        `${NODE_URL}/network/fees/transaction`);
    const feeJSON = await feeResponse.json();
    const feeMult = Math.max(
        feeJSON.medianFeeMultiplier, feeJSON.minFeeMultiplier);

    const transaction = facade.transactionFactory.create({
        type: 'transfer_transaction_v1',
        signerPublicKey: signerKeyPair.publicKey.toString(),
        deadline: timestamp.addHours(2).timestamp,
        recipientAddress: MONITOR_ADDRESS,
    });
    transaction.fee = new models.Amount(feeMult * transaction.size);

    const signature = facade.signTransaction(signerKeyPair, transaction);
    const jsonPayload = facade.transactionFactory.static.attachSignature(
        transaction, signature);
    const transactionHash = facade.hashTransaction(transaction).toString();

このチュートリアルでは、モザイクもメッセージも含まない、監視対象アドレスへの最小限の転送トランザクション を構築します。 簡略化のために転送が使用されていますが、どのトランザクションタイプでも同じWebSocket通知がトリガーされます。

トランザクションは通常通り構築されます。ネットワーク時間と手数料乗数を取得し、トランザクション記述子を作成して署名 します。 ハッシュ はローカルで計算されるため、後で受信するWebSocketメッセージと照合することができます。

アナウンスと承認の待機⚓︎

        announce_request = urllib.request.Request(
            f'{NODE_URL}/transactions',
            data=json_payload.encode(),
            headers={'Content-Type': 'application/json'},
            method='PUT'
        )
        with urllib.request.urlopen(announce_request) as resp:
            resp.read()
        print(f'Announced transaction {transaction_hash[:16]}...')

        # Wait for confirmation via WebSocket
        async for raw_message in websocket:
            message = json.loads(raw_message)
            topic = message['topic']
            message_hash = message['data']['meta']['hash']
            name = topic.split('/')[0]
            print(f'{name}: hash={message_hash[:16]}...')

            if (name == 'confirmedAdded'
                    and message_hash == transaction_hash):
                print(f'Transaction {transaction_hash[:16]}... confirmed')
                break
    const confirmed = new Promise((resolve) => {
        websocket.addEventListener('message', (event) => {
            const message = JSON.parse(event.data);
            const topic = message.topic;
            const messageHash = message.data.meta.hash;
            const name = topic.split('/')[0];
            console.log(
                `${name}: hash=${messageHash.substring(0, 16)}...`);

            if (name === 'confirmedAdded'
                && messageHash === transactionHash) {
                    console.log(
                        `Transaction ${transactionHash.substring(0, 16)}`
                        + '... confirmed');
                    resolve();
            }
        });
    });
    await fetch(`${NODE_URL}/transactions`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: jsonPayload
    });
    console.log(
        'Announced transaction '
        + `${transactionHash.substring(0, 16)}...`);

    // Wait for confirmation via WebSocket
    await confirmed;

コードはトランザクションをアナウンスし、受信メッセージをリスニングして各メッセージを表示します。

注意: チャネルのサブスクライブ後にアナウンスする

リスナーの準備ができていることを確認するために、必ずWebSocketチャネルをサブスクライブした にトランザクションをアナウンスしてください。 そうしないと、WebSocketがリスニング状態になる前に通知が到着する可能性があります。

各メッセージには、チャネルを識別する topic フィールドと、イベントペイロードを含む data オブジェクトが含まれます。

confirmedAdded および unconfirmedAdded メッセージの場合、ペイロードは TransactionInfoDTO スキーマに従います。 unconfirmedRemoved メッセージの場合、ペイロードにはトランザクションハッシュ( meta.hash )のみが含まれます。

ハッシュがアナウンスされたトランザクションと一致する confirmedAdded メッセージが到着すると、プログラムは承認メッセージを出力して終了します。

成功したトランザクションの期待されるシーケンスは、テキストブックの トランザクションのライフサイクル セクションで説明されています。

  1. unconfirmedAdded: トランザクションが未承認プールに入ります。
  2. unconfirmedRemoved: トランザクションが未承認プールを抜けます。
  3. confirmedAdded: トランザクションがブロック内で承認されます。

チャネルのサブスクライブ解除⚓︎

        # Unsubscribe before closing
        for channel in channels:
            await websocket.send(json.dumps({
                'uid': uid,
                'unsubscribe': channel
            }))
        print('Unsubscribed from all channels')
    // Unsubscribe before closing
    for (const channel of channels) {
        websocket.send(JSON.stringify({ uid, unsubscribe: channel }));
    }
    console.log('Unsubscribed from all channels');
    websocket.close();

承認後、コードは接続を閉じる前に3つすべてのチャネルのサブスクライブ解除メッセージを送信します。

出力⚓︎

Using node https://reference.symboltest.net:3001
Monitoring address: TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
Connected to wss://reference.symboltest.net:3001/ws with uid Hj3kL9mN2pQr5tVw=
Subscribed to unconfirmedAdded channel
Subscribed to unconfirmedRemoved channel
Subscribed to confirmedAdded channel
Announced transaction 7A3F1B9E4C2D8A65...
unconfirmedAdded: hash=7A3F1B9E4C2D8A65...
unconfirmedRemoved: hash=7A3F1B9E4C2D8A65...
confirmedAdded: hash=7A3F1B9E4C2D8A65...
Transaction 7A3F1B9E4C2D8A65... confirmed
Unsubscribed from all channels

出力の主なポイント:

  • アドレス (2行目): 監視対象アドレス。
  • 接続 (3行目): WebSocket 接続が確立され、サーバーは一意の uid を返します。
  • サブスクリプション (4-6行目): 3つすべてのトランザクションチャネルがサブスクライブされます。
  • アナウンス (7行目): トランザクションがアナウンスされ、そのハッシュが出力されます。
  • トランザクションフロー (8-10行目): トランザクションは unconfirmedAdded から unconfirmedRemoved 、そして confirmedAdded へと移行し、承認のライフサイクル全体を示しています。
  • 承認 (11行目): confirmedAdded からのハッシュがアナウンスされたトランザクションと一致し、成功が確認されます。
  • サブスクライブ解除 (12行目): コードはすべてのチャネルのサブスクライブを解除します。

結論⚓︎

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

ステップ 関連ドキュメント
unconfirmedAdded のサブスクライブ unconfirmedAdded/{address} WS
unconfirmedRemoved のサブスクライブ unconfirmedRemoved/{address} WS
confirmedAdded のサブスクライブ confirmedAdded/{address} WS
トランザクションメッセージの処理 TransactionInfoDTO

次のステップ⚓︎

拒否されたトランザクションとそのエラーコードを検出するには、トランザクションエラーのリスニング チュートリアルを参照してください。