コンテンツにスキップ

トランザクションのバッチ処理⚓︎

中級

コンプリートアグリゲートトランザクション を使用すると、単一の アカウント からの複数のトランザクションを1つのアトミックな操作に結論、1回の手数料と1回の承認で済ませることができます。

これは、例えば報酬の分配、支払いの分割、または複数のアカウントへの同時資金供給などに役立ちます。

このチュートリアルでは、異なる受信者に XYM を送信する2つの 転送トランザクション をバッチ処理する方法を説明します。

clusterAggregateコンプリートアグリゲートトランザクションclusterT2埋め込み転送 2clusterT1埋め込み転送 1S2署名者R2受信者 2S2->R23 XYMS1署名者R1受信者 1S1->R15 XYM

すべての埋め込みトランザクションが同じ署名者を共有するため、cosignatures:|連署 は必要ありません。 アグリゲートは単一のアカウントによって署名され、アナウンスされます。 複数のアカウントから署名を収集する必要がある例については、コンプリートアグリゲート および ボンデッドアグリゲート のチュートリアルを参照してください。

前提条件⚓︎

開始する前に、開発環境がセットアップされていることを確認してください。 開発環境のセットアップ を参照してください。

また、転送とトランザクション手数料をカバーするのに十分な XYM を持つ アカウントも必要です。 便宜上、事前に資金供給されたテストアカウントが提供していますが、これはメンテナンスされておらず、いつでも資金が不足する可能性があります。

自身のアカウントを使用する場合は、以下の手順を完了してください。

さらに、トランザクションがどのようにアナウンスされ、承認されるかを理解するために、転送トランザクション のチュートリアルを確認してください。

完全なコード⚓︎

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

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
from symbolchain.symbol.Network import Address

NODE_URL = os.getenv('NODE_URL', 'https://reference.symboltest.net:3001')
print(f'Using node {NODE_URL}')


# 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')


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

facade = SymbolFacade('testnet')
signer_address = facade.network.public_key_to_address(
    signer_key_pair.public_key)
print(f'Signer public key: {signer_key_pair.public_key}')
print(f'Signer address: {signer_address}')

RECIPIENT_1 = os.getenv(
    'RECIPIENT_1', 'TCWYXKVYBMO4NBCUF3AXKJMXCGVSYQOS7ZG2TLI')
RECIPIENT_2 = os.getenv(
    'RECIPIENT_2', 'TCD4NC5VIE2EEB3BCV5JRLBNJXYDW5Q5JK547MI')
recipient1_hex = Address(RECIPIENT_1).bytes.hex().upper()
recipient2_hex = Address(RECIPIENT_2).bytes.hex().upper()
print(f'Recipient 1: {RECIPIENT_1} ({recipient1_hex})')
print(f'Recipient 2: {RECIPIENT_2} ({recipient2_hex})')

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}')

    # Embedded tx 1: Send 5 XYM to Recipient 1
    xym_mosaic_id = generate_mosaic_alias_id('symbol.xym')
    embedded_tx_1 = facade.create_embedded_transaction_from_descriptor(
        {
            'type': 'transfer_transaction_v1',
            'recipient_address': Address(RECIPIENT_1),
            'mosaics': [{
                'mosaic_id': xym_mosaic_id,
                'amount': 5_000_000  # 5 XYM
            }]
        },
        signer_key_pair.public_key)

    # Embedded tx 2: Send 3 XYM to Recipient 2
    embedded_tx_2 = facade.create_embedded_transaction_from_descriptor(
        {
            'type': 'transfer_transaction_v1',
            'recipient_address': Address(RECIPIENT_2),
            'mosaics': [{
                'mosaic_id': xym_mosaic_id,
                'amount': 3_000_000  # 3 XYM
            }]
        },
        signer_key_pair.public_key)

    # Build the aggregate transaction
    embedded_transactions = [embedded_tx_1, embedded_tx_2]
    transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'aggregate_complete_transaction_v3',
            'transactions_hash':
                facade.hash_embedded_transactions(embedded_transactions),
            'transactions': embedded_transactions
        },
        signer_key_pair.public_key,
        fee_multiplier,
        2 * 60 * 60)
    print('Built aggregate transaction:')
    print(json.dumps(transaction.to_json(), indent=2))

    # Sign transaction and generate final payload
    signature = facade.sign_transaction(signer_key_pair, transaction)
    json_payload = facade.transaction_factory.attach_signature(
        transaction, signature)

    # Announce the transaction
    announce_transaction(json_payload, 'transaction')

    # Wait for confirmation
    transaction_hash = facade.hash_transaction(transaction)
    print(f'Transaction hash: {transaction_hash}')
    wait_for_confirmation(transaction_hash, 'transaction')

except Exception as e:
    print(e)

Download source

import { PrivateKey } from 'symbol-sdk';
import {
    Address,
    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);

// 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`);
}


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');
const signerAddress = facade.network.publicKeyToAddress(
    signerKeyPair.publicKey);
console.log('Signer public key:', signerKeyPair.publicKey.toString());
console.log('Signer address:', signerAddress.toString());

const RECIPIENT_1 = process.env.RECIPIENT_1 ||
    'TCWYXKVYBMO4NBCUF3AXKJMXCGVSYQOS7ZG2TLI';
const RECIPIENT_2 = process.env.RECIPIENT_2 ||
    'TCD4NC5VIE2EEB3BCV5JRLBNJXYDW5Q5JK547MI';
const recipient1Hex = Buffer.from(
    new Address(RECIPIENT_1).bytes).toString('hex').toUpperCase();
const recipient2Hex = Buffer.from(
    new Address(RECIPIENT_2).bytes).toString('hex').toUpperCase();
console.log(`Recipient 1: ${RECIPIENT_1} (${recipient1Hex})`);
console.log(`Recipient 2: ${RECIPIENT_2} (${recipient2Hex})`);

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);

    // Embedded tx 1: Send 5 XYM to Recipient 1
    const xymMosaicId = generateMosaicAliasId('symbol.xym');
    const embeddedTx1 =
        facade.createEmbeddedTransactionFromTypedDescriptor(
            new descriptors.TransferTransactionV1Descriptor(
                new Address(RECIPIENT_1),
                [
                    new descriptors.UnresolvedMosaicDescriptor(
                        xymMosaicId,
                        new models.Amount(5_000_000n)) // 5 XYM
                ],
                undefined),
            signerKeyPair.publicKey);

    // Embedded tx 2: Send 3 XYM to Recipient 2
    const embeddedTx2 =
        facade.createEmbeddedTransactionFromTypedDescriptor(
            new descriptors.TransferTransactionV1Descriptor(
                new Address(RECIPIENT_2),
                [
                    new descriptors.UnresolvedMosaicDescriptor(
                        xymMosaicId,
                        new models.Amount(3_000_000n)) // 3 XYM
                ],
                undefined),
            signerKeyPair.publicKey);

    // Build the aggregate transaction
    const embeddedTransactions = [embeddedTx1, embeddedTx2];
    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.AggregateCompleteTransactionV3Descriptor(
            facade.static.hashEmbeddedTransactions(embeddedTransactions),
            embeddedTransactions,
            undefined),
        signerKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60);
    console.log('Built aggregate transaction:');
    console.log(JSON.stringify(transaction.toJson(), null, 2));

    // Sign transaction and generate final payload
    const signature = facade.signTransaction(
        signerKeyPair, transaction);
    const jsonPayload = facade.transactionFactory.static
        .attachSignature(transaction, signature);

    // Announce the transaction
    await announceTransaction(jsonPayload, 'transaction');

    // Wait for confirmation
    const transactionHash =
        facade.hashTransaction(transaction).toString();
    console.log('Transaction hash:', transactionHash);
    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.HexFormat;
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.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.*;

public final class TransactionBatching {
    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");

    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());
    }

    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 TransactionBatching().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));

        final Address signerAddress = facade.network.publicKeyToAddress(
            signerKeyPair.getPublicKey());
        System.out.printf("Signer public key: %s%n",
            signerKeyPair.getPublicKey());
        System.out.printf("Signer address: %s%n", signerAddress);

        final String recipient1String = System.getenv().getOrDefault(
            "RECIPIENT_1", "TCWYXKVYBMO4NBCUF3AXKJMXCGVSYQOS7ZG2TLI");
        final String recipient2String = System.getenv().getOrDefault(
            "RECIPIENT_2", "TCD4NC5VIE2EEB3BCV5JRLBNJXYDW5Q5JK547MI");
        final Address recipient1 = new Address(recipient1String);
        final Address recipient2 = new Address(recipient2String);
        final String recipient1Hex = HexFormat.of().formatHex(
            recipient1.bytes()).toUpperCase();
        final String recipient2Hex = HexFormat.of().formatHex(
            recipient2.bytes()).toUpperCase();
        System.out.printf("Recipient 1: %s (%s)%n",
            recipient1String, recipient1Hex);
        System.out.printf("Recipient 2: %s (%s)%n",
            recipient2String, recipient2Hex);

        // 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);

        // Embedded tx 1: Send 5 XYM to Recipient 1
        final long xymMosaicId = IdGenerator.generateMosaicAliasId(
            "symbol.xym");
        final EmbeddedTransaction embeddedTx1 =
            facade.createEmbeddedTransactionFromTypedDescriptor(
                new TransferTransactionV1Descriptor(
                    recipient1,
                    List.of(new UnresolvedMosaicDescriptor(
                        new UnresolvedMosaicId(xymMosaicId),
                        new Amount(5_000_000))), // 5 XYM
                    null),
                signerKeyPair.getPublicKey());

        // Embedded tx 2: Send 3 XYM to Recipient 2
        final EmbeddedTransaction embeddedTx2 =
            facade.createEmbeddedTransactionFromTypedDescriptor(
                new TransferTransactionV1Descriptor(
                    recipient2,
                    List.of(new UnresolvedMosaicDescriptor(
                        new UnresolvedMosaicId(xymMosaicId),
                        new Amount(3_000_000))), // 3 XYM
                    null),
                signerKeyPair.getPublicKey());

        // Build the aggregate transaction
        final List<EmbeddedTransaction> embeddedTransactions =
            List.of(embeddedTx1, embeddedTx2);
        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new AggregateCompleteTransactionV3Descriptor(
                    SymbolFacade.hashEmbeddedTransactions(
                        embeddedTransactions),
                    embeddedTransactions,
                    null),
                signerKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60);
        System.out.println("Built aggregate transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));

        // Sign transaction and generate final payload
        final CryptoTypes.Signature signature = facade.signTransaction(
            signerKeyPair, transaction);
        final String jsonPayload = SymbolTransactionFactory
            .attachSignature(transaction, signature);

        // Announce the transaction
        announceTransaction(jsonPayload, "transaction");

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

Download source

コード解説⚓︎

アカウントの設定⚓︎

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

facade = SymbolFacade('testnet')
signer_address = facade.network.public_key_to_address(
    signer_key_pair.public_key)
print(f'Signer public key: {signer_key_pair.public_key}')
print(f'Signer address: {signer_address}')

RECIPIENT_1 = os.getenv(
    'RECIPIENT_1', 'TCWYXKVYBMO4NBCUF3AXKJMXCGVSYQOS7ZG2TLI')
RECIPIENT_2 = os.getenv(
    'RECIPIENT_2', 'TCD4NC5VIE2EEB3BCV5JRLBNJXYDW5Q5JK547MI')
recipient1_hex = Address(RECIPIENT_1).bytes.hex().upper()
recipient2_hex = Address(RECIPIENT_2).bytes.hex().upper()
print(f'Recipient 1: {RECIPIENT_1} ({recipient1_hex})')
print(f'Recipient 2: {RECIPIENT_2} ({recipient2_hex})')
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');
const signerAddress = facade.network.publicKeyToAddress(
    signerKeyPair.publicKey);
console.log('Signer public key:', signerKeyPair.publicKey.toString());
console.log('Signer address:', signerAddress.toString());

const RECIPIENT_1 = process.env.RECIPIENT_1 ||
    'TCWYXKVYBMO4NBCUF3AXKJMXCGVSYQOS7ZG2TLI';
const RECIPIENT_2 = process.env.RECIPIENT_2 ||
    'TCD4NC5VIE2EEB3BCV5JRLBNJXYDW5Q5JK547MI';
const recipient1Hex = Buffer.from(
    new Address(RECIPIENT_1).bytes).toString('hex').toUpperCase();
const recipient2Hex = Buffer.from(
    new Address(RECIPIENT_2).bytes).toString('hex').toUpperCase();
console.log(`Recipient 1: ${RECIPIENT_1} (${recipient1Hex})`);
console.log(`Recipient 2: ${RECIPIENT_2} (${recipient2Hex})`);
        final String signerPrivateKey = System.getenv().getOrDefault(
            "SIGNER_PRIVATE_KEY", "0".repeat(64));
        final KeyPair signerKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(signerPrivateKey));

        final Address signerAddress = facade.network.publicKeyToAddress(
            signerKeyPair.getPublicKey());
        System.out.printf("Signer public key: %s%n",
            signerKeyPair.getPublicKey());
        System.out.printf("Signer address: %s%n", signerAddress);

        final String recipient1String = System.getenv().getOrDefault(
            "RECIPIENT_1", "TCWYXKVYBMO4NBCUF3AXKJMXCGVSYQOS7ZG2TLI");
        final String recipient2String = System.getenv().getOrDefault(
            "RECIPIENT_2", "TCD4NC5VIE2EEB3BCV5JRLBNJXYDW5Q5JK547MI");
        final Address recipient1 = new Address(recipient1String);
        final Address recipient2 = new Address(recipient2String);
        final String recipient1Hex = HexFormat.of().formatHex(
            recipient1.bytes()).toUpperCase();
        final String recipient2Hex = HexFormat.of().formatHex(
            recipient2.bytes()).toUpperCase();
        System.out.printf("Recipient 1: %s (%s)%n",
            recipient1String, recipient1Hex);
        System.out.printf("Recipient 2: %s (%s)%n",
            recipient2String, recipient2Hex);

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

2つの受信者アドレスは、 RECIPIENT_1 および RECIPIENT_2 環境変数から読み込まれます。 指定されていない場合は、デフォルトでテストアドレスが使用されます。

    # 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);

推奨手数料は、転送トランザクション のチュートリアルで説明されているプロセスに従い、 /network/fees/transaction GET から取得されます。

埋め込みトランザクションの作成⚓︎

    # Embedded tx 1: Send 5 XYM to Recipient 1
    xym_mosaic_id = generate_mosaic_alias_id('symbol.xym')
    embedded_tx_1 = facade.create_embedded_transaction_from_descriptor(
        {
            'type': 'transfer_transaction_v1',
            'recipient_address': Address(RECIPIENT_1),
            'mosaics': [{
                'mosaic_id': xym_mosaic_id,
                'amount': 5_000_000  # 5 XYM
            }]
        },
        signer_key_pair.public_key)

    # Embedded tx 2: Send 3 XYM to Recipient 2
    embedded_tx_2 = facade.create_embedded_transaction_from_descriptor(
        {
            'type': 'transfer_transaction_v1',
            'recipient_address': Address(RECIPIENT_2),
            'mosaics': [{
                'mosaic_id': xym_mosaic_id,
                'amount': 3_000_000  # 3 XYM
            }]
        },
        signer_key_pair.public_key)
    // Embedded tx 1: Send 5 XYM to Recipient 1
    const xymMosaicId = generateMosaicAliasId('symbol.xym');
    const embeddedTx1 =
        facade.createEmbeddedTransactionFromTypedDescriptor(
            new descriptors.TransferTransactionV1Descriptor(
                new Address(RECIPIENT_1),
                [
                    new descriptors.UnresolvedMosaicDescriptor(
                        xymMosaicId,
                        new models.Amount(5_000_000n)) // 5 XYM
                ],
                undefined),
            signerKeyPair.publicKey);

    // Embedded tx 2: Send 3 XYM to Recipient 2
    const embeddedTx2 =
        facade.createEmbeddedTransactionFromTypedDescriptor(
            new descriptors.TransferTransactionV1Descriptor(
                new Address(RECIPIENT_2),
                [
                    new descriptors.UnresolvedMosaicDescriptor(
                        xymMosaicId,
                        new models.Amount(3_000_000n)) // 3 XYM
                ],
                undefined),
            signerKeyPair.publicKey);
        // Embedded tx 1: Send 5 XYM to Recipient 1
        final long xymMosaicId = IdGenerator.generateMosaicAliasId(
            "symbol.xym");
        final EmbeddedTransaction embeddedTx1 =
            facade.createEmbeddedTransactionFromTypedDescriptor(
                new TransferTransactionV1Descriptor(
                    recipient1,
                    List.of(new UnresolvedMosaicDescriptor(
                        new UnresolvedMosaicId(xymMosaicId),
                        new Amount(5_000_000))), // 5 XYM
                    null),
                signerKeyPair.getPublicKey());

        // Embedded tx 2: Send 3 XYM to Recipient 2
        final EmbeddedTransaction embeddedTx2 =
            facade.createEmbeddedTransactionFromTypedDescriptor(
                new TransferTransactionV1Descriptor(
                    recipient2,
                    List.of(new UnresolvedMosaicDescriptor(
                        new UnresolvedMosaicId(xymMosaicId),
                        new Amount(3_000_000))), // 3 XYM
                    null),
                signerKeyPair.getPublicKey());

各転送は、アグリゲート内にラップされる埋め込みトランザクションとして作成されます。 すべての埋め込みトランザクションは同じアカウントから発生するため、同じ signer_public_key を使用します。

この例では、2つの 転送トランザクションを作成します。

  • 最初の転送では、受信者 1 に 5 XYM を送信します。
  • 2番目の転送では、受信者 2 に 3 XYM を送信します。

すべてが同じ署名者を共有している場合でも、各埋め込みトランザクションで signer_public_key が必要です。

埋め込みトランザクションには、手数料や有効期限のフィールドは含まれません。 これらは、それを囲むアグリゲートトランザクションから継承されます。

他のトランザクションタイプのバッチ処理

この例では転送トランザクションをバッチ処理していますが、(他のアグリゲートを除く)任意のトランザクションタイプをアグリゲート内に埋め込むことができます。 例えば、モザイクの作成とネームスペースエイリアスの登録を単一のアトミックな操作としてバッチ処理することができます。

アグリゲートトランザクションの構築⚓︎

    # Build the aggregate transaction
    embedded_transactions = [embedded_tx_1, embedded_tx_2]
    transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'aggregate_complete_transaction_v3',
            'transactions_hash':
                facade.hash_embedded_transactions(embedded_transactions),
            'transactions': embedded_transactions
        },
        signer_key_pair.public_key,
        fee_multiplier,
        2 * 60 * 60)
    print('Built aggregate transaction:')
    print(json.dumps(transaction.to_json(), indent=2))
    // Build the aggregate transaction
    const embeddedTransactions = [embeddedTx1, embeddedTx2];
    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.AggregateCompleteTransactionV3Descriptor(
            facade.static.hashEmbeddedTransactions(embeddedTransactions),
            embeddedTransactions,
            undefined),
        signerKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60);
    console.log('Built aggregate transaction:');
    console.log(JSON.stringify(transaction.toJson(), null, 2));
        // Build the aggregate transaction
        final List<EmbeddedTransaction> embeddedTransactions =
            List.of(embeddedTx1, embeddedTx2);
        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new AggregateCompleteTransactionV3Descriptor(
                    SymbolFacade.hashEmbeddedTransactions(
                        embeddedTransactions),
                    embeddedTransactions,
                    null),
                signerKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60);
        System.out.println("Built aggregate transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));

アグリゲートトランザクションは、トランザクションのディスクリプタから作成します。 ディスクリプタには以下のフィールドが含まれます。

には、署名者の公開鍵、手数料乗数、デッドラインの期間も渡します。 署名者はアグリゲートに署名し、トランザクション手数料を支払います。

が、アグリゲートの合計サイズに基づいて手数料を計算します。 連署は必要ないため、追加の連署数は指定しません。

署名とアナウンス⚓︎

    # Sign transaction and generate final payload
    signature = facade.sign_transaction(signer_key_pair, transaction)
    json_payload = facade.transaction_factory.attach_signature(
        transaction, signature)

    # Announce the transaction
    announce_transaction(json_payload, 'transaction')
    // Sign transaction and generate final payload
    const signature = facade.signTransaction(
        signerKeyPair, transaction);
    const jsonPayload = facade.transactionFactory.static
        .attachSignature(transaction, signature);

    // Announce the transaction
    await announceTransaction(jsonPayload, 'transaction');
        // Sign transaction and generate final payload
        final CryptoTypes.Signature signature = facade.signTransaction(
            signerKeyPair, transaction);
        final String jsonPayload = SymbolTransactionFactory
            .attachSignature(transaction, signature);

        // Announce the transaction
        announceTransaction(jsonPayload, "transaction");

アグリゲートは で署名され、 を使用してペイロードにシリアライズされます。 署名されたペイロードはその後、転送トランザクション チュートリアルで説明されている通常のトランザクションと同じプロセスに従って、 /transactions PUT エンドポイントを使用して ノード にアナウンスされます。

承認の待機⚓︎

    # Wait for confirmation
    transaction_hash = facade.hash_transaction(transaction)
    print(f'Transaction hash: {transaction_hash}')
    wait_for_confirmation(transaction_hash, 'transaction')
    // Wait for confirmation
    const transactionHash =
        facade.hashTransaction(transaction).toString();
    console.log('Transaction hash:', transactionHash);
    await waitForConfirmation(transactionHash, 'transaction');
        // Wait for confirmation
        final String transactionHash =
            facade.hashTransaction(transaction).toString();
        System.out.printf("Transaction hash: %s%n", transactionHash);
        waitForConfirmation(transactionHash, "transaction");

アナウンス後、 /transactionStatus/{hash} GET を使用してトランザクションステータスが監視されます。 ポーリングループは、トランザクションが承認されるか失敗するまで、毎秒ステータスを確認します。

出力⚓︎

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

Using node https://reference.symboltest.net:3001
Signer public key: 3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29
Signer address: TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
Recipient 1: TCWYXKVYBMO4NBCUF3AXKJMXCGVSYQOS7ZG2TLI (98AD8BAAB80B1DC684542EC175259711AB2C41D2FE4DA9AD)
Recipient 2: TCD4NC5VIE2EEB3BCV5JRLBNJXYDW5Q5JK547MI (9887C68BB54134420761157A98AC2D4DF03B761D4ABBCFB1)
Fetching recommended fees from /network/fees/transaction
  Fee multiplier: 100
Built aggregate transaction:
{
  "signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  "signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
  "version": 3,
  "network": 152,
  "type": 16705,
  "fee": "36000",
  "deadline": "107382791542",
  "transactions_hash": "006E8D5F5AC08E7D8EEAB2265569A68FF2921722DCE69D7AA61684A8EE5722C0",
  "transactions": [
    {
      "signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
      "version": 1,
      "network": 152,
      "type": 16724,
      "recipient_address": "98AD8BAAB80B1DC684542EC175259711AB2C41D2FE4DA9AD",
      "mosaics": [
        {
          "mosaic_id": "16666583871264174062",
          "amount": "5000000"
        }
      ],
      "message": ""
    },
    {
      "signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
      "version": 1,
      "network": 152,
      "type": 16724,
      "recipient_address": "9887C68BB54134420761157A98AC2D4DF03B761D4ABBCFB1",
      "mosaics": [
        {
          "mosaic_id": "16666583871264174062",
          "amount": "3000000"
        }
      ],
      "message": ""
    }
  ],
  "cosignatures": []
}
Announcing transaction to /transactions
  Response: {"message":"packet 9 was pushed to the network via /transactions"}
Transaction hash: 5390D8FAC80B9F76275DA857A3A18B6704FEA8B84026C2893F0041326B8C23D2
Waiting for transaction confirmation...
  Transaction status: unconfirmed
  Transaction status: confirmed
transaction confirmed in 6 seconds

出力の主なポイント:

  • 14行目 ("type": 16705): これが AggregateCompleteTransactionV3 であることを識別します。

  • 24行目と38行目 ("recipient_address"): 2つの埋め込み転送は異なるアカウントをターゲットにしています。 これらは、4〜5行目に出力された Base32 アドレスの16進数エンコード形式です。

  • 27-28行目と41-42行目 ("mosaic_id", "amount"): 各転送は XYM(モザイクエイリアス ID 16666583871264174062)を送信します。 このモザイクの 可分性 は 6 であるため、金額 5000000 と 3000000 はそれぞれ 5 および 3 XYM に対応します。

  • 48行目 ("cosignatures": []): すべての埋め込みトランザクションが同じ署名者を共有しているため、空です。 追加の署名は必要ありません。

アグリゲートトランザクションはアトミックに実行されます。つまり、両方の受信者が XYM の転送を受け取るか、どちらも受け取らないかのいずれかになります。

出力されたトランザクションハッシュ(52行目)を使用して、 Symbol Testnet Explorer でトランザクションを検索できます。

結論⚓︎

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

ステップ 関連ドキュメント
埋め込みトランザクションの作成
TransferTransactionV1
アグリゲートの構築
AggregateCompleteTransactionV3
署名とアナウンス

次のステップ⚓︎

  • 連署者の追加: 埋め込みトランザクションに複数の署名者が関与し、アナウンス前にオフチェーンで連署できる場合は、コンプリートアグリゲートのチュートリアルを参照してください。

  • オンチェーンでの署名収集: トランザクションがアナウンスされた後に連署者が署名する必要がある場合は、ボンデッドアグリゲートのチュートリアルを参照してください。

  • 手数料のスポンサー: 他のアカウントの代理での手数料支払い のチュートリアルを使用して、あるアカウントが別のアカウントの代わりにトランザクション手数料を支払うことができるようにします。