コンテンツにスキップ

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

初級

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

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

代替手段: ポーリング

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

前提条件⚓︎

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

さらに、言語固有の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.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 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}/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)
            },
            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 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,
    descriptors
} 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 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 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),
            undefined,
            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 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 ('confirmedAdded' === name &&
                messageHash === transactionHash) {
                console.log(
                    `Transaction ${transactionHash.substring(0, 16)}` +
                        '... confirmed');
                resolve();
            }
        });
    });
    await announceTransaction(
        jsonPayload, '/transactions',
        `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

//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.KeyPair;
import org.symbol.sdk.symbol.SymbolTransactionFactory;
import org.symbol.sdk.symbol.descriptors.*;
import org.symbol.sdk.symbol.models.*;

@ClientEndpoint
public final class ListenTransactionFlow {
    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> confirmed =
        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 ListenTransactionFlow().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 transaction channels
        final List<String> channels = List.of(
            "unconfirmedAdded/" + monitorAddress,
            "unconfirmedRemoved/" + monitorAddress,
            "confirmedAdded/" + monitorAddress);
        for (final String channel : channels) {
            remote.sendText(JSON_MAPPER.createObjectNode()
                .put("uid", uid).put("subscribe", channel)
                .toString());
            System.out.printf(
                "Subscribed to %s channel%n", channel.split("/")[0]);
        }

        // Build and announce a transfer transaction
        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),
                    null,
                    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 confirmation via WebSocket
        confirmed.join();
        // Unsubscribe before closing
        for (final String channel : channels)
            remote.sendText(JSON_MAPPER.createObjectNode()
                .put("uid", uid).put("unsubscribe", channel)
                .toString());
        System.out.println("Unsubscribed from all channels");
        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 topic = message.get("topic").asText();
        final String messageHash = message.get("data")
            .get("meta").get("hash").asText();
        final String name = topic.split("/")[0];
        System.out.printf("%s: hash=%s...%n",
            name, messageHash.substring(0, 16));

        if ("confirmedAdded".equals(name)
            && messageHash.equals(transactionHash)) {
            System.out.printf("Transaction %s... confirmed%n",
                transactionHash.substring(0, 16));
            confirmed.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));

各トランザクション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}`);
        // 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 )を含むメッセージを送信します。

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

        # 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`);
    }
        // Subscribe to transaction channels
        final List<String> channels = List.of(
            "unconfirmedAdded/" + monitorAddress,
            "unconfirmedRemoved/" + monitorAddress,
            "confirmedAdded/" + monitorAddress);
        for (final String channel : channels) {
            remote.sendText(JSON_MAPPER.createObjectNode()
                .put("uid", uid).put("subscribe", channel)
                .toString());
            System.out.printf(
                "Subscribed to %s channel%n", channel.split("/")[0]);
        }

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

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

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

        # Build and announce a transfer transaction
        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)
            },
            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 and announce a transfer transaction
    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),
            undefined,
            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 and announce a transfer transaction
        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),
                    null,
                    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();

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

トランザクションは通常通り構築されます。推奨手数料を取得し、トランザクションの記述子からトランザクションを作成して署名 します。 は手数料乗数を使用してトランザクション手数料を計算し、期限の長さを受け取ります。

トランザクションに署名すると、それを一意に識別するハッシュ が生成されます。 トランザクションチャネルの通知にはトランザクションハッシュが含まれるため、コードはこのハッシュを保存します。 後で定義するメッセージハンドラは、受信した各ハッシュを保存した値と比較して、このトランザクションに関する通知を識別します。

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

        announce_transaction(
            json_payload, '/transactions',
            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 ('confirmedAdded' === name &&
                messageHash === transactionHash) {
                console.log(
                    `Transaction ${transactionHash.substring(0, 16)}` +
                        '... confirmed');
                resolve();
            }
        });
    });
    await announceTransaction(
        jsonPayload, '/transactions',
        `Announced transaction ${transactionHash.substring(0, 16)}...`);

    // Wait for confirmation via WebSocket
    await confirmed;
    // 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 topic = message.get("topic").asText();
        final String messageHash = message.get("data")
            .get("meta").get("hash").asText();
        final String name = topic.split("/")[0];
        System.out.printf("%s: hash=%s...%n",
            name, messageHash.substring(0, 16));

        if ("confirmedAdded".equals(name)
            && messageHash.equals(transactionHash)) {
            System.out.printf("Transaction %s... confirmed%n",
                transactionHash.substring(0, 16));
            confirmed.complete(null);
        }
    }

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

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

リスナーの準備ができていることを確認するために、必ず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();
        // Unsubscribe before closing
        for (final String channel : channels)
            remote.sendText(JSON_MAPPER.createObjectNode()
                .put("uid", uid).put("unsubscribe", channel)
                .toString());
        System.out.println("Unsubscribed from all channels");
        session.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

次のステップ⚓︎

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