コンテンツにスキップ

コンプリートアグリゲートトランザクションの作成⚓︎

上級

このチュートリアルでは、コンプリートアグリゲートトランザクション を使用してアセットスワップを作成する方法を説明します。

この例では、アカウント A がアカウント B に 10 XYM を送信し、同時にアカウント B が 1 つのカスタム モザイク をアカウント A に送り返します。

clusterAggregateコンプリートアグリゲートトランザクションclusterT2埋め込み転送 2clusterT1埋め込み転送 1A1アカウント AB1アカウント BA1->B11 カスタムモザイクA2アカウント AB2アカウント BA2->B210 XYM

両者はアナウンス前にオフチェーンで署名を収集するように調整し、スワップが単一のアトミックな トランザクション として実行されることを保証します。

2種類のアグリゲートトランザクション

アグリゲートトランザクション は、複数のトランザクションを単一の操作にグループ化し、関係するすべてのアカウントからの 署名 を必要とします。

コンプリートアグリゲートトランザクションは、アナウンスされる前にすべての署名を収集します。この手順は、例えば以下のようなシナリオでうまく機能します。

  • 複数者間の調整: 当事者がオフチェーンで通信し、トランザクションペイロードと連署(マルチシグ)を交換できる場合。
  • 単一アカウントによるバッチ処理: 1つのアカウントが複数のトランザクションをアトミックに実行したい場合。この場合、連署は不要です。

オフチェーンでの調整が実用的でない場合は、代わりに ボンデッドアグリゲートトランザクションを使用してください。これにより、オンチェーンで連署を追加できるようになります。

前提条件⚓︎

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

また、スワップを完了させるために、 XYM を持つ2つの アカウント と1つのカスタム モザイク が必要です。便宜上、事前に資金供給されたアカウントが提供されていますが、これらはメンテナンスされておらず資金が不足している可能性があります。

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

  • アグリゲートトランザクションを開始するためのアカウント(アカウント A)を、 コード または ウォレット を使用して作成します。
  • スワップに参加するための2つ目のアカウント(アカウント B)を作成します。
  • トランザクション手数料と転送量を支払うための XYM をアカウント A で入手します。 蛇口 (Faucet) からテストネットの通貨を入手する を参照してください。
  • スワップのためにアカウント B が所有するモザイクを作成します。 モザイクの作成 を参照してください。

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

完全なコード⚓︎

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

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


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


# Account A (initiates the aggregate tx and sends XYM to Account B)
ACCOUNT_A_PRIVATE_KEY = os.getenv(
    'ACCOUNT_A_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000000')
account_a_key_pair = SymbolFacade.KeyPair(
    PrivateKey(ACCOUNT_A_PRIVATE_KEY))

# Account B (sends custom mosaic to Account A)
ACCOUNT_B_PRIVATE_KEY = os.getenv(
    'ACCOUNT_B_PRIVATE_KEY',
    '1111111111111111111111111111111111111111111111111111111111111111')
account_b_key_pair = SymbolFacade.KeyPair(
    PrivateKey(ACCOUNT_B_PRIVATE_KEY))

facade = SymbolFacade('testnet')
account_a_address = facade.network.public_key_to_address(
    account_a_key_pair.public_key)
account_b_address = facade.network.public_key_to_address(
    account_b_key_pair.public_key)
print(f'Account A: {account_a_address}')
print(f'Account B: {account_b_address}')

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: Account A transfers 10 XYM to Account B
    embedded_transaction_1 = (
        facade.create_embedded_transaction_from_descriptor(
            {
                'type': 'transfer_transaction_v1',
                'recipient_address': account_b_address,
                'mosaics': [{
                    'mosaic_id': generate_mosaic_alias_id('symbol.xym'),
                    'amount': 10_000_000  # 10 XYM
                }]
            },
            account_a_key_pair.public_key))

    # Embedded tx 2: Account B transfers 1 custom mosaic to Account A
    custom_mosaic_id = 0x6D1314BE751B62C2
    embedded_transaction_2 = (
        facade.create_embedded_transaction_from_descriptor(
            {
                'type': 'transfer_transaction_v1',
                'recipient_address': account_a_address,
                'mosaics': [{
                    'mosaic_id': custom_mosaic_id,
                    'amount': 1  # 1 custom mosaic
                }]
            },
            account_b_key_pair.public_key))

    # Build the aggregate transaction
    embedded_transactions = [
        embedded_transaction_1, embedded_transaction_2]
    transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'aggregate_complete_transaction_v3',
            'transactions_hash': facade.hash_embedded_transactions(
                embedded_transactions),
            'transactions': embedded_transactions
        },
        account_a_key_pair.public_key,
        fee_multiplier,
        2 * 60 * 60,
        1)
    print('Built aggregate transaction without signatures:')
    print(json.dumps(transaction.to_json(), indent=2))

    # --- ACCOUNT A (Initiator) ---
    print('[Account A] Signing the aggregate...')
    signature_a = facade.sign_transaction(
        account_a_key_pair, transaction)
    transaction_payload = facade.transaction_factory.attach_signature(
        transaction, signature_a)
    payload_formatted = json.dumps(
        json.loads(transaction_payload), indent=2)
    print(f'[Account A] Payload ready to share:\n{payload_formatted}')

    # --- OFF-CHAIN COORDINATION ---
    # Account A sends the payload to Account B
    shared_payload = transaction_payload
    print('[Account A] ==> Payload sent to Account B (offchain)')

    # --- ACCOUNT B (Cosignatory) ---
    received_transaction = facade.transaction_factory.deserialize(
        bytes.fromhex(json.loads(shared_payload)['payload']))

    print('[Account B] Cosigning...')
    cosignature_b = facade.cosign_transaction(
        account_b_key_pair, received_transaction)
    cosignature_formatted = json.dumps(cosignature_b.to_json(), indent=2)
    print(f'[Account B] Cosignature created: {cosignature_formatted}')

    # --- OFF-CHAIN COORDINATION ---
    # Account B sends the cosignature back to Account A
    shared_cosignature = cosignature_b
    print(
        '[Account B] <== Cosignature sent back to Account A (offchain)')

    # --- ACCOUNT A (Initiator) ---
    # Add cosignature to the transaction and rebuild payload
    transaction.cosignatures.append(shared_cosignature)
    transaction_payload = facade.transaction_factory.to_json(transaction)
    json_payload = transaction_payload
    print('[Account A] Ready to announce')

    # Announce the transaction
    transaction_hash = facade.hash_transaction(transaction)
    print(f'Transaction hash: {transaction_hash}')
    announce_transaction(json_payload, 'transaction')

    # Wait for confirmation
    wait_for_confirmation(transaction_hash, 'transaction')

except Exception as e:
    print(e)

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

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


// Account A (initiates the aggregate tx and sends XYM to Account B)
const ACCOUNT_A_PRIVATE_KEY = process.env.ACCOUNT_A_PRIVATE_KEY || (
    '0000000000000000000000000000000000000000000000000000000000000000');
const accountAKeyPair = new SymbolFacade.KeyPair(
    new PrivateKey(ACCOUNT_A_PRIVATE_KEY));

// Account B (sends custom mosaic to Account A)
const ACCOUNT_B_PRIVATE_KEY = process.env.ACCOUNT_B_PRIVATE_KEY || (
    '1111111111111111111111111111111111111111111111111111111111111111');
const accountBKeyPair = new SymbolFacade.KeyPair(
    new PrivateKey(ACCOUNT_B_PRIVATE_KEY));

const facade = new SymbolFacade('testnet');
const accountAAddress = facade.network.publicKeyToAddress(
    accountAKeyPair.publicKey);
const accountBAddress = facade.network.publicKeyToAddress(
    accountBKeyPair.publicKey);
console.log('Account A:', accountAAddress.toString());
console.log('Account B:', accountBAddress.toString());

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: Account A transfers 10 XYM to Account B
    const embeddedTransaction1 =
        facade.createEmbeddedTransactionFromTypedDescriptor(
            new descriptors.TransferTransactionV1Descriptor(
                accountBAddress,
                [
                    new descriptors.UnresolvedMosaicDescriptor(
                        generateMosaicAliasId('symbol.xym'),
                        new models.Amount(10_000_000n)) // 10 XYM
                ],
                undefined),
            accountAKeyPair.publicKey);

    // Embedded tx 2: Account B transfers 1 custom mosaic to Account A
    const customMosaicId = 0x6D1314BE751B62C2n;
    const embeddedTransaction2 =
        facade.createEmbeddedTransactionFromTypedDescriptor(
            new descriptors.TransferTransactionV1Descriptor(
                accountAAddress,
                [
                    new descriptors.UnresolvedMosaicDescriptor(
                        customMosaicId,
                        new models.Amount(1n)) // 1 custom mosaic
                ],
                undefined),
            accountBKeyPair.publicKey);

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

    // --- ACCOUNT A (Initiator) ---
    console.log('[Account A] Signing the aggregate...');
    const signatureA = facade.signTransaction(
        accountAKeyPair, transaction);
    const transactionPayload = facade.transactionFactory.static
        .attachSignature(transaction, signatureA);
    const payloadFormatted = JSON.stringify(
        JSON.parse(transactionPayload), null, 2);
    console.log('[Account A] Payload ready to share:\n',
        payloadFormatted);

    // --- OFF-CHAIN COORDINATION ---
    // Account A sends the payload to Account B
    const sharedPayload = transactionPayload;
    console.log('[Account A] ==> Payload sent to Account B (offchain)');

    // --- ACCOUNT B (Cosignatory) ---
    const payloadHex = JSON.parse(sharedPayload).payload;
    const receivedTransaction = facade.transactionFactory.static
        .deserialize(Buffer.from(payloadHex, 'hex'));

    console.log('[Account B] Cosigning...');
    const cosignatureB = facade.cosignTransaction(
        accountBKeyPair, receivedTransaction);
    const cosignatureFormatted = JSON.stringify(
        cosignatureB.toJson(), null, 2);
    console.log('[Account B] Cosignature created:',
        cosignatureFormatted);

    // --- OFF-CHAIN COORDINATION ---
    // Account B sends the cosignature back to Account A
    const sharedCosignature = cosignatureB;
    console.log('[Account B] <== Cosignature sent back to Account A',
        '(offchain)');

    // --- ACCOUNT A (Initiator) ---
    // Add cosignature to the transaction and rebuild payload
    transaction.cosignatures.push(sharedCosignature);
    const transactionPayloadFinal = facade.transactionFactory.static
        .toJson(transaction);
    const jsonPayload = transactionPayloadFinal;
    console.log('[Account A] Ready to announce');

    // Announce the transaction
    const transactionHash =
        facade.hashTransaction(transaction).toString();
    console.log('Transaction hash:', transactionHash);
    await announceTransaction(jsonPayload, 'transaction');

    // Wait for confirmation
    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 CompleteAggregate {
    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 CompleteAggregate().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);

        // Account A (initiates the aggregate tx and sends XYM to
        // Account B)
        final String accountAPrivateKey = System.getenv().getOrDefault(
            "ACCOUNT_A_PRIVATE_KEY", "0".repeat(64));
        final KeyPair accountAKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(accountAPrivateKey));

        // Account B (sends custom mosaic to Account A)
        final String accountBPrivateKey = System.getenv().getOrDefault(
            "ACCOUNT_B_PRIVATE_KEY", "1".repeat(64));
        final KeyPair accountBKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(accountBPrivateKey));

        final Address accountAAddress = facade.network.publicKeyToAddress(
            accountAKeyPair.getPublicKey());
        final Address accountBAddress = facade.network.publicKeyToAddress(
            accountBKeyPair.getPublicKey());
        System.out.printf("Account A: %s%n", accountAAddress);
        System.out.printf("Account B: %s%n", accountBAddress);

        // 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: Account A transfers 10 XYM to Account B
        final EmbeddedTransaction embeddedTransaction1 =
            facade.createEmbeddedTransactionFromTypedDescriptor(
                new TransferTransactionV1Descriptor(
                    accountBAddress,
                    List.of(new UnresolvedMosaicDescriptor(
                        new UnresolvedMosaicId(
                            IdGenerator.generateMosaicAliasId(
                                "symbol.xym")),
                        new Amount(10_000_000))), // 10 XYM
                    null),
                accountAKeyPair.getPublicKey());

        // Embedded tx 2: Account B transfers 1 custom mosaic to Account A
        final long customMosaicId = 0x6D1314BE751B62C2L;
        final EmbeddedTransaction embeddedTransaction2 =
            facade.createEmbeddedTransactionFromTypedDescriptor(
                new TransferTransactionV1Descriptor(
                    accountAAddress,
                    List.of(new UnresolvedMosaicDescriptor(
                        new UnresolvedMosaicId(customMosaicId),
                        new Amount(1))), // 1 custom mosaic
                    null),
                accountBKeyPair.getPublicKey());

        // Build the aggregate transaction
        final List<EmbeddedTransaction> embeddedTransactions =
            List.of(embeddedTransaction1, embeddedTransaction2);
        final AggregateCompleteTransactionV3 transaction =
            (AggregateCompleteTransactionV3) facade
                .createTransactionFromTypedDescriptor(
                    new AggregateCompleteTransactionV3Descriptor(
                        SymbolFacade.hashEmbeddedTransactions(
                            embeddedTransactions),
                        embeddedTransactions,
                        null),
                    accountAKeyPair.getPublicKey(),
                    feeMultiplier,
                    2 * 60 * 60,
                    1);
        System.out.println(
            "Built aggregate transaction without signatures:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));

        // --- ACCOUNT A (Initiator) ---
        System.out.println("[Account A] Signing the aggregate...");
        final CryptoTypes.Signature signatureA = facade.signTransaction(
            accountAKeyPair, transaction);
        final String transactionPayload = SymbolTransactionFactory
            .attachSignature(transaction, signatureA);
        final String payloadFormatted = JSON_MAPPER
            .writerWithDefaultPrettyPrinter()
            .writeValueAsString(JSON_MAPPER.readTree(transactionPayload));
        System.out.printf("[Account A] Payload ready to share:%n %s%n",
            payloadFormatted);

        // --- OFF-CHAIN COORDINATION ---
        // Account A sends the payload to Account B
        final String sharedPayload = transactionPayload;
        System.out.println(
            "[Account A] ==> Payload sent to Account B (offchain)");

        // --- ACCOUNT B (Cosignatory) ---
        final String payloadHex = JSON_MAPPER.readTree(sharedPayload)
            .get("payload").asText();
        final Transaction receivedTransaction =
            SymbolTransactionFactory.deserialize(
                HexFormat.of().parseHex(payloadHex));

        System.out.println("[Account B] Cosigning...");
        final Cosignature cosignatureB = facade.cosignTransaction(
            accountBKeyPair, receivedTransaction);
        final String cosignatureFormatted = JSON_MAPPER
            .writerWithDefaultPrettyPrinter()
            .writeValueAsString(cosignatureB.toJson());
        System.out.printf("[Account B] Cosignature created: %s%n",
            cosignatureFormatted);

        // --- OFF-CHAIN COORDINATION ---
        // Account B sends the cosignature back to Account A
        final Cosignature sharedCosignature = cosignatureB;
        System.out.println("[Account B] <== Cosignature sent back "
            + "to Account A (offchain)");

        // --- ACCOUNT A (Initiator) ---
        // Add cosignature to the transaction and rebuild payload
        transaction.getCosignatures().add(sharedCosignature);
        final String transactionPayloadFinal =
            SymbolTransactionFactory.toJson(transaction);
        final String jsonPayload = transactionPayloadFinal;
        System.out.println("[Account A] Ready to announce");

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

        // Wait for confirmation
        waitForConfirmation(transactionHash, "transaction");

    }
}

Download source

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

コンプリートアグリゲートトランザクションには、2つの異なる役割が含まれる場合があります。アグリゲートを構築してアナウンスする 開始者 (アカウント A)と、オフチェーンでトランザクションペイロードを受け取り、トランザクションを検証した後に署名を追加する1人以上の 連署者 (アカウント B)です。1つのアカウントのみが関与する場合、連署は不要です。

実際には、それぞれの役割は別々のマシンの別々のプログラムとして動作します。このチュートリアルでは複数者が関与するケースを実演しますが、簡略化のため両方の役割を1つのスクリプトにまとめています。

アカウント A: 開始者のワークフロー⚓︎

アカウントの設定⚓︎

# Account A (initiates the aggregate tx and sends XYM to Account B)
ACCOUNT_A_PRIVATE_KEY = os.getenv(
    'ACCOUNT_A_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000000')
account_a_key_pair = SymbolFacade.KeyPair(
    PrivateKey(ACCOUNT_A_PRIVATE_KEY))

# Account B (sends custom mosaic to Account A)
ACCOUNT_B_PRIVATE_KEY = os.getenv(
    'ACCOUNT_B_PRIVATE_KEY',
    '1111111111111111111111111111111111111111111111111111111111111111')
account_b_key_pair = SymbolFacade.KeyPair(
    PrivateKey(ACCOUNT_B_PRIVATE_KEY))

facade = SymbolFacade('testnet')
account_a_address = facade.network.public_key_to_address(
    account_a_key_pair.public_key)
account_b_address = facade.network.public_key_to_address(
    account_b_key_pair.public_key)
print(f'Account A: {account_a_address}')
print(f'Account B: {account_b_address}')
// Account A (initiates the aggregate tx and sends XYM to Account B)
const ACCOUNT_A_PRIVATE_KEY = process.env.ACCOUNT_A_PRIVATE_KEY || (
    '0000000000000000000000000000000000000000000000000000000000000000');
const accountAKeyPair = new SymbolFacade.KeyPair(
    new PrivateKey(ACCOUNT_A_PRIVATE_KEY));

// Account B (sends custom mosaic to Account A)
const ACCOUNT_B_PRIVATE_KEY = process.env.ACCOUNT_B_PRIVATE_KEY || (
    '1111111111111111111111111111111111111111111111111111111111111111');
const accountBKeyPair = new SymbolFacade.KeyPair(
    new PrivateKey(ACCOUNT_B_PRIVATE_KEY));

const facade = new SymbolFacade('testnet');
const accountAAddress = facade.network.publicKeyToAddress(
    accountAKeyPair.publicKey);
const accountBAddress = facade.network.publicKeyToAddress(
    accountBKeyPair.publicKey);
console.log('Account A:', accountAAddress.toString());
console.log('Account B:', accountBAddress.toString());
        // Account A (initiates the aggregate tx and sends XYM to
        // Account B)
        final String accountAPrivateKey = System.getenv().getOrDefault(
            "ACCOUNT_A_PRIVATE_KEY", "0".repeat(64));
        final KeyPair accountAKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(accountAPrivateKey));

        // Account B (sends custom mosaic to Account A)
        final String accountBPrivateKey = System.getenv().getOrDefault(
            "ACCOUNT_B_PRIVATE_KEY", "1".repeat(64));
        final KeyPair accountBKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(accountBPrivateKey));

        final Address accountAAddress = facade.network.publicKeyToAddress(
            accountAKeyPair.getPublicKey());
        final Address accountBAddress = facade.network.publicKeyToAddress(
            accountBKeyPair.getPublicKey());
        System.out.printf("Account A: %s%n", accountAAddress);
        System.out.printf("Account B: %s%n", accountBAddress);

この例では、簡略化のため1つのスクリプトに両方の 秘密鍵 を含めています。実際には、各当事者が自身のマシンで署名します。 アカウント A は、 埋め込みトランザクション の署名者としてアカウント B を設定し、B の アドレス を派生させるために、アカウント B の 公開鍵 のみを必要とします 。

環境変数 ACCOUNT_A_PRIVATE_KEYACCOUNT_B_PRIVATE_KEY で各アカウントの鍵を設定します。提供されない場合は、デフォルトのテストキーが使用されます。自身の鍵を使用する場合は、アカウント A が XYM を持ち、アカウント B がスワップ用のカスタムモザイクを保持していることを確認してください。

両方のアカウントのアドレスは、ファサードのネットワーク設定を使用して公開鍵から派生します。

    # 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: Account A transfers 10 XYM to Account B
    embedded_transaction_1 = (
        facade.create_embedded_transaction_from_descriptor(
            {
                'type': 'transfer_transaction_v1',
                'recipient_address': account_b_address,
                'mosaics': [{
                    'mosaic_id': generate_mosaic_alias_id('symbol.xym'),
                    'amount': 10_000_000  # 10 XYM
                }]
            },
            account_a_key_pair.public_key))

    # Embedded tx 2: Account B transfers 1 custom mosaic to Account A
    custom_mosaic_id = 0x6D1314BE751B62C2
    embedded_transaction_2 = (
        facade.create_embedded_transaction_from_descriptor(
            {
                'type': 'transfer_transaction_v1',
                'recipient_address': account_a_address,
                'mosaics': [{
                    'mosaic_id': custom_mosaic_id,
                    'amount': 1  # 1 custom mosaic
                }]
            },
            account_b_key_pair.public_key))
    // Embedded tx 1: Account A transfers 10 XYM to Account B
    const embeddedTransaction1 =
        facade.createEmbeddedTransactionFromTypedDescriptor(
            new descriptors.TransferTransactionV1Descriptor(
                accountBAddress,
                [
                    new descriptors.UnresolvedMosaicDescriptor(
                        generateMosaicAliasId('symbol.xym'),
                        new models.Amount(10_000_000n)) // 10 XYM
                ],
                undefined),
            accountAKeyPair.publicKey);

    // Embedded tx 2: Account B transfers 1 custom mosaic to Account A
    const customMosaicId = 0x6D1314BE751B62C2n;
    const embeddedTransaction2 =
        facade.createEmbeddedTransactionFromTypedDescriptor(
            new descriptors.TransferTransactionV1Descriptor(
                accountAAddress,
                [
                    new descriptors.UnresolvedMosaicDescriptor(
                        customMosaicId,
                        new models.Amount(1n)) // 1 custom mosaic
                ],
                undefined),
            accountBKeyPair.publicKey);
        // Embedded tx 1: Account A transfers 10 XYM to Account B
        final EmbeddedTransaction embeddedTransaction1 =
            facade.createEmbeddedTransactionFromTypedDescriptor(
                new TransferTransactionV1Descriptor(
                    accountBAddress,
                    List.of(new UnresolvedMosaicDescriptor(
                        new UnresolvedMosaicId(
                            IdGenerator.generateMosaicAliasId(
                                "symbol.xym")),
                        new Amount(10_000_000))), // 10 XYM
                    null),
                accountAKeyPair.getPublicKey());

        // Embedded tx 2: Account B transfers 1 custom mosaic to Account A
        final long customMosaicId = 0x6D1314BE751B62C2L;
        final EmbeddedTransaction embeddedTransaction2 =
            facade.createEmbeddedTransactionFromTypedDescriptor(
                new TransferTransactionV1Descriptor(
                    accountAAddress,
                    List.of(new UnresolvedMosaicDescriptor(
                        new UnresolvedMosaicId(customMosaicId),
                        new Amount(1))), // 1 custom mosaic
                    null),
                accountBKeyPair.getPublicKey());

埋め込みトランザクション は、アトミックに実行される操作を定義します。各埋め込みトランザクションでは以下を指定します。

  • タイプ: すべてのトランザクションタイプをアグリゲート内に埋め込むことができます(他のアグリゲートを除く)。埋め込み転送の場合は、通常の転送トランザクションと同じ TransferTransactionV1 を使用します。
  • 署名者の公開鍵: このトランザクションが単独でアナウンスされた場合に署名するアカウントです。
  • トランザクション固有のフィールド: トランザクションタイプに固有のすべてのフィールドを指定する必要があります。転送の場合は、受信者アドレスと送信するモザイクが含まれます。

埋め込みトランザクションには、手数料(fee)や有効期限(deadline)のフィールドは 含まれない ことに注意してください。これらは、それらを包むアグリゲートトランザクションから継承されます。

この例では、スワップのために2つの転送トランザクションを作成します。

  • 1つ目の転送は、アカウント A からアカウント B へ 10 XYM を送信します。
  • 2つ目の転送は、アカウント B からアカウント A へ 1 つのカスタムモザイクを送信します。

カスタムモザイクについて

ID 0x6D1314BE751B62C2 のカスタムモザイクはこのチュートリアルのために作成されました。デフォルトのアカウント B にはこのモザイクが事前に配布されているため、スワップを正常に実行できます。

自身のアカウントを使用する場合は、アカウント B がカスタムモザイクを保持していることを確認し、コード内のモザイク ID を更新してください。

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

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

埋め込みトランザクションの準備ができたら、トランザクションのディスクリプタからコンプリートアグリゲートトランザクションを作成します。 ディスクリプタには以下のフィールドが含まれます。

  • タイプ: AggregateCompleteTransactionV3 を使用します。
  • トランザクションハッシュ: すべての埋め込みトランザクションから計算されるハッシュです。これにより、署名後に埋め込みトランザクションが変更されないことが保証されます。この値を計算するには SymbolFacade.hash_embedded_transactions を使用します。
  • トランザクション: 実行する埋め込みトランザクションの配列です。

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

トランザクション手数料の共有

署名者が全額を前払いで支払いますが、他の参加者がアグリゲート内で費用を負担することも可能です。詳細は 他のアカウントの代理でのトランザクション手数料の支払い チュートリアルを参照してください。

が、すべての埋め込みトランザクションと1つの連署用に予約されたスペースを含むアグリゲートの総サイズに基づいて手数料を計算します。 予約する連署数は最後の引数として渡し、この例では 1 を指定しています。

トランザクションの署名⚓︎

    # --- ACCOUNT A (Initiator) ---
    print('[Account A] Signing the aggregate...')
    signature_a = facade.sign_transaction(
        account_a_key_pair, transaction)
    transaction_payload = facade.transaction_factory.attach_signature(
        transaction, signature_a)
    payload_formatted = json.dumps(
        json.loads(transaction_payload), indent=2)
    print(f'[Account A] Payload ready to share:\n{payload_formatted}')

    # --- OFF-CHAIN COORDINATION ---
    # Account A sends the payload to Account B
    shared_payload = transaction_payload
    print('[Account A] ==> Payload sent to Account B (offchain)')
    // --- ACCOUNT A (Initiator) ---
    console.log('[Account A] Signing the aggregate...');
    const signatureA = facade.signTransaction(
        accountAKeyPair, transaction);
    const transactionPayload = facade.transactionFactory.static
        .attachSignature(transaction, signatureA);
    const payloadFormatted = JSON.stringify(
        JSON.parse(transactionPayload), null, 2);
    console.log('[Account A] Payload ready to share:\n',
        payloadFormatted);

    // --- OFF-CHAIN COORDINATION ---
    // Account A sends the payload to Account B
    const sharedPayload = transactionPayload;
    console.log('[Account A] ==> Payload sent to Account B (offchain)');
        // --- ACCOUNT A (Initiator) ---
        System.out.println("[Account A] Signing the aggregate...");
        final CryptoTypes.Signature signatureA = facade.signTransaction(
            accountAKeyPair, transaction);
        final String transactionPayload = SymbolTransactionFactory
            .attachSignature(transaction, signatureA);
        final String payloadFormatted = JSON_MAPPER
            .writerWithDefaultPrettyPrinter()
            .writeValueAsString(JSON_MAPPER.readTree(transactionPayload));
        System.out.printf("[Account A] Payload ready to share:%n %s%n",
            payloadFormatted);

        // --- OFF-CHAIN COORDINATION ---
        // Account A sends the payload to Account B
        final String sharedPayload = transactionPayload;
        System.out.println(
            "[Account A] ==> Payload sent to Account B (offchain)");

アカウント A は SDK を使用してトランザクションに署名し、中間ペイロードを生成します。アカウント B の連署が欠けているため、このペイロードはまだアナウンスできる状態ではありません。アカウント A はオフチェーンのチャネルを通じて、この中間ペイロードをアカウント B に送信します。

アグリゲートトランザクションの署名

複数の埋め込みトランザクションで署名者として表示される場合でも、アカウントは一度だけ署名します。このチュートリアルでは、アカウント A がアグリゲートトランザクションに署名しますが、これはアグリゲート自体と、アカウント A が署名者である最初の埋め込みトランザクションの両方をカバーします。

すべての埋め込みトランザクションが同じ署名者を共有する場合(1つのアカウントからの複数の操作をバッチ処理する場合)、連署は 不要 です。アグリゲートは署名後すぐにアナウンスでき、手数料計算で連署用のスペースを予約する必要もありません。

アカウント B: 連署者のワークフロー⚓︎

検証と連署⚓︎

    # --- ACCOUNT B (Cosignatory) ---
    received_transaction = facade.transaction_factory.deserialize(
        bytes.fromhex(json.loads(shared_payload)['payload']))

    print('[Account B] Cosigning...')
    cosignature_b = facade.cosign_transaction(
        account_b_key_pair, received_transaction)
    cosignature_formatted = json.dumps(cosignature_b.to_json(), indent=2)
    print(f'[Account B] Cosignature created: {cosignature_formatted}')

    # --- OFF-CHAIN COORDINATION ---
    # Account B sends the cosignature back to Account A
    shared_cosignature = cosignature_b
    print(
        '[Account B] <== Cosignature sent back to Account A (offchain)')
    // --- ACCOUNT B (Cosignatory) ---
    const payloadHex = JSON.parse(sharedPayload).payload;
    const receivedTransaction = facade.transactionFactory.static
        .deserialize(Buffer.from(payloadHex, 'hex'));

    console.log('[Account B] Cosigning...');
    const cosignatureB = facade.cosignTransaction(
        accountBKeyPair, receivedTransaction);
    const cosignatureFormatted = JSON.stringify(
        cosignatureB.toJson(), null, 2);
    console.log('[Account B] Cosignature created:',
        cosignatureFormatted);

    // --- OFF-CHAIN COORDINATION ---
    // Account B sends the cosignature back to Account A
    const sharedCosignature = cosignatureB;
    console.log('[Account B] <== Cosignature sent back to Account A',
        '(offchain)');
        // --- ACCOUNT B (Cosignatory) ---
        final String payloadHex = JSON_MAPPER.readTree(sharedPayload)
            .get("payload").asText();
        final Transaction receivedTransaction =
            SymbolTransactionFactory.deserialize(
                HexFormat.of().parseHex(payloadHex));

        System.out.println("[Account B] Cosigning...");
        final Cosignature cosignatureB = facade.cosignTransaction(
            accountBKeyPair, receivedTransaction);
        final String cosignatureFormatted = JSON_MAPPER
            .writerWithDefaultPrettyPrinter()
            .writeValueAsString(cosignatureB.toJson());
        System.out.printf("[Account B] Cosignature created: %s%n",
            cosignatureFormatted);

        // --- OFF-CHAIN COORDINATION ---
        // Account B sends the cosignature back to Account A
        final Cosignature sharedCosignature = cosignatureB;
        System.out.println("[Account B] <== Cosignature sent back "
            + "to Account A (offchain)");

アカウント B はペイロードを受け取り、 を使用してデシリアライズし、トランザクションオブジェクトを再構築します。アカウント B は、埋め込みトランザクションが自身が署名しようとしている内容と一致しているか検証する必要があります。

その後、連署します。これによりトランザクションハッシュが計算され、連署オブジェクトが生成されます。この連署のみがアカウント A に送り返されます。

連署する前に検証すること

連署する前に必ずトランザクション内容を検査してください。連署は拘束力を持ち、取り消すことはできません。

アカウント A: アナウンスと承認⚓︎

連署の収集⚓︎

    # --- ACCOUNT A (Initiator) ---
    # Add cosignature to the transaction and rebuild payload
    transaction.cosignatures.append(shared_cosignature)
    transaction_payload = facade.transaction_factory.to_json(transaction)
    json_payload = transaction_payload
    print('[Account A] Ready to announce')
    // --- ACCOUNT A (Initiator) ---
    // Add cosignature to the transaction and rebuild payload
    transaction.cosignatures.push(sharedCosignature);
    const transactionPayloadFinal = facade.transactionFactory.static
        .toJson(transaction);
    const jsonPayload = transactionPayloadFinal;
    console.log('[Account A] Ready to announce');
        // --- ACCOUNT A (Initiator) ---
        // Add cosignature to the transaction and rebuild payload
        transaction.getCosignatures().add(sharedCosignature);
        final String transactionPayloadFinal =
            SymbolTransactionFactory.toJson(transaction);
        final String jsonPayload = transactionPayloadFinal;
        System.out.println("[Account A] Ready to announce");

アカウント A はアカウント B の連署を受け取り、トランザクションオブジェクトの cosignatures 配列に追加し、アナウンス用のペイロードを再構築します。

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

トランザクションのアナウンス準備が整ったので、通常のアグリゲートではないトランザクションと同じプロセスに従います。

    # Announce the transaction
    transaction_hash = facade.hash_transaction(transaction)
    print(f'Transaction hash: {transaction_hash}')
    announce_transaction(json_payload, 'transaction')
    // Announce the transaction
    const transactionHash =
        facade.hashTransaction(transaction).toString();
    console.log('Transaction hash:', transactionHash);
    await announceTransaction(jsonPayload, 'transaction');
        // Announce the transaction
        final String transactionHash =
            facade.hashTransaction(transaction).toString();
        System.out.printf("Transaction hash: %s%n", transactionHash);
        announceTransaction(jsonPayload, "transaction");

すべての署名が収集されたら、 /transactions PUT エンドポイントを使用して ノード にトランザクションをアナウンスします。

ノードは、トランザクションを受け入れる前に、必要なすべての署名が存在し有効であることを検証します。検証に合格すると、トランザクションは 未承認トランザクションプール に追加され、他のノードにブロードキャストされます。

承認の待機⚓︎

    # Wait for confirmation
    wait_for_confirmation(transaction_hash, 'transaction')
    // Wait for confirmation
    await waitForConfirmation(transactionHash, 'transaction');
        // Wait for confirmation
        waitForConfirmation(transactionHash, "transaction");

アナウンス後、 /transactionStatus/{hash} GET を使用してトランザクションステータスを監視します。

ポーリングループは、トランザクションが承認されるか失敗するまで、1秒ごとにステータスを確認します。承認されるとスワップが完了し、両方の転送が実行されます。

出力⚓︎

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

Using node https://reference.symboltest.net:3001
Account A: TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
Account B: TCWYXKVYBMO4NBCUF3AXKJMXCGVSYQOS7ZG2TLI
Fetching recommended fees from /network/fees/transaction
  Fee multiplier: 100
Built aggregate transaction without signatures:
{
  "signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  "signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
  "version": 3,
  "network": 152,
  "type": 16705,
  "fee": "46400",
  "deadline": "96608924742",
  "transactions_hash": "C2F8137E86FA6CD2B5A6E0CBBEA5485DCC427C5F1C9AC98F7A35E84CF0A28877",
  "transactions": [
    {
      "signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
      "version": 1,
      "network": 152,
      "type": 16724,
      "recipient_address": "98AD8BAAB80B1DC684542EC175259711AB2C41D2FE4DA9AD",
      "mosaics": [
        {
          "mosaic_id": "16666583871264174062",
          "amount": "10000000"
        }
      ],
      "message": ""
    },
    {
      "signer_public_key": "D04AB232742BB4AB3A1368BD4615E4E6D0224AB71A016BAF8520A332C9778737",
      "version": 1,
      "network": 152,
      "type": 16724,
      "recipient_address": "988E1191A25A88142C2FB3F69787576E3DC713EFC1CE4DE9",
      "mosaics": [
        {
          "mosaic_id": "7859648582932718274",
          "amount": "1"
        }
      ],
      "message": ""
    }
  ],
  "cosignatures": []
}
[Account A] Signing the aggregate...
[Account A] Payload ready to share:
{
  "payload": "680100000000000068EB60FF9F330E4F86DE7FB30F4E6A398519C23C0AE2EC465524CD03C79382489EB0048EA67FA73E6C68B290BA0BE8A4DDC8DD1911F181C688E10B7EB7625B043B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29000000000398414140B50000000000004634577E16000000C2F8137E86FA6CD2B5A6E0CBBEA5485DCC427C5F1C9AC98F7A35E84CF0A28877C00000000000000060000000000000003B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29000000000198544198AD8BAAB80B1DC684542EC175259711AB2C41D2FE4DA9AD0000010000000000EEAFF441BA994BE780969800000000006000000000000000D04AB232742BB4AB3A1368BD4615E4E6D0224AB71A016BAF8520A332C97787370000000001985441988E1191A25A88142C2FB3F69787576E3DC713EFC1CE4DE90000010000000000C2621B75BE14136D0100000000000000"
}
[Account A] ==> Payload sent to Account B (offchain)
[Account B] Cosigning...
[Account B] Cosignature created: {
  "version": "0",
  "signer_public_key": "D04AB232742BB4AB3A1368BD4615E4E6D0224AB71A016BAF8520A332C9778737",
  "signature": "7037EBED281136D1ADAF2F1CC1721ABC58F6CFC6130C6B6864057BC77F6167AAF31271751E2E93BB0993CC1A99F2A6807C1DE87A16A9DF1C44133652F939A90F"
}
[Account B] <== Cosignature sent back to Account A (offchain)
[Account A] Ready to announce
Transaction hash: 7F8BE244311CEB28C77CBACE81469DDAD231A48C954961F0E04E38A2B7AA2016
Announcing transaction to /transactions
  Response: {"message":"packet 9 was pushed to the network via /transactions"}
Waiting for transaction confirmation...
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  Transaction status: confirmed
transaction confirmed in 14 seconds

出力の主なポイント:

  • 8行目 ("signature": "0000..."): トランザクションがまだ署名されていないため、最初はすべてゼロが表示されています。
  • 12行目 ("type": 16705): これが AggregateCompleteTransactionV3 であることを示します。
  • 16行目 ("transactions"): アトミックに実行される2つの埋め込み転送が含まれています。
  • 46行目 ("cosignatures": []): 最初は空です。アカウント B の連署はアナウンス前に追加されます。
  • 51行目 ("payload": "6801..."): アグリゲートトランザクションとその埋め込みトランザクションから計算されたトランザクションペイロード。
  • 58行目 ("signature": "7037..."): アグリゲートトランザクションに対するアカウント B の連署。
  • 62行目 (Transaction hash): このハッシュは、 Symbol Testnet Explorer でトランザクションを検索するために使用できます。

アグリゲートトランザクションは、ネットワークによって単一のアトミックな単位として扱われます。スワップは完全に実行されるか、トランザクション全体が失敗してアセットが一切転送されないかのどちらかとなります。

結論⚓︎

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

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

次のステップ⚓︎