コンテンツにスキップ

転送トランザクションの作成⚓︎

初級

転送トランザクション は、Symbol における最も基本的なトランザクションのタイプです。 これを使用すると、ある アカウント から別のアカウントへ XYM やその他の任意の モザイク を送信でき、オプションでメッセージを含めることも可能です。

このチュートリアルでは、転送トランザクションを作成、署名、およびアナウンスし、その後トランザクションが承認されるまでステータスをポーリングする方法を説明します。 SDK が適切なトランザクション手数料を計算できるよう、推奨手数料乗数をネットワークから取得します。

このチュートリアルでは、トランザクションの作成とアナウンスに関する基本的な概念を説明します。 他のチュートリアルでは、これらの基本的な手法の説明としてこのチュートリアルを参照します。

代替手段: 手動トランザクション作成

このチュートリアルでは、最も便利で型安全な方法であるディスクリプタからトランザクションを作成する方法を説明します。 代替となる下位レベルの手動の方法については、 手動トランザクション作成 チュートリアルを参照してください。

前提条件⚓︎

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

完全なコード⚓︎

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

import json
import os
import time
import urllib.request

from symbolchain.CryptoTypes import PrivateKey
from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.symbol.IdGenerator import generate_mosaic_alias_id

NODE_URL = os.getenv('NODE_URL', 'https://reference.symboltest.net:3001')

print(f'Using node {NODE_URL}')

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

facade = SymbolFacade('testnet')


# Helper function to announce a transaction
def announce_transaction(payload, label):
    print(f'Announcing {label} to /transactions')
    request = urllib.request.Request(
        f'{NODE_URL}/transactions',
        data=payload.encode(),
        headers={'Content-Type': 'application/json'},
        method='PUT'
    )
    with urllib.request.urlopen(request) as announce_response:
        print(f'  Response: {announce_response.read().decode()}')


# Helper function to wait for transaction confirmation
def wait_for_confirmation(tx_hash, label):
    print(f'Waiting for {label} confirmation...')
    for attempt in range(60):
        time.sleep(1)
        try:
            url = f'{NODE_URL}/transactionStatus/{tx_hash}'
            with urllib.request.urlopen(url) as confirm_response:
                status = json.loads(confirm_response.read().decode())
                print(f'  Transaction status: {status["group"]}')
                if status['group'] == 'confirmed':
                    print(f'{label} confirmed in {attempt} seconds')
                    return
                if status['group'] == 'failed':
                    raise RuntimeError(
                        f'{label} failed: {status["code"]}')
        except urllib.error.HTTPError:
            print('  Transaction status: unknown')
    raise TimeoutError(f'{label} not confirmed after 60 seconds')


try:
    # Fetch recommended fees
    fee_path = '/network/fees/transaction'
    print(f'Fetching recommended fees from {fee_path}')
    with urllib.request.urlopen(f'{NODE_URL}{fee_path}') as response:
        response_json = json.loads(response.read().decode())
        median_multiplier = response_json['medianFeeMultiplier']
        minimum_multiplier = response_json['minFeeMultiplier']
        fee_multiplier = max(median_multiplier, minimum_multiplier)
        print(f'  Fee multiplier: {fee_multiplier}')

    # Build the transaction
    transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'transfer_transaction_v1',
            'recipient_address':
                facade.network.public_key_to_address(
                    signer_key_pair.public_key),
            'mosaics': [{
                'mosaic_id': generate_mosaic_alias_id('symbol.xym'),
                'amount': 1_000_000  # 1 XYM
            }]
        },
        signer_key_pair.public_key,
        fee_multiplier,
        2 * 60 * 60)

    # Sign transaction and generate final payload
    signature = facade.sign_transaction(signer_key_pair, transaction)
    json_payload = facade.transaction_factory.attach_signature(
        transaction, signature)
    print('Built transaction:')
    print(json.dumps(transaction.to_json(), indent=2))

    transaction_hash = facade.hash_transaction(transaction)
    print(f'Transaction hash: {transaction_hash}')
    announce_transaction(json_payload, 'transaction')
    wait_for_confirmation(transaction_hash, 'transaction')
except urllib.error.URLError as e:
    print(e.reason)

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';
console.log('Using node', NODE_URL);

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

const facade = new SymbolFacade('testnet');

// Helper function to announce a transaction
async function announceTransaction(payload, label) {
    console.log(`Announcing ${label} to /transactions`);
    const response = await fetch(`${NODE_URL}/transactions`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: payload
    });
    console.log('  Response:', await response.text());
}


// Helper function to wait for transaction confirmation
async function waitForConfirmation(transactionHash, label) {
    console.log(`Waiting for ${label} confirmation...`);
    for (let attempt = 0; 60 > attempt; attempt++) {
        await new Promise(resolve => { setTimeout(resolve, 1000); });
        const response = await fetch(
            `${NODE_URL}/transactionStatus/${transactionHash}`);
        if (!response.ok) {
            if (404 === response.status) {
                console.log('  Transaction status: unknown');
                continue;
            }
            throw new Error(`HTTP ${response.status}`);
        }
        const status = await response.json();
        console.log('  Transaction status:', status.group);
        if ('confirmed' === status.group) {
            console.log(`${label} confirmed in`, attempt, 'seconds');
            return;
        }
        if ('failed' === status.group)
            throw new Error(`${label} failed: ${status.code}`);
    }
    throw new Error(`${label} not confirmed after 60 seconds`);
}


try {
    // Fetch recommended fees
    const feePath = '/network/fees/transaction';
    console.log('Fetching recommended fees from', feePath);
    const feeResponse = await fetch(`${NODE_URL}${feePath}`);
    const feeJSON = await feeResponse.json();
    const medianMultiplier = feeJSON.medianFeeMultiplier;
    const minimumMultiplier = feeJSON.minFeeMultiplier;
    const feeMultiplier = Math.max(medianMultiplier, minimumMultiplier);
    console.log('  Fee multiplier:', feeMultiplier);

    // Build the transaction
    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.TransferTransactionV1Descriptor(
            facade.network.publicKeyToAddress(signerKeyPair.publicKey),
            [
                new descriptors.UnresolvedMosaicDescriptor(
                    generateMosaicAliasId('symbol.xym'),
                    new models.Amount(1_000_000n)) // 1 XYM
            ],
            undefined),
        signerKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60);

    // Sign transaction and generate final payload
    const signature = facade.signTransaction(signerKeyPair, transaction);
    const jsonPayload = facade.transactionFactory.static.attachSignature(
        transaction, signature);
    console.log('Built transaction:');
    console.dir(transaction.toJson(), { colors: true });

    const transactionHash =
        facade.hashTransaction(transaction).toString();
    console.log('Transaction hash:', transactionHash);
    await announceTransaction(jsonPayload, 'transaction');
    await waitForConfirmation(transactionHash, 'transaction');
} catch (e) {
    console.error(e.message, '| Cause:', e.cause?.code ?? 'unknown');
}

Download source

//JAVA 21+
//DEPS org.symbol:symbol-sdk:3.3.1

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 com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.symbol.sdk.CryptoTypes;
import org.symbol.sdk.facade.SymbolFacade;
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.*;

public final class Transfer {
    private static final ObjectMapper JSON_MAPPER = new ObjectMapper();

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

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

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

    // Helper method to announce a transaction
    private void announceTransaction(
        final String payload,
        final String label
    ) throws IOException, InterruptedException {
        System.out.printf("Announcing %s to /transactions%n", label);
        final HttpRequest request = HttpRequest.newBuilder(
            URI.create(nodeUrl + "/transactions"))
            .header("Content-Type", "application/json")
            .PUT(HttpRequest.BodyPublishers.ofString(payload))
            .build();
        final HttpResponse<String> response = HTTP_CLIENT.send(
            request, BodyHandlers.ofString());
        System.out.printf("  Response: %s%n", response.body());
    }


    // Helper method to wait for transaction confirmation
    private void waitForConfirmation(
        final String transactionHash,
        final String label
    ) throws IOException, InterruptedException {
        System.out.printf("Waiting for %s confirmation...%n", label);
        for (int attempt = 0; 60 > attempt; ++attempt) {
            Thread.sleep(1000);
            final String statusPath =
                "/transactionStatus/" + transactionHash;
            final HttpRequest statusRequest = HttpRequest.newBuilder(
                URI.create(nodeUrl + statusPath)).GET().build();
            final HttpResponse<String> statusResponse = HTTP_CLIENT
                .send(statusRequest, BodyHandlers.ofString());
            if (404 == statusResponse.statusCode()) {
                System.out.println("  Transaction status: unknown");
                continue;
            }
            if (2 != statusResponse.statusCode() / 100)
                throw new IOException(
                    "HTTP " + statusResponse.statusCode());

            final JsonNode status =
                JSON_MAPPER.readTree(statusResponse.body());
            final String group = status.get("group").asText();
            System.out.printf("  Transaction status: %s%n", group);
            if ("confirmed".equals(group)) {
                System.out.printf("%s confirmed in %d seconds%n",
                    label, attempt);
                return;
            }
            if ("failed".equals(group))
                throw new IOException(String.format("%s failed: %s",
                    label, status.get("code").asText()));
        }
        throw new IOException(String.format(
            "%s not confirmed after 60 seconds", label));
    }


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

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


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


        // Fetch recommended fees
        final String feePath = "/network/fees/transaction";
        System.out.printf("Fetching recommended fees from %s%n", feePath);
        final HttpRequest feeRequest = HttpRequest.newBuilder(
            URI.create(nodeUrl + feePath)).GET().build();
        final HttpResponse<String> feeResponse = HTTP_CLIENT.send(
            feeRequest, BodyHandlers.ofString());
        final JsonNode feeJSON = JSON_MAPPER.readTree(feeResponse.body());
        final long medianMultiplier =
            feeJSON.get("medianFeeMultiplier").asLong();
        final long minimumMultiplier =
            feeJSON.get("minFeeMultiplier").asLong();
        final long feeMultiplier = Math.max(
            medianMultiplier, minimumMultiplier);
        System.out.printf("  Fee multiplier: %d%n", feeMultiplier);

        // Build the transaction
        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new TransferTransactionV1Descriptor(
                    facade.network.publicKeyToAddress(
                        signerKeyPair.getPublicKey()),
                    List.of(new UnresolvedMosaicDescriptor(
                        new UnresolvedMosaicId(
                            IdGenerator.generateMosaicAliasId(
                                "symbol.xym")),
                        new Amount(1_000_000))), // 1 XYM
                    null),
                signerKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60);

        // Sign transaction and generate final payload
        final CryptoTypes.Signature signature = facade.signTransaction(
            signerKeyPair, transaction);
        final String jsonPayload = SymbolTransactionFactory
            .attachSignature(transaction, signature);
        System.out.println("Built transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));

        final String transactionHash =
            facade.hashTransaction(transaction).toString();
        System.out.printf("Transaction hash: %s%n", transactionHash);
        announceTransaction(jsonPayload, "transaction");
        waitForConfirmation(transactionHash, "transaction");
    }
}

Download source

コード全体は、単純なエラー処理を提供するために単一の try ブロックでラップされていますが、実際のアプリケーションではより詳細な制御が必要になるでしょう。

コード解説⚓︎

アカウントのセットアップ⚓︎

SIGNER_PRIVATE_KEY = os.getenv('SIGNER_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000000')
signer_key_pair = SymbolFacade.KeyPair(PrivateKey(SIGNER_PRIVATE_KEY))
const SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY ||
    '0000000000000000000000000000000000000000000000000000000000000000';
const signerKeyPair = new SymbolFacade.KeyPair(
    new PrivateKey(SIGNER_PRIVATE_KEY));
        final String signerPrivateKey = System.getenv().getOrDefault(
            "SIGNER_PRIVATE_KEY", "0".repeat(64));
        final KeyPair signerKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(signerPrivateKey));

署名者アカウントは、 SIGNER_PRIVATE_KEY 環境変数から読み込まれます。 指定されていない場合は、デフォルトでテストキーが使用されます。

    # Fetch recommended fees
    fee_path = '/network/fees/transaction'
    print(f'Fetching recommended fees from {fee_path}')
    with urllib.request.urlopen(f'{NODE_URL}{fee_path}') as response:
        response_json = json.loads(response.read().decode())
        median_multiplier = response_json['medianFeeMultiplier']
        minimum_multiplier = response_json['minFeeMultiplier']
        fee_multiplier = max(median_multiplier, minimum_multiplier)
        print(f'  Fee multiplier: {fee_multiplier}')
    // Fetch recommended fees
    const feePath = '/network/fees/transaction';
    console.log('Fetching recommended fees from', feePath);
    const feeResponse = await fetch(`${NODE_URL}${feePath}`);
    const feeJSON = await feeResponse.json();
    const medianMultiplier = feeJSON.medianFeeMultiplier;
    const minimumMultiplier = feeJSON.minFeeMultiplier;
    const feeMultiplier = Math.max(medianMultiplier, minimumMultiplier);
    console.log('  Fee multiplier:', feeMultiplier);
        // Fetch recommended fees
        final String feePath = "/network/fees/transaction";
        System.out.printf("Fetching recommended fees from %s%n", feePath);
        final HttpRequest feeRequest = HttpRequest.newBuilder(
            URI.create(nodeUrl + feePath)).GET().build();
        final HttpResponse<String> feeResponse = HTTP_CLIENT.send(
            feeRequest, BodyHandlers.ofString());
        final JsonNode feeJSON = JSON_MAPPER.readTree(feeResponse.body());
        final long medianMultiplier =
            feeJSON.get("medianFeeMultiplier").asLong();
        final long minimumMultiplier =
            feeJSON.get("minFeeMultiplier").asLong();
        final long feeMultiplier = Math.max(
            medianMultiplier, minimumMultiplier);
        System.out.printf("  Fee multiplier: %d%n", feeMultiplier);

Symbol の トランザクションは、ノードにトランザクションをブロックに含めるインセンティブを与えるために手数料を支払う必要があります。 手数料が低すぎると、どのノードもトランザクションを含めない可能性があります。 高すぎると、送信者は資金を無駄にします。 さらに、各ノードは入ってくるトランザクションに対して最低手数料のしきい値を強制する場合があります。

最適な手数料は、ネットワークの現在の状態、特に送信されているトランザクションの数とそれらが提示している手数料に依存します。 手数料の見積もりをサポートするために、Symbol は最近のトランザクション活動に基づく 推奨手数料乗数(recommended fee multiplier) を返す /network/fees/transaction GET エンドポイントを提供しています。

最終的な手数料は、推奨乗数にトランザクションのサイズ(バイト単位)を掛けて計算されます。 すべてのチュートリアルで行うようにディスクリプタからトランザクションを作成する場合、この処理は SDK によって実行されます。 を使用して手動でトランザクションを作成する場合は、最終的な手数料を自分で計算する必要があります。

アプリケーションは簡略化のために固定の手数料を使用することもできますが、ネットワークの推奨に従う方がより効率的です。 トランザクションごとに乗数を照会する必要はありませんが、定期的に更新する必要があります。

上記のスニペットは、推奨乗数( medianFeeMultiplier )とノードの最小乗数( minFeeMultiplier )の大きい方を取得し、トランザクションのサイズが判明した後に使用できるように保存します。

トランザクションの構築⚓︎

ディスクリプタについて

転送トランザクションは、トランザクションの ディスクリプタ から作成します。

Java や JavaScript のような型付き言語では、これらのディスクリプタも型付きであるため、構築時や使用時に誤ったパラメータを指定する可能性が低くなります。 Python のような型なし言語では、ディスクリプタは各トランザクションタイプに必要な正しいフィールドを含む汎用オブジェクトです。

ディスクリプタにはトランザクション固有のフィールドを含め、共通フィールドはトランザクションを完成させるために作成メソッドに渡します。

    # Build the transaction
    transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'transfer_transaction_v1',
            'recipient_address':
                facade.network.public_key_to_address(
                    signer_key_pair.public_key),
            'mosaics': [{
                'mosaic_id': generate_mosaic_alias_id('symbol.xym'),
                'amount': 1_000_000  # 1 XYM
            }]
        },
        signer_key_pair.public_key,
        fee_multiplier,
        2 * 60 * 60)
    // Build the transaction
    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.TransferTransactionV1Descriptor(
            facade.network.publicKeyToAddress(signerKeyPair.publicKey),
            [
                new descriptors.UnresolvedMosaicDescriptor(
                    generateMosaicAliasId('symbol.xym'),
                    new models.Amount(1_000_000n)) // 1 XYM
            ],
            undefined),
        signerKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60);
        // Build the transaction
        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new TransferTransactionV1Descriptor(
                    facade.network.publicKeyToAddress(
                        signerKeyPair.getPublicKey()),
                    List.of(new UnresolvedMosaicDescriptor(
                        new UnresolvedMosaicId(
                            IdGenerator.generateMosaicAliasId(
                                "symbol.xym")),
                        new Amount(1_000_000))), // 1 XYM
                    null),
                signerKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60);

は、以下の引数を受け取ります。

  • トランザクションのディスクリプタ: TransferTransactionV1 と、以下で説明する転送フィールドを定義します。
  • 署名者の公開鍵: 署名者は手数料を支払うアカウントです。 転送トランザクションでは、転送されるモザイクの送信元でもあります。
  • 手数料乗数: トランザクション手数料の計算に使用されます。
  • デッドラインの期間: 現在時刻から2時間後に設定されます。

    デッドラインとネットワーク時間

    Symbol のトランザクションには、デッドラインを含める必要があります。 これは、ネットワークがトランザクションを破棄する前に承認を試みる期間を定義します。 デッドラインは、ネメシスブロック からの経過時間である ネットワーク時間 で表されます。

    トランザクションのデッドラインが現在のネットワーク時間より前、または6時間以上先の場合、トランザクションは拒否されます。 ディスクリプタからトランザクションを作成する場合、SDK がネットワーク時間を扱い、現在からの秒数で表した相対的なデッドラインの期間を受け取ります。

    を使用して手動でトランザクションを作成する場合は、絶対的なデッドラインを自分で指定する必要があります。 詳しくは 手動トランザクション作成 チュートリアルを参照してください。

トランザクションのディスクリプタには、以下のフィールドが含まれます。

  • Recipient address (受信者アドレス): この例では、受信者は送信者と同じです。 これはデモンストレーションとしては有用ですが、実用的ではありません。

  • Mosaics (モザイク): 転送トランザクションは一度に複数のモザイクを送信できるため、これは配列になります。 各エントリにはモザイク IDと金額が含まれます。

    この例では、XYM のモザイク ID は、完全な 16 進数 ID よりも覚えやすいエイリアスである symbol.xym を使用して取得されています。

    金額は絶対単位で表され、これはモザイクの 可分性 に依存します。 XYM の場合、可分性は 6 であるため、 1 XYM は 1_000_000 として表現する必要があります。

ディスクリプタには、署名者の公開鍵、デッドライン、手数料などの共通トランザクションフィールドは含めません。 がそれらを設定し、相対的なデッドラインのためにネットワーク時間を扱い、手数料乗数から手数料を計算します。

トランザクションにメッセージを含める

トランザクションには、オプションで自由形式のメッセージを含めることができます。 転送トランザクションでのメッセージの送信 で、その方法を説明しています。

署名とシリアライズ⚓︎

    # Sign transaction and generate final payload
    signature = facade.sign_transaction(signer_key_pair, transaction)
    json_payload = facade.transaction_factory.attach_signature(
        transaction, signature)
    print('Built transaction:')
    print(json.dumps(transaction.to_json(), indent=2))
    // Sign transaction and generate final payload
    const signature = facade.signTransaction(signerKeyPair, transaction);
    const jsonPayload = facade.transactionFactory.static.attachSignature(
        transaction, signature);
    console.log('Built transaction:');
    console.dir(transaction.toJson(), { colors: true });
        // Sign transaction and generate final payload
        final CryptoTypes.Signature signature = facade.signTransaction(
            signerKeyPair, transaction);
        final String jsonPayload = SymbolTransactionFactory
            .attachSignature(transaction, signature);
        System.out.println("Built transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));

トランザクションが作成されたら、署名するアカウントの秘密鍵を使用して署名する必要があります。 署名により、トランザクションが本物であり、送信者によって承認されていることが保証されます。

署名を返します。

は、署名をトランザクションに追加し、アナウンスのためにノードに直接送信できる状態の JSON ペイロードにシリアライズします。

トランザクションのアナウンス⚓︎

# Helper function to announce a transaction
def announce_transaction(payload, label):
    print(f'Announcing {label} to /transactions')
    request = urllib.request.Request(
        f'{NODE_URL}/transactions',
        data=payload.encode(),
        headers={'Content-Type': 'application/json'},
        method='PUT'
    )
    with urllib.request.urlopen(request) as announce_response:
        print(f'  Response: {announce_response.read().decode()}')
// Helper function to announce a transaction
async function announceTransaction(payload, label) {
    console.log(`Announcing ${label} to /transactions`);
    const response = await fetch(`${NODE_URL}/transactions`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: payload
    });
    console.log('  Response:', await response.text());
}
    // Helper method to announce a transaction
    private void announceTransaction(
        final String payload,
        final String label
    ) throws IOException, InterruptedException {
        System.out.printf("Announcing %s to /transactions%n", label);
        final HttpRequest request = HttpRequest.newBuilder(
            URI.create(nodeUrl + "/transactions"))
            .header("Content-Type", "application/json")
            .PUT(HttpRequest.BodyPublishers.ofString(payload))
            .build();
        final HttpResponse<String> response = HTTP_CLIENT.send(
            request, BodyHandlers.ofString());
        System.out.printf("  Response: %s%n", response.body());
    }

このヘルパーは JSON ペイロードと、人間が読めるラベルを受け取ります。 ラベルはログメッセージでのみ使用されるため、複数のトランザクションをアナウンスするチュートリアルでも同じヘルパーを再利用できます。

トランザクションのアナウンスは、任意の SymbolのAPI ノード/transactions PUT エンドポイントへの単純なリクエストです。 ペイロードが正しく形成されていれば、リクエストは HTTP 200 レスポンスで成功します。

しかし、このレスポンスはトランザクションが有効であること、またはネットワークによって受け入れられたことを示すものでは ありません。 検証、手数料の確認、およびその他のルールは、トランザクションが受信された後に非同期に適用されます。

トランザクションが実際に受け入れられ、ブロックに含まれたことを確認するには、次のステップで示すように、そのステータスを個別に監視する必要があります。

承認の待機⚓︎

# Helper function to wait for transaction confirmation
def wait_for_confirmation(tx_hash, label):
    print(f'Waiting for {label} confirmation...')
    for attempt in range(60):
        time.sleep(1)
        try:
            url = f'{NODE_URL}/transactionStatus/{tx_hash}'
            with urllib.request.urlopen(url) as confirm_response:
                status = json.loads(confirm_response.read().decode())
                print(f'  Transaction status: {status["group"]}')
                if status['group'] == 'confirmed':
                    print(f'{label} confirmed in {attempt} seconds')
                    return
                if status['group'] == 'failed':
                    raise RuntimeError(
                        f'{label} failed: {status["code"]}')
        except urllib.error.HTTPError:
            print('  Transaction status: unknown')
    raise TimeoutError(f'{label} not confirmed after 60 seconds')
// Helper function to wait for transaction confirmation
async function waitForConfirmation(transactionHash, label) {
    console.log(`Waiting for ${label} confirmation...`);
    for (let attempt = 0; 60 > attempt; attempt++) {
        await new Promise(resolve => { setTimeout(resolve, 1000); });
        const response = await fetch(
            `${NODE_URL}/transactionStatus/${transactionHash}`);
        if (!response.ok) {
            if (404 === response.status) {
                console.log('  Transaction status: unknown');
                continue;
            }
            throw new Error(`HTTP ${response.status}`);
        }
        const status = await response.json();
        console.log('  Transaction status:', status.group);
        if ('confirmed' === status.group) {
            console.log(`${label} confirmed in`, attempt, 'seconds');
            return;
        }
        if ('failed' === status.group)
            throw new Error(`${label} failed: ${status.code}`);
    }
    throw new Error(`${label} not confirmed after 60 seconds`);
}
    // Helper method to wait for transaction confirmation
    private void waitForConfirmation(
        final String transactionHash,
        final String label
    ) throws IOException, InterruptedException {
        System.out.printf("Waiting for %s confirmation...%n", label);
        for (int attempt = 0; 60 > attempt; ++attempt) {
            Thread.sleep(1000);
            final String statusPath =
                "/transactionStatus/" + transactionHash;
            final HttpRequest statusRequest = HttpRequest.newBuilder(
                URI.create(nodeUrl + statusPath)).GET().build();
            final HttpResponse<String> statusResponse = HTTP_CLIENT
                .send(statusRequest, BodyHandlers.ofString());
            if (404 == statusResponse.statusCode()) {
                System.out.println("  Transaction status: unknown");
                continue;
            }
            if (2 != statusResponse.statusCode() / 100)
                throw new IOException(
                    "HTTP " + statusResponse.statusCode());

            final JsonNode status =
                JSON_MAPPER.readTree(statusResponse.body());
            final String group = status.get("group").asText();
            System.out.printf("  Transaction status: %s%n", group);
            if ("confirmed".equals(group)) {
                System.out.printf("%s confirmed in %d seconds%n",
                    label, attempt);
                return;
            }
            if ("failed".equals(group))
                throw new IOException(String.format("%s failed: %s",
                    label, status.get("code").asText()));
        }
        throw new IOException(String.format(
            "%s not confirmed after 60 seconds", label));
    }

Note

このステップでは、トランザクションが承認されたかどうかを確認するためにポーリングを使用しています。 ポーリングはここでは説明の目的で使用されていますが、実際のアプリケーションには推奨されるアプローチではありません。

WebSocket を使用すると、API 呼び出しを繰り返すオーバーヘッドなしに、よりレスポンスの高いソリューションを提供できます。

さらに、トランザクションステータスを確認するロジックは再利用可能です。 このチュートリアルでは、ほぼすべてのトランザクションをアナウンスした後に必要となるため、ヘルパーとして定義しています。

上記のスニペットは、送信されたトランザクションのハッシュを使用して、繰り返し /transactionStatus/{hash} GET エンドポイントを照会します。 アナウンス用ヘルパーと同様に、このヘルパーもラベルを受け取るため、複数のトランザクションをアナウンスするチュートリアルでも出力が分かりやすくなります。 レスポンスは以下のいずれかの形式をとる可能性があります。

  • HTTP エラー。ノードがまだトランザクションの処理を開始していないことを示します。
  • トランザクションステータスを含む有効な JSON オブジェクト。

ステータスグループが confirmed の場合、トランザクションは受け入れられ、ブロックに含まれています。

ステータスグループが failed の場合、トランザクションは拒否されています。例えば、残高不足などが原因です。

それ以外の場合は、コードは 1 秒待機してから再試行します(最大 60 回まで)。

コードは、最初のステータス確認を実行する 前に 最初の待機を行っていることに注目してください。 これにより、アナウンス後にノードがトランザクションの処理を開始するための時間が確保されます。

出力⚓︎

以下に示す出力は、プログラムの典型的な実行結果に対応しています。

Using node https://reference.symboltest.net:3001
Fetching recommended fees from /network/fees/transaction
  Fee multiplier: 100
Built transaction:
{
  signature: '728D968E14F50EBB2496B560721E938629D6B4C1522B4A22DD659507B469C0EC5125485EBD38D48FBF351FF9DEC9CF3AFD7A5AFC5E945087E53173589B0B6B08',
  signerPublicKey: '87DA603E7BE5656C45692D5FC7F6D0EF8F24BB7A5C10ED5FDA8C5CFBC49FCBC8',
  version: 1,
  network: 152,
  type: 16724,
  fee: '17600',
  deadline: '78242662065',
  recipientAddress: '98F96BD2F803DE1EE39AACFC53A246F4F7A46901A5D0A53E',
  mosaics: [ { mosaicId: '16666583871264174062', amount: '1000000' } ],
  message: ''
}
Transaction hash: 260CD293E05C2853A967874BCF67FAB36FD331CE14925CA611B3877B99BB325D
Announcing transaction to /transactions
  Response: {"message":"packet 9 was pushed to the network via /transactions"}
Waiting for transaction confirmation...
  Transaction status: unknown
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  Transaction status: confirmed
transaction confirmed in 5 seconds

出力のハイライトをいくつか紹介します。

  • 手数料乗数(3 行目): ネットワークから取得した推奨乗数で、トランザクションサイズと組み合わせて手数料を計算するために使用されます。

  • 署名者公開鍵(7 行目): トランザクションに署名し、モザイクを送信するアカウントです。

  • トランザクション手数料(11 行目): 17600 絶対単位(0.0176 XYM)で、手数料乗数とトランザクションのバイトサイズから算出されます。

  • 受信者アドレス(13 行目): モザイクを受け取るアカウントです。 コードで使用したアドレスと異なって見えるのは、トランザクション形式が Base32 テキストではなく生の 16 進数形式でエンコードするためです。

  • モザイク(14 行目): 転送される資産です。 ここでは、symbol.xymXYM)のエイリアスを持つモザイクの 1000000 絶対単位で、1 XYM に相当します。

  • アナウンス応答(19 行目): ノードがペイロードを受理したことを示します。 ただし、この時点ではトランザクションが有効である、またはブロックに取り込まれたことを意味するわけではありません。

  • 承認済みステータス(26 行目): トランザクションが受理され、ブロックに取り込まれたことを示します。

承認までのステータス確認の回数はネットワークの状況によって変化し、ノードがトランザクションの処理を開始する速度によっては、最初の unknown ステータスが表示されたりされなかったりします。

ネットワークの観点からトランザクションを確認するには、 Symbol Testnet Explorer にアクセスし、トランザクションハッシュを検索します。 ハッシュは Transaction hash: で始まる行に出力されています。 トランザクションが承認プロセスを進む様子をリアルタイムで確認できるはずです。

あるいは、 signerPublicKey を検索して、署名者アカウントの履歴内でトランザクションを表示することもできます。

結論⚓︎

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

ステップ 関連ドキュメント
手数料情報の取得 /network/fees/transaction GET
トランザクションの作成 , TransferTransactionV1
トランザクションの署名
トランザクションのアナウンス /transactions PUT
承認の待機 /transactionStatus/{hash} GET

他のトランザクションタイプも、同じ一般的なプロセスに従います。