コンテンツにスキップ

トランザクションフローをリッスンする⚓︎

中級

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

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

別の方法: ポーリング

ポーリングを使う方法については、トランザクションのステータスを監視する チュートリアルを参照してください。

前提条件⚓︎

始める前に、次の準備をしてください。

さらに、NEM は SockJS 上で STOMP メッセージングプロトコルを使って WebSocket を提供するため、STOMP クライアントと WebSocket トランスポートが必要です。

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

pip install stomper websockets

@stomp/stompjssockjs-client ライブラリをインストールします。

npm install @stomp/stompjs sockjs-client

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

完全なコード⚓︎

import asyncio
import json
import os
import random
import urllib.request
import uuid

import stomper
from symbolchain.CryptoTypes import PrivateKey
from symbolchain.facade.NemFacade import NemFacade
from symbolchain.nc import Amount
from symbolchain.nem.FeeCalculator import calculate_transaction_fee
from symbolchain.nem.Network import NetworkTimestamp
from websockets import connect

NODE_URL = os.getenv('NODE_URL', 'http://libertalia.nemtest.net:7890')
WS_URL = NODE_URL.replace(':7890', ':7778')
print(f'Using node {NODE_URL}')


# SockJS has no Python client library.
# These helpers wrap the raw WebSocket transport to mirror a STOMP client.
def sockjs_url(endpoint_url):
    # SockJS raw WebSocket transport adds a random server and session id
    server = random.randint(100, 999)
    session = uuid.uuid4().hex
    ws_base = endpoint_url.replace('http', 'ws', 1)
    return f'{ws_base}/{server}/{session}/websocket'


async def send_frame(websocket, frame):
    # SockJS wraps each client payload as a JSON array of frame strings
    await websocket.send(json.dumps([frame]))


async def stomp_connect(websocket):
    await websocket.recv()  # consume the SockJS open frame
    await send_frame(
        websocket, stomper.connect('', '', NODE_URL, heartbeats=(0, 0)))


async def stomp_subscribe(websocket, destination, sub_id):
    await send_frame(websocket, stomper.subscribe(destination, sub_id))


async def stomp_send(websocket, destination, body):
    await send_frame(websocket, stomper.send(destination, body))


async def stomp_unsubscribe(websocket, sub_id):
    await send_frame(websocket, stomper.unsubscribe(sub_id))


async def stomp_disconnect(websocket):
    await send_frame(websocket, stomper.disconnect())


def stomp_messages(raw_frame):
    # Yield each STOMP MESSAGE frame in a SockJS data frame
    if 'a' != raw_frame[0]:  # skip 'o' open, 'h' heartbeat, 'c' close
        return
    for payload in json.loads(raw_frame[1:]):
        frame = stomper.unpack_frame(payload)
        if 'MESSAGE' == frame['cmd']:
            yield frame


async def stomp_frames(websocket):
    # Yield each STOMP MESSAGE frame as it arrives
    async for raw_frame in websocket:
        for frame in stomp_messages(raw_frame):
            yield frame


# Set up the monitored address and signer
MONITOR_ADDRESS = os.getenv(
    'MONITOR_ADDRESS',
    'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4'
)
print(f'Monitoring address: {MONITOR_ADDRESS}')

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


async def main():
    # Build and sign a transfer to the monitored address
    with urllib.request.urlopen(
        f'{NODE_URL}/time-sync/network-time'
    ) as resp:
        network_time = json.loads(
            resp.read().decode())['receiveTimeStamp'] // 1000
    timestamp = NetworkTimestamp(network_time)
    deadline = timestamp.add_hours(2)
    transaction = facade.transaction_factory.create({
        'type': 'transfer_transaction_v2',
        'signer_public_key': signer_key_pair.public_key,
        'timestamp': timestamp.timestamp,
        'deadline': deadline.timestamp,
        'recipient_address': MONITOR_ADDRESS,
        'amount': 0,
    })
    transaction.fee = Amount(calculate_transaction_fee(transaction))
    signature = facade.sign_transaction(signer_key_pair, transaction)
    json_payload = facade.transaction_factory.attach_signature(
        transaction, signature)
    transaction_hash = str(
        facade.hash_transaction(transaction)).upper()

    # Connect to the WebSocket
    endpoint = f'{WS_URL}/w/messages'
    async with connect(sockjs_url(endpoint)) as websocket:
        await stomp_connect(websocket)
        print(f'Connected to {WS_URL}')
        frames = stomp_frames(websocket)

        # Subscribe to the account and transaction channels
        account_channel = f'/account/{MONITOR_ADDRESS}'
        channels = {
            account_channel: 'id-0',
            f'/unconfirmed/{MONITOR_ADDRESS}': 'id-1',
            f'/transactions/{MONITOR_ADDRESS}': 'id-2',
        }
        for channel, sub_id in channels.items():
            await stomp_subscribe(websocket, channel, sub_id)
            print(f'Subscribed to {channel} channel')

        # Register the account and confirm it is active
        await stomp_send(websocket, '/w/api/account/get',
            json.dumps({'account': MONITOR_ADDRESS}))
        async for frame in frames:
            if account_channel == frame['headers']['destination']:
                balance = json.loads(
                    frame['body'])['account']['balance']
                print(f'Account update: balance={balance}')
                break
        print('Account registered')

        # Announce the transaction
        print(f'Announcing transaction {transaction_hash[:16]}...')
        announce_request = urllib.request.Request(
            f'{NODE_URL}/transaction/announce',
            data=json_payload.encode(),
            headers={'Content-Type': 'application/json'},
            method='POST'
        )
        with urllib.request.urlopen(announce_request) as resp:
            result = json.loads(resp.read().decode())

        # Wait for the transaction to confirm
        if 'SUCCESS' == result['message']:
            confirmed = False
            async for frame in frames:
                destination = frame['headers']['destination']
                body = json.loads(frame['body'])
                if account_channel == destination:
                    balance = body['account']['balance']
                    print(f'Account update: balance={balance}')
                    if confirmed:
                        break
                elif '/transactions/' in destination:
                    message_hash = body['meta']['hash']['data']
                    print(f'confirmed: hash={message_hash[:16]}...')
                    if message_hash.upper() == transaction_hash:
                        short_hash = transaction_hash[:16]
                        print(f'Transaction {short_hash}... confirmed')
                        confirmed = True
                else:
                    message_hash = body['meta']['hash']['data']
                    if message_hash.upper() == transaction_hash:
                        print(f'unconfirmed: hash={message_hash[:16]}...')
        else:
            print(f'Transaction rejected: {result["message"]}')

        # Unsubscribe before closing
        for sub_id in channels.values():
            await stomp_unsubscribe(websocket, sub_id)
        print('Unsubscribed from all channels')
        await stomp_disconnect(websocket)


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

Download source

import { Client } from '@stomp/stompjs';
import SockJS from 'sockjs-client';
import { PrivateKey } from 'symbol-sdk';
import {
    NemFacade, NetworkTimestamp, calculateTransactionFee, models
} from 'symbol-sdk/nem';

const NODE_URL = process.env.NODE_URL ||
    'http://libertalia.nemtest.net:7890';
const WS_URL = NODE_URL.replace(':7890', ':7778');
console.log(`Using node ${NODE_URL}`);
// Set up the monitored address and signer
const MONITOR_ADDRESS = process.env.MONITOR_ADDRESS ||
    'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4';
console.log(`Monitoring address: ${MONITOR_ADDRESS}`);

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

try {
    // Build and sign a transfer to the monitored address
    const timeResponse = await fetch(
        `${NODE_URL}/time-sync/network-time`);
    const networkTime = Math.floor(
        (await timeResponse.json()).receiveTimeStamp / 1000);
    const timestamp = new NetworkTimestamp(networkTime);
    const deadline = timestamp.addHours(2);
    const transaction = facade.transactionFactory.create({
        type: 'transfer_transaction_v2',
        signerPublicKey: signerKeyPair.publicKey.toString(),
        timestamp: timestamp.timestamp,
        deadline: deadline.timestamp,
        recipientAddress: MONITOR_ADDRESS,
        amount: 0n
    });
    transaction.fee = new models.Amount(
        calculateTransactionFee(transaction));
    const signature = facade.signTransaction(signerKeyPair, transaction);
    const jsonPayload = facade.transactionFactory.static.attachSignature(
        transaction, signature);
    const transactionHash =
        facade.hashTransaction(transaction).toString().toUpperCase();
    const shortHash = transactionHash.substring(0, 16);

    // Connect to the WebSocket
    const client = new Client({
        webSocketFactory: () => new SockJS(`${WS_URL}/w/messages`)
    });
    await new Promise(resolve => {
        client.onConnect = resolve;
        client.activate();
    });
    console.log(`Connected to ${WS_URL}`);

    // Wait for the transaction to confirm
    let confirmed = false;
    let resolveRegistered;
    let resolveDone;
    const registered = new Promise(resolve => {
        resolveRegistered = resolve;
    });
    const done = new Promise(resolve => {
        resolveDone = resolve;
    });
    const onUnconfirmed = message => {
        const messageHash = JSON.parse(message.body).meta.hash.data;
        if (messageHash.toUpperCase() === transactionHash) {
            console.log(
                `unconfirmed: hash=${messageHash.substring(0, 16)}...`);
        }
    };
    const onConfirmed = message => {
        const messageHash = JSON.parse(message.body).meta.hash.data;
        console.log(`confirmed: hash=${messageHash.substring(0, 16)}...`);
        if (messageHash.toUpperCase() === transactionHash) {
            console.log(`Transaction ${shortHash}... confirmed`);
            confirmed = true;
        }
    };
    const onAccountUpdate = message => {
        const { balance } = JSON.parse(message.body).account;
        console.log(`Account update: balance=${balance}`);
        resolveRegistered();
        if (confirmed)
            resolveDone();
    };

    // Subscribe to the account and transaction channels
    const accountChannel = `/account/${MONITOR_ADDRESS}`;
    const subscriptions = [
        { channel: accountChannel, handler: onAccountUpdate, id: 'id-0' },
        {
            channel: `/unconfirmed/${MONITOR_ADDRESS}`,
            handler: onUnconfirmed,
            id: 'id-1'
        },
        {
            channel: `/transactions/${MONITOR_ADDRESS}`,
            handler: onConfirmed,
            id: 'id-2'
        }
    ];
    for (const { channel, handler, id } of subscriptions) {
        client.subscribe(channel, handler, { id });
        console.log(`Subscribed to ${channel} channel`);
    }

    // Register the account and confirm it is active
    client.publish({
        destination: '/w/api/account/get',
        body: JSON.stringify({ account: MONITOR_ADDRESS })
    });
    await registered;
    console.log('Account registered');

    // Announce the transaction
    console.log(`Announcing transaction ${shortHash}...`);
    const response = await fetch(`${NODE_URL}/transaction/announce`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: jsonPayload
    });
    const announceResult = await response.json();

    // Wait for the transaction to confirm
    if ('SUCCESS' === announceResult.message)
        await done;
    else
        console.log(`Transaction rejected: ${announceResult.message}`);
    // Unsubscribe before closing
    for (const { id } of subscriptions)
        client.unsubscribe(id);
    console.log('Unsubscribed from all channels');
    client.deactivate();
} catch (error) {
    console.error(error);
}

Download source

スニペットでは、NODE_URL 環境変数を使って NEM ノード を指定します。 値が指定されていない場合は、デフォルト値を使用します。

WS_URL は同じノードの WebSocket エンドポイントを定義します。 NODE_URL のポート 7890(デフォルトの HTTP API ポート)を 7778(デフォルトの NIS WebSocket ポート)に置き換えて導出します。

Python の SockJS ヘルパー

Python 用の SockJS クライアントライブラリはないため、便宜上、ファイルの先頭に小さなヘルパーメソッドをいくつか定義しています。

コードの説明⚓︎

監視対象アドレスと署名者をセットアップする⚓︎

# Set up the monitored address and signer
MONITOR_ADDRESS = os.getenv(
    'MONITOR_ADDRESS',
    'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4'
)
print(f'Monitoring address: {MONITOR_ADDRESS}')

SIGNER_PRIVATE_KEY = os.getenv(
    'SIGNER_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000000'
)
facade = NemFacade('testnet')
signer_key_pair = NemFacade.KeyPair(PrivateKey(SIGNER_PRIVATE_KEY))
// Set up the monitored address and signer
const MONITOR_ADDRESS = process.env.MONITOR_ADDRESS ||
    'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4';
console.log(`Monitoring address: ${MONITOR_ADDRESS}`);

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

この手順では、監視するアドレスと、そこへ送金するアカウントをセットアップします。

MONITOR_ADDRESS は監視するアドレスです。 このチュートリアルがサブスクライブするチャネルはこのアドレスに対応付けられ、送金の送信者や受取人など、トランザクションに関係するたびに通知します。 WebSocket API では、アドレスを大文字かつハイフンなしで指定します。

SIGNER_PRIVATE_KEY は送金を送信するアカウントの秘密鍵で、通知を発生させます。

これらの環境変数が設定されていない場合は、チュートリアルがデフォルト値を用意します。

転送トランザクションを構築して署名する⚓︎

    # Build and sign a transfer to the monitored address
    with urllib.request.urlopen(
        f'{NODE_URL}/time-sync/network-time'
    ) as resp:
        network_time = json.loads(
            resp.read().decode())['receiveTimeStamp'] // 1000
    timestamp = NetworkTimestamp(network_time)
    deadline = timestamp.add_hours(2)
    transaction = facade.transaction_factory.create({
        'type': 'transfer_transaction_v2',
        'signer_public_key': signer_key_pair.public_key,
        'timestamp': timestamp.timestamp,
        'deadline': deadline.timestamp,
        'recipient_address': MONITOR_ADDRESS,
        'amount': 0,
    })
    transaction.fee = Amount(calculate_transaction_fee(transaction))
    signature = facade.sign_transaction(signer_key_pair, transaction)
    json_payload = facade.transaction_factory.attach_signature(
        transaction, signature)
    transaction_hash = str(
        facade.hash_transaction(transaction)).upper()
    // Build and sign a transfer to the monitored address
    const timeResponse = await fetch(
        `${NODE_URL}/time-sync/network-time`);
    const networkTime = Math.floor(
        (await timeResponse.json()).receiveTimeStamp / 1000);
    const timestamp = new NetworkTimestamp(networkTime);
    const deadline = timestamp.addHours(2);
    const transaction = facade.transactionFactory.create({
        type: 'transfer_transaction_v2',
        signerPublicKey: signerKeyPair.publicKey.toString(),
        timestamp: timestamp.timestamp,
        deadline: deadline.timestamp,
        recipientAddress: MONITOR_ADDRESS,
        amount: 0n
    });
    transaction.fee = new models.Amount(
        calculateTransactionFee(transaction));
    const signature = facade.signTransaction(signerKeyPair, transaction);
    const jsonPayload = facade.transactionFactory.static.attachSignature(
        transaction, signature);
    const transactionHash =
        facade.hashTransaction(transaction).toString().toUpperCase();
    const shortHash = transactionHash.substring(0, 16);

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

トランザクションは XEM を送信する チュートリアルと同じ方法で構築します。ネットワーク時刻を取得し、トランザクションを作成して、署名します。

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

トランザクションは準備されますが、まだ アナウンス されません。 チャネルのサブスクリプションを確立した後でアナウンスするため、結果の通知を取り逃しません。

WebSocket に接続する⚓︎

    # Connect to the WebSocket
    endpoint = f'{WS_URL}/w/messages'
    async with connect(sockjs_url(endpoint)) as websocket:
        await stomp_connect(websocket)
        print(f'Connected to {WS_URL}')
        frames = stomp_frames(websocket)
    // Connect to the WebSocket
    const client = new Client({
        webSocketFactory: () => new SockJS(`${WS_URL}/w/messages`)
    });
    await new Promise(resolve => {
        client.onConnect = resolve;
        client.activate();
    });
    console.log(`Connected to ${WS_URL}`);

コードは WS_URL/w/messages エンドポイントへ SockJS 接続を開き、その上で STOMP セッション を開始します。

チャネルをサブスクライブする⚓︎

        # Subscribe to the account and transaction channels
        account_channel = f'/account/{MONITOR_ADDRESS}'
        channels = {
            account_channel: 'id-0',
            f'/unconfirmed/{MONITOR_ADDRESS}': 'id-1',
            f'/transactions/{MONITOR_ADDRESS}': 'id-2',
        }
        for channel, sub_id in channels.items():
            await stomp_subscribe(websocket, channel, sub_id)
            print(f'Subscribed to {channel} channel')
    // Subscribe to the account and transaction channels
    const accountChannel = `/account/${MONITOR_ADDRESS}`;
    const subscriptions = [
        { channel: accountChannel, handler: onAccountUpdate, id: 'id-0' },
        {
            channel: `/unconfirmed/${MONITOR_ADDRESS}`,
            handler: onUnconfirmed,
            id: 'id-1'
        },
        {
            channel: `/transactions/${MONITOR_ADDRESS}`,
            handler: onConfirmed,
            id: 'id-2'
        }
    ];
    for (const { channel, handler, id } of subscriptions) {
        client.subscribe(channel, handler, { id });
        console.log(`Subscribed to ${channel} channel`);
    }

コードは、アドレスに対応付けられた次の 3 チャネルをサブスクライブします。

サブスクリプションには id-0id-1id-2 を使います。プログラムが最後にサブスクライブを解除するときに、それぞれを識別します。

メッセージ処理の違い

JavaScript では、各チャネルを専用のハンドラー関数でサブスクライブします。関数は下の 承認を待つ 手順で定義します。 Python では、接続から到着したメッセージを順番に読み取ります。

3 つのチャネルは、次の手順でアドレスを登録するまで何も送信しません。

アカウントを登録する⚓︎

        # Register the account and confirm it is active
        await stomp_send(websocket, '/w/api/account/get',
            json.dumps({'account': MONITOR_ADDRESS}))
        async for frame in frames:
            if account_channel == frame['headers']['destination']:
                balance = json.loads(
                    frame['body'])['account']['balance']
                print(f'Account update: balance={balance}')
                break
        print('Account registered')
    // Register the account and confirm it is active
    client.publish({
        destination: '/w/api/account/get',
        body: JSON.stringify({ account: MONITOR_ADDRESS })
    });
    await registered;
    console.log('Account registered');

アカウントのチャネルから通知を受け取るには、まずアドレスをノードに 登録 する必要があります。

コードは w/api/account/get REQ にリクエストを送信します。アドレスを登録するとともに、account/{address} WS チャネルでアカウントの現在の状態を送信するようノードに要求します。

コードは最初のアカウント通知を待ち、登録が有効になったことを確認します。 通知は AccountMetaDataPair スキーマに従います。

アカウントチャネルのサブスクリプションは実行中ずっと開いたままなので、トランザクションの承認で発生するアカウント通知も出力に表示されます。

トランザクションをアナウンスする⚓︎

        # Announce the transaction
        print(f'Announcing transaction {transaction_hash[:16]}...')
        announce_request = urllib.request.Request(
            f'{NODE_URL}/transaction/announce',
            data=json_payload.encode(),
            headers={'Content-Type': 'application/json'},
            method='POST'
        )
        with urllib.request.urlopen(announce_request) as resp:
            result = json.loads(resp.read().decode())
    // Announce the transaction
    console.log(`Announcing transaction ${shortHash}...`);
    const response = await fetch(`${NODE_URL}/transaction/announce`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: jsonPayload
    });
    const announceResult = await response.json();

チャネルをサブスクライブしてからアナウンスしてください

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

コードは /transaction/announce POST エンドポイントへトランザクションをアナウンスし、結果を確認します。 ノードが拒否した場合は、拒否理由を表示して停止します。

承認を待つ⚓︎

        # Wait for the transaction to confirm
        if 'SUCCESS' == result['message']:
            confirmed = False
            async for frame in frames:
                destination = frame['headers']['destination']
                body = json.loads(frame['body'])
                if account_channel == destination:
                    balance = body['account']['balance']
                    print(f'Account update: balance={balance}')
                    if confirmed:
                        break
                elif '/transactions/' in destination:
                    message_hash = body['meta']['hash']['data']
                    print(f'confirmed: hash={message_hash[:16]}...')
                    if message_hash.upper() == transaction_hash:
                        short_hash = transaction_hash[:16]
                        print(f'Transaction {short_hash}... confirmed')
                        confirmed = True
                else:
                    message_hash = body['meta']['hash']['data']
                    if message_hash.upper() == transaction_hash:
                        print(f'unconfirmed: hash={message_hash[:16]}...')
        else:
            print(f'Transaction rejected: {result["message"]}')
    // Wait for the transaction to confirm
    let confirmed = false;
    let resolveRegistered;
    let resolveDone;
    const registered = new Promise(resolve => {
        resolveRegistered = resolve;
    });
    const done = new Promise(resolve => {
        resolveDone = resolve;
    });
    const onUnconfirmed = message => {
        const messageHash = JSON.parse(message.body).meta.hash.data;
        if (messageHash.toUpperCase() === transactionHash) {
            console.log(
                `unconfirmed: hash=${messageHash.substring(0, 16)}...`);
        }
    };
    const onConfirmed = message => {
        const messageHash = JSON.parse(message.body).meta.hash.data;
        console.log(`confirmed: hash=${messageHash.substring(0, 16)}...`);
        if (messageHash.toUpperCase() === transactionHash) {
            console.log(`Transaction ${shortHash}... confirmed`);
            confirmed = true;
        }
    };
    const onAccountUpdate = message => {
        const { balance } = JSON.parse(message.body).account;
        console.log(`Account update: balance=${balance}`);
        resolveRegistered();
        if (confirmed)
            resolveDone();
    };

受け付けられた場合、コードは承認を待ち、サブスクライブしたチャネルからの各メッセージを表示します。

トランザクションチャネルからのメッセージは TransactionMetaDataPair スキーマに従い、meta.hash.data フィールドにトランザクションハッシュを持ちます。 メッセージが届くたびに、コードはそのハッシュを保存値と比較して、チャネル通知の中からこのトランザクションを認識します。

成功したトランザクションで想定される順序は、トランザクションのライフサイクル に説明されています。

  1. unconfirmed: トランザクションが 未承認トランザクションプール に入る。
  2. confirmed: トランザクションが ブロック に含まれる。

トランザクションを含むブロックは、account/{address} WS チャネルでも最後の通知を発生させます。 トランザクションチャネルとは異なり、この通知にはトランザクションハッシュではなくアカウントの更新後の状態が含まれるため、特定のトランザクションとは照合できません。

この最後の通知が届くと、プログラムは後片付けの手順へ進みます。

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

        # Unsubscribe before closing
        for sub_id in channels.values():
            await stomp_unsubscribe(websocket, sub_id)
        print('Unsubscribed from all channels')
        await stomp_disconnect(websocket)
    // Unsubscribe before closing
    for (const { id } of subscriptions)
        client.unsubscribe(id);
    console.log('Unsubscribed from all channels');
    client.deactivate();

承認後、コードは 3 つのチャネルのサブスクライブを解除し、接続を閉じる前に STOMP セッションを終了します。

出力⚓︎

Using node http://libertalia.nemtest.net:7890
Monitoring address: TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4
Connected to http://libertalia.nemtest.net:7778
Subscribed to /account/TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4 channel
Subscribed to /unconfirmed/TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4 channel
Subscribed to /transactions/TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4 channel
Account update: balance=10035200000
Account registered
Announcing transaction 2928C2D9554AE127...
unconfirmed: hash=2928c2d9554ae127...
confirmed: hash=2928c2d9554ae127...
Transaction 2928C2D9554AE127... confirmed
Account update: balance=10035200000
Unsubscribed from all channels

出力には次の内容が表示されます。

  • アドレス(2 行目): 監視対象のアドレス。
  • 接続(3 行目): ノードのポート 7778 の WebSocket エンドポイント上で STOMP セッションが確立されます。
  • サブスクリプション(4~6 行目): アカウントチャネルと 2 つのトランザクションチャネルがサブスクライブされます。
  • 登録(7~8 行目): アカウントの現在の状態がアカウントチャネルに届き、登録を確認します。
  • アナウンス(9 行目): トランザクションがアナウンスされ、そのハッシュが表示されます。
  • トランザクションフロー(10~11 行目): トランザクションが unconfirmed から confirmed へ進み、承認のライフサイクルが表示されます。
  • 承認(12 行目): transactions/{address} WS チャネルのハッシュが、アナウンスしたトランザクションと一致します。
  • アカウント更新(13 行目): トランザクションを含むブロックが最後のアカウント通知を発生させます。送金額が 0 なので残高は変わりません。
  • サブスクライブ解除(14 行目): コードが 3 つのチャネルのサブスクライブを解除します。

まとめ⚓︎

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

手順 関連ドキュメント
アカウントチャネルをサブスクライブする account/{address} WS
未承認チャネルをサブスクライブする unconfirmed/{address} WS
トランザクションチャネルをサブスクライブする transactions/{address} WS
アカウントを登録する w/api/account/get REQ
トランザクションメッセージを処理する TransactionMetaDataPair