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

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

status/{address} WS WebSocket チャネルは、特定の アカウント に関連する トランザクション がネットワークによって拒否されたときに、リアルタイムの通知を送信します。 /transactionStatus/{hash} GET エンドポイントをポーリングする代わりに、 status チャネルはネットワークがトランザクションを拒否するとすぐにエラーの詳細をプッシュします。

このチュートリアルでは、 status チャネルをサブスクライブし、拒否通知を処理する方法を説明します。 リスナーをテストするために、コードはネットワークが拒否する無効なトランザクションを意図的に送信します。

前提条件⚓︎

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

さらに、言語固有の 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.symbol.IdGenerator import generate_mosaic_alias_id
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 status channel
        channel = f'status/{MONITOR_ADDRESS}'
        await websocket.send(json.dumps(
            {'uid': uid, 'subscribe': channel}
        ))
        print('Subscribed to status channel')

        # Build a transfer transaction with a non-existent mosaic
        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,
            'mosaics': [{
                'mosaic_id': generate_mosaic_alias_id('symbol.unknown'),
                'amount': 1
            }],
        })
        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 error via WebSocket
        async for raw_message in websocket:
            msg = json.loads(raw_message)
            tx_hash = msg['data']['hash']
            code = msg['data']['code']
            print(
                f'Transaction {tx_hash[:16]}... '
                f'rejected with code: {code}'
            )

            if tx_hash == transaction_hash:
                break

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


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

Download source

import { PrivateKey } from 'symbol-sdk';
import {
    SymbolFacade,
    NetworkTimestamp,
    models,
    generateMosaicAliasId
} 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 status channel
    const channel = `status/${MONITOR_ADDRESS}`;
    websocket.send(JSON.stringify({ uid, subscribe: channel }));
    console.log('Subscribed to status channel');

    // Build a transfer transaction with a non-existent mosaic
    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,
        mosaics: [{
            mosaicId: generateMosaicAliasId('symbol.unknown'),
            amount: 1n
        }],
    });
    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 rejected = new Promise((resolve) => {
        websocket.addEventListener('message', (event) => {
            const msg = JSON.parse(event.data);
            const txHash = msg.data.hash;
            const code = msg.data.code;
            console.log(
                `Transaction ${txHash.substring(0, 16)}... `
                + `rejected with code: ${code}`);

            if (txHash === transactionHash) {
                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 error via WebSocket
    await rejected;

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

Download source

このスニペットでは、 NODE_URL 環境変数を使用して Symbol API [ノード] (default: ノード) を設定します。 値が指定されない場合は、デフォルト値が使用されます。 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));

status チャネルは、特定のアドレスをスコープとします。 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 リファレンス を参照してください。

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

        # Subscribe to status channel
        channel = f'status/{MONITOR_ADDRESS}'
        await websocket.send(json.dumps(
            {'uid': uid, 'subscribe': channel}
        ))
        print('Subscribed to status channel')
    // Subscribe to status channel
    const channel = `status/${MONITOR_ADDRESS}`;
    websocket.send(JSON.stringify({ uid, subscribe: channel }));
    console.log('Subscribed to status channel');

コードは、監視対象アドレスをスコープとする status/{address} WS チャネルをサブスクライブします。 このチャネルは、そのアドレスが関与するトランザクションがネットワークによって拒否されるたびに通知を行い、エラーコードとトランザクションハッシュを提供します。

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

        # Build a transfer transaction with a non-existent mosaic
        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,
            'mosaics': [{
                'mosaic_id': generate_mosaic_alias_id('symbol.unknown'),
                'amount': 1
            }],
        })
        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 a transfer transaction with a non-existent mosaic
    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,
        mosaics: [{
            mosaicId: generateMosaicAliasId('symbol.unknown'),
            amount: 1n
        }],
    });
    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();

このチュートリアルでは、エイリアス symbol.unknown を持つモザイクを含め、監視対象アドレスに送信される 転送トランザクション を構築します。 このモザイクはネットワーク上に存在しないため、トランザクションは拒否されます。

トランザクションは通常通り構築されます。ネットワーク時間と手数料乗数を取得し、トランザクション記述子を作成して署名します。 ハッシュはローカルで計算されるため、受信する 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 error via WebSocket
        async for raw_message in websocket:
            msg = json.loads(raw_message)
            tx_hash = msg['data']['hash']
            code = msg['data']['code']
            print(
                f'Transaction {tx_hash[:16]}... '
                f'rejected with code: {code}'
            )

            if tx_hash == transaction_hash:
                break
    const rejected = new Promise((resolve) => {
        websocket.addEventListener('message', (event) => {
            const msg = JSON.parse(event.data);
            const txHash = msg.data.hash;
            const code = msg.data.code;
            console.log(
                `Transaction ${txHash.substring(0, 16)}... `
                + `rejected with code: ${code}`);

            if (txHash === transactionHash) {
                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 error via WebSocket
    await rejected;

コードはトランザクションをアナウンスし、受信メッセージをリスニングします。 各メッセージは TransactionStatusDTO スキーマに従い、以下が含まれます。

  • hash: 拒否されたトランザクションのハッシュ。
  • code: トランザクションが拒否された理由を説明するエラーコード。 すべての可能な値については、 TransactionStatusEnum スキーマを参照してください。

受信したハッシュがアナウンスされたトランザクションと一致すると、プログラムはエラーコードを出力して終了します。

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

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

エラーを受信した後、コードは接続を閉じる前にサブスクライブ解除メッセージを送信します。

出力⚓︎

1
2
3
4
5
6
7
Using node https://reference.symboltest.net:3001
Monitoring address: TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
Connected to wss://reference.symboltest.net:3001/ws with uid jI0YhF0bJflDsIO915kmWUlZZew=
Subscribed to status channel
Announced transaction E14B012D3D254EF2...
Transaction E14B012D3D254EF2... rejected with code: Failure_Core_Insufficient_Balance
Unsubscribed from status channel

出力の主なポイント:

  • アドレス (2行目): 監視対象アドレス。
  • 接続 (3行目): WebSocket 接続が確立され、サーバーは一意の uid を返します。
  • サブスクリプション (4行目): status チャネルがサブスクライブされます。
  • アナウンス (5行目): トランザクションがアナウンスされ、そのハッシュが出力されます。
  • エラー (6行目): 送信者が要求されたモザイクを保持していないため、ネットワークは Failure_Core_Insufficient_Balance でトランザクションを拒否します。
  • サブスクライブ解除 (7行目): コードは status チャネルのサブスクライブを解除します。

結論⚓︎

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

ステップ 関連ドキュメント
status チャネルのサブスクライブ status/{address} WS
拒否をトリガーする 転送トランザクション
エラーメッセージの処理 TransactionStatusDTO