コンテンツにスキップ

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

初級

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

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

前提条件⚓︎

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

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

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

pip install websockets

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

このチュートリアルでは Tyrus WebSocket クライアントを使用します。これはスニペット内の JBang 依存関係の行によって読み込まれます。

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

完全なコード⚓︎

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

import asyncio
import json
import os
import urllib.request

from symbolchain.CryptoTypes import PrivateKey
from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.symbol.IdGenerator import generate_mosaic_alias_id
from symbolchain.symbol.Network import Address
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}')


def announce_transaction(payload, endpoint, label):
    announce_request = urllib.request.Request(
        f'{NODE_URL}{endpoint}',
        data=payload.encode(),
        headers={'Content-Type': 'application/json'},
        method='PUT'
    )
    with urllib.request.urlopen(announce_request) as response:
        response.read()
    print(label)


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}/network/fees/transaction'
        ) as resp:
            fee_json = json.loads(resp.read().decode())
            fee_multiplier = max(
                fee_json['medianFeeMultiplier'],
                fee_json['minFeeMultiplier'])

        transaction = facade.create_transaction_from_descriptor(
            {
                'type': 'transfer_transaction_v1',
                'recipient_address': Address(MONITOR_ADDRESS),
                'mosaics': [{
                    'mosaic_id': generate_mosaic_alias_id(
                        'symbol.unknown'),
                    'amount': 1
                }]
            },
            signer_key_pair.public_key,
            fee_multiplier,
            2 * 60 * 60)

        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_transaction(
            json_payload, '/transactions',
            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,
    descriptors,
    generateMosaicAliasId,
    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}`);

async function announceTransaction(payload, endpoint, label) {
    await fetch(`${NODE_URL}${endpoint}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: payload
    });
    console.log(label);
}


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 feeResponse = await fetch(
        `${NODE_URL}/network/fees/transaction`);
    const feeJSON = await feeResponse.json();
    const feeMultiplier = Math.max(
        feeJSON.medianFeeMultiplier, feeJSON.minFeeMultiplier);

    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.TransferTransactionV1Descriptor(
            new SymbolFacade.Address(MONITOR_ADDRESS),
            [
                new descriptors.UnresolvedMosaicDescriptor(
                    generateMosaicAliasId('symbol.unknown'),
                    new models.Amount(1n))
            ],
            undefined),
        signerKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60);

    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 announceTransaction(
        jsonPayload, '/transactions',
        `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

//JAVA 21+
//DEPS org.symbol:symbol-sdk:3.3.1
//DEPS org.glassfish.tyrus.bundles:tyrus-standalone-client:2.2.0

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.List;
import java.util.concurrent.CompletableFuture;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.websocket.ClientEndpoint;
import jakarta.websocket.ContainerProvider;
import jakarta.websocket.OnMessage;
import jakarta.websocket.RemoteEndpoint;
import jakarta.websocket.Session;
import jakarta.websocket.WebSocketContainer;

import org.symbol.sdk.CryptoTypes;
import org.symbol.sdk.facade.SymbolFacade;
import org.symbol.sdk.symbol.Address;
import org.symbol.sdk.symbol.IdGenerator;
import org.symbol.sdk.symbol.KeyPair;
import org.symbol.sdk.symbol.SymbolTransactionFactory;
import org.symbol.sdk.symbol.descriptors.*;
import org.symbol.sdk.symbol.models.*;

@ClientEndpoint
public final class ListenTransactionError {
    private static final ObjectMapper JSON_MAPPER = new ObjectMapper();

    private static final HttpClient HTTP_CLIENT =
        HttpClient.newHttpClient();

    private final CompletableFuture<String> uidFuture =
        new CompletableFuture<>();

    private final CompletableFuture<Void> rejected =
        new CompletableFuture<>();

    private final String nodeUrl = System.getenv().getOrDefault(
        "NODE_URL", "https://reference.symboltest.net:3001");

    private final String wsUrl = nodeUrl.replaceFirst("http", "ws")
        + "/ws";

    private final SymbolFacade facade = new SymbolFacade("testnet");

    private String transactionHash;

    public static void main(final String[] args) {
        try {
            new ListenTransactionError().run();
        } catch (final Exception ex) {
            System.out.println(null == ex.getMessage()
                ? ex.toString()
                : ex.getMessage());
        }
    }

    private void run() throws Exception {
        System.out.printf("Using node %s%n", nodeUrl);

        final String monitorAddress = System.getenv().getOrDefault(
            "MONITOR_ADDRESS",
            "TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I");
        System.out.printf("Monitoring address: %s%n", monitorAddress);

        final String signerPrivateKey = System.getenv().getOrDefault(
            "SIGNER_PRIVATE_KEY", "0".repeat(64));
        final KeyPair signerKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(signerPrivateKey));

        // Connect to WebSocket
        final WebSocketContainer container =
            ContainerProvider.getWebSocketContainer();
        final Session session = container.connectToServer(
            this, URI.create(wsUrl));
        final RemoteEndpoint.Basic remote = session.getBasicRemote();
        final String uid = uidFuture.join();
        System.out.printf("Connected to %s with uid %s%n", wsUrl, uid);

        // Subscribe to status channel
        final String channel = "status/" + monitorAddress;
        remote.sendText(JSON_MAPPER.createObjectNode()
            .put("uid", uid).put("subscribe", channel)
            .toString());
        System.out.println("Subscribed to status channel");

        // Build a transfer transaction with a non-existent mosaic
        final JsonNode feeJSON = getJson("/network/fees/transaction");
        final long feeMultiplier = Math.max(
            feeJSON.get("medianFeeMultiplier").asLong(),
            feeJSON.get("minFeeMultiplier").asLong());

        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new TransferTransactionV1Descriptor(
                    new Address(monitorAddress),
                    List.of(new UnresolvedMosaicDescriptor(
                        new UnresolvedMosaicId(
                            IdGenerator.generateMosaicAliasId(
                                "symbol.unknown")),
                        new Amount(1))),
                    null),
                signerKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60);

        final CryptoTypes.Signature signature = facade.signTransaction(
            signerKeyPair, transaction);
        final String jsonPayload = SymbolTransactionFactory
            .attachSignature(transaction, signature);
        transactionHash = facade.hashTransaction(transaction).toString();

        announceTransaction(
            jsonPayload, "/transactions",
            "Announced transaction "
            + transactionHash.substring(0, 16) + "...");

        // Wait for error via WebSocket
        rejected.join();
        // Unsubscribe before closing
        remote.sendText(JSON_MAPPER.createObjectNode()
            .put("uid", uid).put("unsubscribe", channel)
            .toString());
        System.out.println("Unsubscribed from status channel");
        session.close();
    }

    private JsonNode getJson(final String path)
        throws IOException, InterruptedException {
        final HttpRequest request = HttpRequest.newBuilder(
            URI.create(nodeUrl + path)).GET().build();
        final HttpResponse<String> response = HTTP_CLIENT.send(
            request, BodyHandlers.ofString());
        return JSON_MAPPER.readTree(response.body());
    }

    private void announceTransaction(
        final String payload,
        final String endpoint,
        final String label
    )
        throws IOException, InterruptedException {
        final HttpRequest request = HttpRequest.newBuilder(
            URI.create(nodeUrl + endpoint))
            .header("Content-Type", "application/json")
            .PUT(HttpRequest.BodyPublishers.ofString(payload))
            .build();
        HTTP_CLIENT.send(request, BodyHandlers.ofString());
        System.out.println(label);
    }

    // Handle incoming messages
    @OnMessage
    public void onMessage(final String payload) throws IOException {
        final JsonNode message = JSON_MAPPER.readTree(payload);

        // Special case for the initial handshake message
        if (message.has("uid")) {
            uidFuture.complete(message.get("uid").asText());
            return;
        }

        final String txHash = message.get("data").get("hash").asText();
        final String code = message.get("data").get("code").asText();
        System.out.printf(
            "Transaction %s... rejected with code: %s%n",
            txHash.substring(0, 16), code);

        if (txHash.equals(transactionHash))
            rejected.complete(null);
    }

}

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));
        final String monitorAddress = System.getenv().getOrDefault(
            "MONITOR_ADDRESS",
            "TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I");
        System.out.printf("Monitoring address: %s%n", monitorAddress);

        final String signerPrivateKey = System.getenv().getOrDefault(
            "SIGNER_PRIVATE_KEY", "0".repeat(64));
        final KeyPair signerKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(signerPrivateKey));

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}`);
        // Connect to WebSocket
        final WebSocketContainer container =
            ContainerProvider.getWebSocketContainer();
        final Session session = container.connectToServer(
            this, URI.create(wsUrl));
        final RemoteEndpoint.Basic remote = session.getBasicRemote();
        final String uid = uidFuture.join();
        System.out.printf("Connected to %s with uid %s%n", wsUrl, uid);

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

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');
        // Subscribe to status channel
        final String channel = "status/" + monitorAddress;
        remote.sendText(JSON_MAPPER.createObjectNode()
            .put("uid", uid).put("subscribe", channel)
            .toString());
        System.out.println("Subscribed to status channel");

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

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

        # Build a transfer transaction with a non-existent mosaic
        with urllib.request.urlopen(
            f'{NODE_URL}/network/fees/transaction'
        ) as resp:
            fee_json = json.loads(resp.read().decode())
            fee_multiplier = max(
                fee_json['medianFeeMultiplier'],
                fee_json['minFeeMultiplier'])

        transaction = facade.create_transaction_from_descriptor(
            {
                'type': 'transfer_transaction_v1',
                'recipient_address': Address(MONITOR_ADDRESS),
                'mosaics': [{
                    'mosaic_id': generate_mosaic_alias_id(
                        'symbol.unknown'),
                    'amount': 1
                }]
            },
            signer_key_pair.public_key,
            fee_multiplier,
            2 * 60 * 60)

        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 feeResponse = await fetch(
        `${NODE_URL}/network/fees/transaction`);
    const feeJSON = await feeResponse.json();
    const feeMultiplier = Math.max(
        feeJSON.medianFeeMultiplier, feeJSON.minFeeMultiplier);

    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.TransferTransactionV1Descriptor(
            new SymbolFacade.Address(MONITOR_ADDRESS),
            [
                new descriptors.UnresolvedMosaicDescriptor(
                    generateMosaicAliasId('symbol.unknown'),
                    new models.Amount(1n))
            ],
            undefined),
        signerKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60);

    const signature = facade.signTransaction(signerKeyPair, transaction);
    const jsonPayload = facade.transactionFactory.static.attachSignature(
        transaction, signature);
    const transactionHash =
        facade.hashTransaction(transaction).toString();
        // Build a transfer transaction with a non-existent mosaic
        final JsonNode feeJSON = getJson("/network/fees/transaction");
        final long feeMultiplier = Math.max(
            feeJSON.get("medianFeeMultiplier").asLong(),
            feeJSON.get("minFeeMultiplier").asLong());

        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new TransferTransactionV1Descriptor(
                    new Address(monitorAddress),
                    List.of(new UnresolvedMosaicDescriptor(
                        new UnresolvedMosaicId(
                            IdGenerator.generateMosaicAliasId(
                                "symbol.unknown")),
                        new Amount(1))),
                    null),
                signerKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60);

        final CryptoTypes.Signature signature = facade.signTransaction(
            signerKeyPair, transaction);
        final String jsonPayload = SymbolTransactionFactory
            .attachSignature(transaction, signature);
        transactionHash = facade.hashTransaction(transaction).toString();

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

トランザクションは通常通り構築されます。推奨手数料を取得し、トランザクションの記述子からトランザクションを作成して署名します。 は手数料乗数を使用してトランザクション手数料を計算し、期限の長さを受け取ります。 ハッシュはローカルで計算されるため、受信する WebSocket エラーメッセージと照合することができます。

アナウンスとエラーの待機⚓︎

        announce_transaction(
            json_payload, '/transactions',
            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 announceTransaction(
        jsonPayload, '/transactions',
        `Announced transaction ${transactionHash.substring(0, 16)}...`);

    // Wait for error via WebSocket
    await rejected;
    // Handle incoming messages
    @OnMessage
    public void onMessage(final String payload) throws IOException {
        final JsonNode message = JSON_MAPPER.readTree(payload);

        // Special case for the initial handshake message
        if (message.has("uid")) {
            uidFuture.complete(message.get("uid").asText());
            return;
        }

        final String txHash = message.get("data").get("hash").asText();
        final String code = message.get("data").get("code").asText();
        System.out.printf(
            "Transaction %s... rejected with code: %s%n",
            txHash.substring(0, 16), code);

        if (txHash.equals(transactionHash))
            rejected.complete(null);
    }

コードはトランザクションをアナウンスし、受信メッセージをリスニングします。 各メッセージは 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();
        // Unsubscribe before closing
        remote.sendText(JSON_MAPPER.createObjectNode()
            .put("uid", uid).put("unsubscribe", channel)
            .toString());
        System.out.println("Unsubscribed from status channel");
        session.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