コンテンツにスキップ

ボンデッドアグリゲートトランザクションの作成⚓︎

上級

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

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

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

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

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

「ボンデッドアグリゲートトランザクション」は、アナウンスされた後にオンチェーンで署名を収集します。これは、オフチェーンでの調整が実用的でない場合に有効です。

例:

  • 共有インフラがない: 当事者間で共通のシステムを通じた調整ができないため、ブロックチェーンを共通のインターフェースとして使用します。
  • 非同期ワークフロー: 連署者が同時に対応できない、またはリアルタイムでの調整ができません。

スパムを防止するため、ボンデッドアグリゲートには「ハッシュロック」が必要です(10 XYM のデポジット)。すべての 連署 が届き、トランザクションが承認されると、ネットワークはこのデポジットを返却します。

当事者がオフチェーンで通信して署名を交換できる場合、コンプリートアグリゲートトランザクション を使用すれば、このデポジットは不要です。

前提条件⚓︎

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

また、スワップを完了させるために、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 transaction
def announce_transaction(payload, endpoint, label):
    print(f'Announcing {label} to {endpoint}')
    request = urllib.request.Request(
        f'{NODE_URL}{endpoint}',
        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 status
def wait_for_status(hash_value, expected_status, label):
    print(f'Waiting for {label} to reach {expected_status} status...')
    attempts = 0
    max_attempts = 60

    while attempts < max_attempts:
        try:
            url = f'{NODE_URL}/transactionStatus/{hash_value}'
            with urllib.request.urlopen(url) as status_response:
                status = json.loads(status_response.read().decode())

                print(f'  Transaction status: {status["group"]}')

                if status['group'] == 'failed':
                    raise RuntimeError(
                        f'{label} failed: {status["code"]}')

                if status['group'] == expected_status:
                    print(f'{label} {expected_status} ' +
                    f'in {attempts} seconds')
                    return

        except urllib.error.HTTPError as err:
            if err.code != 404:
                raise
            print('  Transaction status: not yet available')

        attempts += 1
        time.sleep(1)

    raise TimeoutError(
        f'{label} not {expected_status} after {max_attempts} attempts'
    )


# 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 bonded aggregate transaction
    embedded_transactions = [
        embedded_transaction_1, embedded_transaction_2]
    bonded_transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'aggregate_bonded_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 without signatures:')
    print(json.dumps(bonded_transaction.to_json(), indent=2))

    # --- ACCOUNT A (Initiator) ---
    # Sign the bonded aggregate transaction
    print('[Account A] Signing the bonded aggregate...')
    bonded_signature = facade.sign_transaction(
        account_a_key_pair, bonded_transaction)
    bonded_json_payload = facade.transaction_factory.attach_signature(
        bonded_transaction, bonded_signature)
    bonded_hash = facade.hash_transaction(bonded_transaction)
    print(f'Bonded aggregate transaction hash: {bonded_hash}')

    # Create hash lock transaction
    print('Creating hash lock transaction...')
    hash_lock = facade.create_transaction_from_descriptor(
        {
            'type': 'hash_lock_transaction_v1',
            'mosaic': {
                'mosaic_id': generate_mosaic_alias_id('symbol.xym'),
                'amount': 10_000_000  # 10 XYM deposit
            },
            'duration': 100,  # Lock duration in blocks
            'hash': bonded_hash
        },
        account_a_key_pair.public_key,
        fee_multiplier,
        2 * 60 * 60)

    # Sign hash lock
    print('[Account A] Signing the hash lock...')
    hash_lock_signature = facade.sign_transaction(
        account_a_key_pair, hash_lock)
    hash_lock_payload = facade.transaction_factory.attach_signature(
        hash_lock, hash_lock_signature)
    hash_lock_hash = facade.hash_transaction(hash_lock)
    print(f'Hash lock transaction hash: {hash_lock_hash}')

    # Announce hash lock and wait for confirmation
    announce_transaction(hash_lock_payload, '/transactions', 'Hash lock')
    wait_for_status(hash_lock_hash, 'confirmed', 'Hash lock')

    # Announce bonded aggregate and wait for partial status
    announce_transaction(
        bonded_json_payload, '/transactions/partial',
        'Bonded aggregate transaction'
    )
    wait_for_status(
        bonded_hash, 'partial',
        'Bonded aggregate transaction'
    )

    # --- ACCOUNT B (Cosigner) ---
    # Retrieves partial transactions waiting for signature
    partial_path = f'/transactions/partial?address={account_b_address}'
    print(
        '[Account B] Checking for partial transactions from '
        '/transactions/partial'
    )
    with urllib.request.urlopen(f'{NODE_URL}{partial_path}') as response:
        partial_txs = json.loads(response.read().decode())
        if not partial_txs['data']:
            raise RuntimeError('No partial transactions found')

    print(f'Found {len(partial_txs["data"])} partial transaction(s)')

    # Find the transaction matching the expected hash
    found = any(
        tx['meta']['hash'] == str(bonded_hash)
        for tx in partial_txs['data']
    )
    if not found:
        raise RuntimeError(
            f'Expected transaction {bonded_hash} not found in '
            f'partial transactions')
    print(f'Found matching transaction: {bonded_hash}')

    # Fetch full transaction details using the hash
    detail_path = f'/transactions/partial/{bonded_hash}'
    with urllib.request.urlopen(f'{NODE_URL}{detail_path}') as response:
        partial_tx_json = json.loads(response.read().decode())

    # Verify transaction content before cosigning
    tx_data = partial_tx_json['transaction']
    print(
        f'[Account B] Verifying transaction: '
        f'{len(tx_data["transactions"])} embedded transactions'
    )

    # Submit Account B's cosignature using the transaction hash
    cosignature_path = '/transactions/cosignature'
    print('[Account B] Cosigning the bonded aggregate...')
    cosignature = facade.cosign_transaction_hash(
        account_b_key_pair, bonded_hash, True)
    cosignature_payload = json.dumps({
        'version': str(cosignature.version),
        'signerPublicKey': str(cosignature.signer_public_key),
        'signature': str(cosignature.signature),
        'parentHash': str(cosignature.parent_hash)
    })

    # Announce cosignature
    announce_transaction(
        cosignature_payload, cosignature_path, 'cosignature'
    )

    # Wait for final confirmation
    wait_for_status(
        bonded_hash, 'confirmed',
        'Bonded aggregate transaction'
    )

except Exception as e:
    print(e)

Download source

import { Hash256, 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 transaction
async function announceTransaction(payload, endpoint, label) {
    console.log(`Announcing ${label} to ${endpoint}`);
    const response = await fetch(`${NODE_URL}${endpoint}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: payload
    });
    console.log('  Response:', await response.text());
}

// Helper function to wait for transaction status
async function waitForStatus(hash, expectedStatus, label) {
    console.log(
        `Waiting for ${label} to reach ${expectedStatus} status...`);
    let attempts = 0;
    const maxAttempts = 60;

    while (attempts < maxAttempts) {
        try {
            const url = `${NODE_URL}/transactionStatus/${hash}`;
            const response = await fetch(url);

            if (!response.ok) {
                const error = new Error(
                    `HTTP ${response.status}: ${response.statusText}`);
                error.status = response.status;
                throw error;
            }

            const status = await response.json();

            console.log('  Transaction status:', status.group);

            if ('failed' === status.group)
                throw new Error(`${label} failed: ${status.code}`);

            if (status.group === expectedStatus) {
                console.log(
                    `${label} ${expectedStatus} in ${attempts} seconds`
                );
                return;
            }
        } catch (error) {
            if (404 === error.status)
                console.log('  Transaction status: not yet available');
            else
                throw error;
        }

        attempts++;
        await new Promise(resolve => { setTimeout(resolve, 1000); });
    }

    throw new Error(
        `${label} not ${expectedStatus} after ${maxAttempts} attempts`
    );
}

// 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 bonded aggregate transaction
    const embeddedTransactions = [
        embeddedTransaction1, embeddedTransaction2];
    const bondedTransaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.AggregateBondedTransactionV3Descriptor(
            facade.static.hashEmbeddedTransactions(embeddedTransactions),
            embeddedTransactions,
            undefined),
        accountAKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60,
        1);
    console.log('Built aggregate without signatures:');
    console.log(JSON.stringify(bondedTransaction.toJson(), null, 2));

    // --- ACCOUNT A (Initiator) ---
    // Sign the bonded aggregate transaction
    console.log('[Account A] Signing the bonded aggregate...');
    const bondedSignature = facade.signTransaction(
        accountAKeyPair, bondedTransaction);
    const bondedJsonPayload = facade.transactionFactory.static
        .attachSignature(bondedTransaction, bondedSignature);
    const bondedHash = facade.hashTransaction(
        bondedTransaction).toString();
    console.log('Bonded aggregate transaction hash:', bondedHash);

    // Create hash lock transaction
    console.log('Creating hash lock transaction...');
    const hashLock = facade.createTransactionFromTypedDescriptor(
        new descriptors.HashLockTransactionV1Descriptor(
            new descriptors.UnresolvedMosaicDescriptor(
                generateMosaicAliasId('symbol.xym'),
                new models.Amount(10_000_000n)), // 10 XYM deposit
            new models.BlockDuration(100n),
            new Hash256(bondedHash)),
        accountAKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60);

    // Sign hash lock
    console.log('[Account A] Signing the hash lock...');
    const hashLockSignature = facade.signTransaction(
        accountAKeyPair, hashLock);
    const hashLockPayload = facade.transactionFactory.static
        .attachSignature(hashLock, hashLockSignature);
    const hashLockHash = facade.hashTransaction(hashLock)
        .toString();
    console.log('Hash lock transaction hash:', hashLockHash);

    // Announce hash lock and wait for confirmation
    await announceTransaction(
        hashLockPayload, '/transactions', 'Hash lock'
    );
    await waitForStatus(hashLockHash, 'confirmed', 'Hash lock');

    // Announce bonded aggregate and wait for partial status //
    await announceTransaction(
        bondedJsonPayload, '/transactions/partial',
        'Bonded aggregate transaction'
    );
    await waitForStatus(
        bondedHash, 'partial', 'Bonded aggregate transaction'
    );

    // --- ACCOUNT B (Cosigner) ---
    // Retrieves partial transactions waiting for signature
    const partialPath =
        `/transactions/partial?address=${accountBAddress}`;
    console.log(
        '[Account B] Checking for partial transactions from ' +
        '/transactions/partial'
    );
    const partialResponse = await fetch(`${NODE_URL}${partialPath}`);
    const partialTxs = await partialResponse.json();
    if (!partialTxs.data || 0 === partialTxs.data.length)
        throw new Error('No partial transactions found');

    console.log(
        `Found ${partialTxs.data.length} partial transaction(s)`);

    // Find the transaction matching the expected hash
    const found = partialTxs.data.some(
        tx => tx.meta.hash === bondedHash
    );
    if (!found) {
        throw new Error(
            `Expected transaction ${bondedHash} not found in ` +
            'partial transactions'
        );
    }
    console.log(`Found matching transaction: ${bondedHash}`);

    // Fetch full transaction details using the hash
    const detailPath = `/transactions/partial/${bondedHash}`;
    const detailResponse = await fetch(`${NODE_URL}${detailPath}`);
    const partialTxJson = await detailResponse.json();

    // Verify transaction content before cosigning
    const txData = partialTxJson.transaction;
    console.log('[Account B] Verifying transaction: ' +
        `${txData.transactions.length} embedded transactions`
    );

    // Submit Account B's cosignature using the transaction hash
    const cosignaturePath = '/transactions/cosignature';
    console.log('[Account B] Cosigning the bonded aggregate...');
    const cosignature = SymbolFacade.cosignTransactionHash(
        accountBKeyPair, new Hash256(bondedHash), true
    );
    const cosignaturePayload = JSON.stringify({
        version: cosignature.version.toString(),
        signerPublicKey: cosignature.signerPublicKey.toString(),
        signature: cosignature.signature.toString(),
        parentHash: cosignature.parentHash.toString()
    });

    // Announce cosignature
    await announceTransaction(
        cosignaturePayload, cosignaturePath, 'cosignature'
    );

    // Wait for final confirmation
    await waitForStatus(
        new Hash256(bondedHash), 'confirmed',
        'Bonded aggregate 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.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 BondedAggregate {
    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 KeyPair accountAKeyPair;

    private KeyPair accountBKeyPair;

    private Address accountAAddress;

    private Address accountBAddress;

    // Helper method to announce transaction
    private void announceTransaction(
        final String payload,
        final String endpoint,
        final String label
    ) throws IOException, InterruptedException {
        System.out.printf("Announcing %s to %s%n", label, endpoint);
        final HttpRequest request = HttpRequest.newBuilder(
            URI.create(nodeUrl + endpoint))
            .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 status
    private void waitForStatus(
        final String hash,
        final String expectedStatus,
        final String label
    ) throws IOException, InterruptedException {
        System.out.printf(
            "Waiting for %s to reach %s status...%n",
            label, expectedStatus);
        int attempts = 0;
        final int maxAttempts = 60;

        while (attempts < maxAttempts) {
            final String url = nodeUrl + "/transactionStatus/" + hash;
            final HttpRequest request = HttpRequest.newBuilder(
                URI.create(url)).GET().build();
            final HttpResponse<String> response = HTTP_CLIENT.send(
                request, BodyHandlers.ofString());

            if (response.statusCode() / 100 != 2) {
                if (404 == response.statusCode()) {
                    System.out.println(
                        "  Transaction status: not yet available");
                } else
                    throw new IOException(
                        "HTTP " + response.statusCode());
            } else {
                final JsonNode status = JSON_MAPPER.readTree(
                    response.body());

                System.out.printf("  Transaction status: %s%n",
                    status.get("group").asText());

                if ("failed".equals(status.get("group").asText()))
                    throw new IOException(String.format("%s failed: %s",
                        label, status.get("code").asText()));

                if (status.get("group").asText().equals(expectedStatus)) {
                    System.out.printf("%s %s in %d seconds%n",
                        label, expectedStatus, attempts);
                    return;
                }
            }

            ++attempts;
            Thread.sleep(1000);
        }

        throw new IOException(String.format(
            "%s not %s after %d attempts",
            label, expectedStatus, maxAttempts));
    }

    public static void main(final String[] args) {
        try {
            new BondedAggregate().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));
        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));
        accountBKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(accountBPrivateKey));

        accountAAddress = facade.network.publicKeyToAddress(
            accountAKeyPair.getPublicKey());
        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 bonded aggregate transaction
        final List<EmbeddedTransaction> embeddedTransactions =
            List.of(embeddedTransaction1, embeddedTransaction2);
        final Transaction bondedTransaction =
            facade.createTransactionFromTypedDescriptor(
                new AggregateBondedTransactionV3Descriptor(
                    SymbolFacade.hashEmbeddedTransactions(
                        embeddedTransactions),
                    embeddedTransactions,
                    null),
                accountAKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60,
                1);
        System.out.println("Built aggregate without signatures:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(bondedTransaction.toJson()));

        // --- ACCOUNT A (Initiator) ---
        // Sign the bonded aggregate transaction
        System.out.println("[Account A] Signing the bonded aggregate...");
        final CryptoTypes.Signature bondedSignature =
            facade.signTransaction(accountAKeyPair, bondedTransaction);
        final String bondedJsonPayload = SymbolTransactionFactory
            .attachSignature(bondedTransaction, bondedSignature);
        final String bondedHash = facade.hashTransaction(
            bondedTransaction).toString();
        System.out.printf("Bonded aggregate transaction hash: %s%n",
            bondedHash);

        // Create hash lock transaction
        System.out.println("Creating hash lock transaction...");
        final Transaction hashLock =
            facade.createTransactionFromTypedDescriptor(
                new HashLockTransactionV1Descriptor(
                    new UnresolvedMosaicDescriptor(
                        new UnresolvedMosaicId(
                            IdGenerator.generateMosaicAliasId(
                                "symbol.xym")),
                        new Amount(10_000_000)), // 10 XYM deposit
                    new BlockDuration(100),
                    new CryptoTypes.Hash256(bondedHash)),
                accountAKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60);

        // Sign hash lock
        System.out.println("[Account A] Signing the hash lock...");
        final CryptoTypes.Signature hashLockSignature =
            facade.signTransaction(accountAKeyPair, hashLock);
        final String hashLockPayload = SymbolTransactionFactory
            .attachSignature(hashLock, hashLockSignature);
        final String hashLockHash = facade.hashTransaction(hashLock)
            .toString();
        System.out.printf("Hash lock transaction hash: %s%n",
            hashLockHash);

        // Announce hash lock and wait for confirmation
        announceTransaction(
            hashLockPayload, "/transactions", "Hash lock"
        );
        waitForStatus(hashLockHash, "confirmed", "Hash lock");

        // Announce bonded aggregate and wait for partial status
        announceTransaction(
            bondedJsonPayload, "/transactions/partial",
            "Bonded aggregate transaction"
        );
        waitForStatus(
            bondedHash, "partial", "Bonded aggregate transaction"
        );

        // --- ACCOUNT B (Cosigner) ---
        // Retrieves partial transactions waiting for signature
        final String partialPath =
            "/transactions/partial?address=" + accountBAddress;
        System.out.println(
            "[Account B] Checking for partial transactions from "
            + "/transactions/partial"
        );
        final HttpRequest partialRequest = HttpRequest.newBuilder(
            URI.create(nodeUrl + partialPath)).GET().build();
        final HttpResponse<String> partialResponse = HTTP_CLIENT.send(
            partialRequest, BodyHandlers.ofString());
        final JsonNode partialTxs = JSON_MAPPER.readTree(
            partialResponse.body());
        if (!partialTxs.has("data") || partialTxs.get("data").isEmpty())
            throw new IOException("No partial transactions found");

        System.out.printf("Found %d partial transaction(s)%n",
            partialTxs.get("data").size());

        // Find the transaction matching the expected hash
        boolean found = false;
        for (final JsonNode tx : partialTxs.get("data"))
                found |= bondedHash.equals(
                    tx.get("meta").get("hash").asText());
        if (!found) {
            throw new IOException(
                "Expected transaction " + bondedHash
                + " not found in partial transactions"
            );
        }
        System.out.printf("Found matching transaction: %s%n", bondedHash);

        // Fetch full transaction details using the hash
        final String detailPath = "/transactions/partial/" + bondedHash;
        final HttpRequest detailRequest = HttpRequest.newBuilder(
            URI.create(nodeUrl + detailPath)).GET().build();
        final HttpResponse<String> detailResponse = HTTP_CLIENT.send(
            detailRequest, BodyHandlers.ofString());
        final JsonNode partialTxJson = JSON_MAPPER.readTree(
            detailResponse.body());

        // Verify transaction content before cosigning
        final JsonNode txData = partialTxJson.get("transaction");
        System.out.println("[Account B] Verifying transaction: "
            + txData.get("transactions").size()
            + " embedded transactions"
        );

        // Submit Account B's cosignature using the transaction hash
        final String cosignaturePath = "/transactions/cosignature";
        System.out.println(
            "[Account B] Cosigning the bonded aggregate...");
        final DetachedCosignature cosignature =
            SymbolFacade.cosignTransactionHashDetached(
                accountBKeyPair, new CryptoTypes.Hash256(bondedHash)
            );
        final String cosignaturePayload =
            JSON_MAPPER.writeValueAsString(cosignature.toJson());

        // Announce cosignature
        announceTransaction(
            cosignaturePayload, cosignaturePath, "cosignature"
        );

        // Wait for final confirmation
        waitForStatus(
            bondedHash, "confirmed",
            "Bonded aggregate transaction"
        );
    }
}

Download source

ボンデッドアグリゲートトランザクションには、2つの異なる役割が含まれます。アグリゲートを構築・署名・アナウンスする 開始者(アカウント A)と、保留中のトランザクションをポーリングし、内容を確認した後に署名を追加する1人以上の 連署者(アカウント B)です。

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

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

アカウント 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));
        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));
        accountBKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(accountBPrivateKey));

        accountAAddress = facade.network.publicKeyToAddress(
            accountAKeyPair.getPublicKey());
        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 bonded aggregate transaction
    embedded_transactions = [
        embedded_transaction_1, embedded_transaction_2]
    bonded_transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'aggregate_bonded_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 without signatures:')
    print(json.dumps(bonded_transaction.to_json(), indent=2))
    // Build the bonded aggregate transaction
    const embeddedTransactions = [
        embeddedTransaction1, embeddedTransaction2];
    const bondedTransaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.AggregateBondedTransactionV3Descriptor(
            facade.static.hashEmbeddedTransactions(embeddedTransactions),
            embeddedTransactions,
            undefined),
        accountAKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60,
        1);
    console.log('Built aggregate without signatures:');
    console.log(JSON.stringify(bondedTransaction.toJson(), null, 2));
        // Build the bonded aggregate transaction
        final List<EmbeddedTransaction> embeddedTransactions =
            List.of(embeddedTransaction1, embeddedTransaction2);
        final Transaction bondedTransaction =
            facade.createTransactionFromTypedDescriptor(
                new AggregateBondedTransactionV3Descriptor(
                    SymbolFacade.hashEmbeddedTransactions(
                        embeddedTransactions),
                    embeddedTransactions,
                    null),
                accountAKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60,
                1);
        System.out.println("Built aggregate without signatures:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(bondedTransaction.toJson()));

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

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

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

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

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

ボンデッドトランザクションの署名⚓︎

    # --- ACCOUNT A (Initiator) ---
    # Sign the bonded aggregate transaction
    print('[Account A] Signing the bonded aggregate...')
    bonded_signature = facade.sign_transaction(
        account_a_key_pair, bonded_transaction)
    bonded_json_payload = facade.transaction_factory.attach_signature(
        bonded_transaction, bonded_signature)
    bonded_hash = facade.hash_transaction(bonded_transaction)
    print(f'Bonded aggregate transaction hash: {bonded_hash}')
    // --- ACCOUNT A (Initiator) ---
    // Sign the bonded aggregate transaction
    console.log('[Account A] Signing the bonded aggregate...');
    const bondedSignature = facade.signTransaction(
        accountAKeyPair, bondedTransaction);
    const bondedJsonPayload = facade.transactionFactory.static
        .attachSignature(bondedTransaction, bondedSignature);
    const bondedHash = facade.hashTransaction(
        bondedTransaction).toString();
    console.log('Bonded aggregate transaction hash:', bondedHash);
        // --- ACCOUNT A (Initiator) ---
        // Sign the bonded aggregate transaction
        System.out.println("[Account A] Signing the bonded aggregate...");
        final CryptoTypes.Signature bondedSignature =
            facade.signTransaction(accountAKeyPair, bondedTransaction);
        final String bondedJsonPayload = SymbolTransactionFactory
            .attachSignature(bondedTransaction, bondedSignature);
        final String bondedHash = facade.hashTransaction(
            bondedTransaction).toString();
        System.out.printf("Bonded aggregate transaction hash: %s%n",
            bondedHash);

アカウント A がボンデッドトランザクションに署名し、メインの署名を作成してトランザクションハッシュを確定させます。

このハッシュは、次のステップであるハッシュロックトランザクションの作成に必要です。

ハッシュロックの作成⚓︎

    # Create hash lock transaction
    print('Creating hash lock transaction...')
    hash_lock = facade.create_transaction_from_descriptor(
        {
            'type': 'hash_lock_transaction_v1',
            'mosaic': {
                'mosaic_id': generate_mosaic_alias_id('symbol.xym'),
                'amount': 10_000_000  # 10 XYM deposit
            },
            'duration': 100,  # Lock duration in blocks
            'hash': bonded_hash
        },
        account_a_key_pair.public_key,
        fee_multiplier,
        2 * 60 * 60)

    # Sign hash lock
    print('[Account A] Signing the hash lock...')
    hash_lock_signature = facade.sign_transaction(
        account_a_key_pair, hash_lock)
    hash_lock_payload = facade.transaction_factory.attach_signature(
        hash_lock, hash_lock_signature)
    hash_lock_hash = facade.hash_transaction(hash_lock)
    print(f'Hash lock transaction hash: {hash_lock_hash}')

    # Announce hash lock and wait for confirmation
    announce_transaction(hash_lock_payload, '/transactions', 'Hash lock')
    wait_for_status(hash_lock_hash, 'confirmed', 'Hash lock')
    // Create hash lock transaction
    console.log('Creating hash lock transaction...');
    const hashLock = facade.createTransactionFromTypedDescriptor(
        new descriptors.HashLockTransactionV1Descriptor(
            new descriptors.UnresolvedMosaicDescriptor(
                generateMosaicAliasId('symbol.xym'),
                new models.Amount(10_000_000n)), // 10 XYM deposit
            new models.BlockDuration(100n),
            new Hash256(bondedHash)),
        accountAKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60);

    // Sign hash lock
    console.log('[Account A] Signing the hash lock...');
    const hashLockSignature = facade.signTransaction(
        accountAKeyPair, hashLock);
    const hashLockPayload = facade.transactionFactory.static
        .attachSignature(hashLock, hashLockSignature);
    const hashLockHash = facade.hashTransaction(hashLock)
        .toString();
    console.log('Hash lock transaction hash:', hashLockHash);

    // Announce hash lock and wait for confirmation
    await announceTransaction(
        hashLockPayload, '/transactions', 'Hash lock'
    );
    await waitForStatus(hashLockHash, 'confirmed', 'Hash lock');
        // Create hash lock transaction
        System.out.println("Creating hash lock transaction...");
        final Transaction hashLock =
            facade.createTransactionFromTypedDescriptor(
                new HashLockTransactionV1Descriptor(
                    new UnresolvedMosaicDescriptor(
                        new UnresolvedMosaicId(
                            IdGenerator.generateMosaicAliasId(
                                "symbol.xym")),
                        new Amount(10_000_000)), // 10 XYM deposit
                    new BlockDuration(100),
                    new CryptoTypes.Hash256(bondedHash)),
                accountAKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60);

        // Sign hash lock
        System.out.println("[Account A] Signing the hash lock...");
        final CryptoTypes.Signature hashLockSignature =
            facade.signTransaction(accountAKeyPair, hashLock);
        final String hashLockPayload = SymbolTransactionFactory
            .attachSignature(hashLock, hashLockSignature);
        final String hashLockHash = facade.hashTransaction(hashLock)
            .toString();
        System.out.printf("Hash lock transaction hash: %s%n",
            hashLockHash);

        // Announce hash lock and wait for confirmation
        announceTransaction(
            hashLockPayload, "/transactions", "Hash lock"
        );
        waitForStatus(hashLockHash, "confirmed", "Hash lock");

ボンデッドアグリゲートをアナウンスする前に、ハッシュロックトランザクションを作成し、承認させる必要があります。ハッシュロックは、スパムを防止し、未完了の 部分トランザクション によってネットワークリソースが枯渇しないようにするためのデポジットとして機能します。

ハッシュロックトランザクションでは以下を指定します。

  • タイプ: HashLockTransactionV1 を使用します。

  • モザイク: デポジット額(10 XYM)。このデポジットは連署を待つ間、一時的にロックされます。

  • 期間 (Duration): デポジットがロックされるブロック数(この例では 100 ブロック)。期間が切れる前にすべての連署が収集され、ボンデッドアグリゲートが承認されれば、デポジットは返却されます。そうでない場合は没収されます。

  • ハッシュ: ロック対象となるボンデッドアグリゲートトランザクションのハッシュです。

ハッシュロックは を使用して署名され、ヘルパー関数 announce_transaction を使用してアナウンスされます。ボンデッドアグリゲートをアナウンスするには、ハッシュロックが先に承認されている必要があります。

その後、wait_for_status ヘルパー関数が承認されるまでトランザクションステータスをポーリングします。

ボンデッドトランザクションのアナウンス⚓︎

    # Announce bonded aggregate and wait for partial status
    announce_transaction(
        bonded_json_payload, '/transactions/partial',
        'Bonded aggregate transaction'
    )
    wait_for_status(
        bonded_hash, 'partial',
        'Bonded aggregate transaction'
    )
    // Announce bonded aggregate and wait for partial status //
    await announceTransaction(
        bondedJsonPayload, '/transactions/partial',
        'Bonded aggregate transaction'
    );
    await waitForStatus(
        bondedHash, 'partial', 'Bonded aggregate transaction'
    );
        // Announce bonded aggregate and wait for partial status
        announceTransaction(
            bondedJsonPayload, "/transactions/partial",
            "Bonded aggregate transaction"
        );
        waitForStatus(
            bondedHash, "partial", "Bonded aggregate transaction"
        );

ハッシュロックが承認されたら、ボンデッドアグリゲートを announce_transaction ヘルパーを使用して /transactions/partial PUT にアナウンスします。

ノード はトランザクションを検証し、有効なハッシュロックが存在することを確認して、それを「部分的(partial)」な状態に置きます。wait_for_status ヘルパーはこの状態に達するまでトランザクションを監視し、その時点で連署を収集できるようになります。

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

トランザクションの復元⚓︎

    # --- ACCOUNT B (Cosigner) ---
    # Retrieves partial transactions waiting for signature
    partial_path = f'/transactions/partial?address={account_b_address}'
    print(
        '[Account B] Checking for partial transactions from '
        '/transactions/partial'
    )
    with urllib.request.urlopen(f'{NODE_URL}{partial_path}') as response:
        partial_txs = json.loads(response.read().decode())
        if not partial_txs['data']:
            raise RuntimeError('No partial transactions found')

    print(f'Found {len(partial_txs["data"])} partial transaction(s)')

    # Find the transaction matching the expected hash
    found = any(
        tx['meta']['hash'] == str(bonded_hash)
        for tx in partial_txs['data']
    )
    if not found:
        raise RuntimeError(
            f'Expected transaction {bonded_hash} not found in '
            f'partial transactions')
    print(f'Found matching transaction: {bonded_hash}')
    // --- ACCOUNT B (Cosigner) ---
    // Retrieves partial transactions waiting for signature
    const partialPath =
        `/transactions/partial?address=${accountBAddress}`;
    console.log(
        '[Account B] Checking for partial transactions from ' +
        '/transactions/partial'
    );
    const partialResponse = await fetch(`${NODE_URL}${partialPath}`);
    const partialTxs = await partialResponse.json();
    if (!partialTxs.data || 0 === partialTxs.data.length)
        throw new Error('No partial transactions found');

    console.log(
        `Found ${partialTxs.data.length} partial transaction(s)`);

    // Find the transaction matching the expected hash
    const found = partialTxs.data.some(
        tx => tx.meta.hash === bondedHash
    );
    if (!found) {
        throw new Error(
            `Expected transaction ${bondedHash} not found in ` +
            'partial transactions'
        );
    }
    console.log(`Found matching transaction: ${bondedHash}`);
        // --- ACCOUNT B (Cosigner) ---
        // Retrieves partial transactions waiting for signature
        final String partialPath =
            "/transactions/partial?address=" + accountBAddress;
        System.out.println(
            "[Account B] Checking for partial transactions from "
            + "/transactions/partial"
        );
        final HttpRequest partialRequest = HttpRequest.newBuilder(
            URI.create(nodeUrl + partialPath)).GET().build();
        final HttpResponse<String> partialResponse = HTTP_CLIENT.send(
            partialRequest, BodyHandlers.ofString());
        final JsonNode partialTxs = JSON_MAPPER.readTree(
            partialResponse.body());
        if (!partialTxs.has("data") || partialTxs.get("data").isEmpty())
            throw new IOException("No partial transactions found");

        System.out.printf("Found %d partial transaction(s)%n",
            partialTxs.get("data").size());

        // Find the transaction matching the expected hash
        boolean found = false;
        for (final JsonNode tx : partialTxs.get("data"))
                found |= bondedHash.equals(
                    tx.get("meta").get("hash").asText());
        if (!found) {
            throw new IOException(
                "Expected transaction " + bondedHash
                + " not found in partial transactions"
            );
        }
        System.out.printf("Found matching transaction: %s%n", bondedHash);

トランザクションペイロードがオフチェーンで共有されるコンプリートアグリゲートとは異なり、ボンデッドアグリゲートはオンチェーンでの調整を可能にします。

まず、アカウント B は address パラメータを指定して /transactions/partial GET をポーリングし、自身の署名を待っているトランザクションを探します。これにより、アカウント B が関与している部分的トランザクションのリストが返されます。

この例では、両方のアカウントが同じスクリプトで動作しているため、特定のトランザクションハッシュを探しています。実際には、アカウント B はポーリングによって保留中のトランザクションを発見し、その内容に基づいてどれに連署するかを決定します。

トランザクションの検証⚓︎

    # Fetch full transaction details using the hash
    detail_path = f'/transactions/partial/{bonded_hash}'
    with urllib.request.urlopen(f'{NODE_URL}{detail_path}') as response:
        partial_tx_json = json.loads(response.read().decode())

    # Verify transaction content before cosigning
    tx_data = partial_tx_json['transaction']
    print(
        f'[Account B] Verifying transaction: '
        f'{len(tx_data["transactions"])} embedded transactions'
    )
    // Fetch full transaction details using the hash
    const detailPath = `/transactions/partial/${bondedHash}`;
    const detailResponse = await fetch(`${NODE_URL}${detailPath}`);
    const partialTxJson = await detailResponse.json();

    // Verify transaction content before cosigning
    const txData = partialTxJson.transaction;
    console.log('[Account B] Verifying transaction: ' +
        `${txData.transactions.length} embedded transactions`
    );
        // Fetch full transaction details using the hash
        final String detailPath = "/transactions/partial/" + bondedHash;
        final HttpRequest detailRequest = HttpRequest.newBuilder(
            URI.create(nodeUrl + detailPath)).GET().build();
        final HttpResponse<String> detailResponse = HTTP_CLIENT.send(
            detailRequest, BodyHandlers.ofString());
        final JsonNode partialTxJson = JSON_MAPPER.readTree(
            detailResponse.body());

        // Verify transaction content before cosigning
        final JsonNode txData = partialTxJson.get("transaction");
        System.out.println("[Account B] Verifying transaction: "
            + txData.get("transactions").size()
            + " embedded transactions"
        );

トランザクションが見つかったら、アカウント B はそのハッシュを使用して、/transactions/partial/{transactionId} GET から詳細(埋め込みトランザクションを含む)を完全に取得します。

連署する前に、アカウント B は埋め込みトランザクションが期待される操作と一致しているか検証する必要があります。この例では、単に埋め込みトランザクションの数をログに記録していますが、実際には金額、受信者、モザイクを確認して、スワップ条件が正しいことを確認します。

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

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

トランザクションへの連署⚓︎

    # Submit Account B's cosignature using the transaction hash
    cosignature_path = '/transactions/cosignature'
    print('[Account B] Cosigning the bonded aggregate...')
    cosignature = facade.cosign_transaction_hash(
        account_b_key_pair, bonded_hash, True)
    cosignature_payload = json.dumps({
        'version': str(cosignature.version),
        'signerPublicKey': str(cosignature.signer_public_key),
        'signature': str(cosignature.signature),
        'parentHash': str(cosignature.parent_hash)
    })

    # Announce cosignature
    announce_transaction(
        cosignature_payload, cosignature_path, 'cosignature'
    )
    // Submit Account B's cosignature using the transaction hash
    const cosignaturePath = '/transactions/cosignature';
    console.log('[Account B] Cosigning the bonded aggregate...');
    const cosignature = SymbolFacade.cosignTransactionHash(
        accountBKeyPair, new Hash256(bondedHash), true
    );
    const cosignaturePayload = JSON.stringify({
        version: cosignature.version.toString(),
        signerPublicKey: cosignature.signerPublicKey.toString(),
        signature: cosignature.signature.toString(),
        parentHash: cosignature.parentHash.toString()
    });

    // Announce cosignature
    await announceTransaction(
        cosignaturePayload, cosignaturePath, 'cosignature'
    );
        // Submit Account B's cosignature using the transaction hash
        final String cosignaturePath = "/transactions/cosignature";
        System.out.println(
            "[Account B] Cosigning the bonded aggregate...");
        final DetachedCosignature cosignature =
            SymbolFacade.cosignTransactionHashDetached(
                accountBKeyPair, new CryptoTypes.Hash256(bondedHash)
            );
        final String cosignaturePayload =
            JSON_MAPPER.writeValueAsString(cosignature.toJson());

        // Announce cosignature
        announceTransaction(
            cosignaturePayload, cosignaturePath, "cosignature"
        );

アカウント B は、トランザクションハッシュと detached パラメータを に設定した を使用して、トランザクションに連署します。

デタッチド署名は、ネットワークに単独で送信できるスタンドアロンのオブジェクトです。連署者はノードに直接送信するため、ボンデッドアグリゲートではこれが必要です。

生成されたデタッチド署名のペイロードには以下が含まれます:

  • バージョン: 連署形式のバージョン。
  • 署名者の公開鍵: 誰が連署したかを識別するための、アカウント B の公開鍵。
  • 署名: トランザクションハッシュとアカウント B の秘密鍵から計算された署名。
  • 親ハッシュ: 連署対象となるボンデッドトランザクションのハッシュ。

連署ペイロードは、announce_transaction ヘルパー関数を使用して /transactions/cosignature PUT に送信されます。ネットワークは連署を検証し、それを部分的トランザクションに付加します。

すべての埋め込みトランザクションを満たすのに十分な連署が収集されると、ネットワークは自動的にボンデッドアグリゲートを処理し、ブロックに含めます。

承認の待機⚓︎

    # Wait for final confirmation
    wait_for_status(
        bonded_hash, 'confirmed',
        'Bonded aggregate transaction'
    )
    // Wait for final confirmation
    await waitForStatus(
        new Hash256(bondedHash), 'confirmed',
        'Bonded aggregate transaction'
    );
        // Wait for final confirmation
        waitForStatus(
            bondedHash, "confirmed",
            "Bonded aggregate transaction"
        );

wait_for_status ヘルパー関数が、トランザクションが承認されるか失敗するまで /transactionStatus/{hash} GET をポーリングします。

期限前に必要なすべての連署が収集されれば、トランザクションは承認され、両方の転送が実行され、ハッシュロックのデポジットはアカウント A に返却されます。

期限が切れるか、連署のいずれかが無効な場合、トランザクションは失敗し、デポジットは没収されます。

出力⚓︎

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

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 without signatures:
{
  "signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  "signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
  "version": 3,
  "network": 152,
  "type": 16961,
  "fee": "46400",
  "deadline": "96991154275",
  "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 bonded aggregate...
Bonded aggregate transaction hash: 59CD54BA6EFC220C9C0C8DA6C7615F2A7F23A302836A69ACE29D48569D8CBAF8
Creating hash lock transaction...
[Account A] Signing the hash lock...
Hash lock transaction hash: 8E7AB27A920195BB783F822C39E14A42E2BA965250114E619D011CFE437ACC68
Announcing Hash lock to /transactions
  Response: {"message":"packet 9 was pushed to the network via /transactions"}
Waiting for Hash lock confirmation...
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  Transaction status: confirmed
Hash lock confirmed in 24 seconds
Announcing Bonded aggregate transaction to /transactions/partial
  Response: {"message":"packet 256 was pushed to the network via /transactions/partial"}
Waiting for Bonded aggregate transaction to reach partial status...
  Transaction status: partial
Bonded aggregate transaction partial in 1 seconds
[Account B] Checking for partial transactions from /transactions/partial
Found 1 partial transaction(s)
Found matching transaction: 59CD54BA6EFC220C9C0C8DA6C7615F2A7F23A302836A69ACE29D48569D8CBAF8
[Account B] Verifying transaction: 2 embedded transactions
[Account B] Cosigning the bonded aggregate...
Announcing cosignature to /transactions/cosignature
  Response: {"message":"packet 257 was pushed to the network via /transactions/cosignature"}
Waiting for Bonded aggregate transaction confirmation...
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  Transaction status: confirmed
Bonded aggregate transaction confirmed in 16 seconds

出力の主なポイント:

  • 12行目 ("type": 16961): これが AggregateBondedTransactionV3 であることを示します。
  • 16行目 ("transactions"): アトミックに実行される2つの埋め込み転送が含まれています。
  • 46行目 ("cosignatures": []): 最初は空です。署名はアナウンス後にオンチェーンで送信されます。
  • 49行目 (Bonded aggregate transaction hash:): ハッシュロックの作成とトランザクションのアナウンスに必要な、ボンデッドアグリゲートのハッシュです。
  • 53行目 (Announcing Hash lock to /transactions): ボンデッドアグリゲートの前にハッシュロックをアナウンスし、承認させる必要があります。
  • 61行目 (Announcing Bonded aggregate transaction to /transactions/partial): ボンデッドアグリゲートは通常のトランザクションとは異なるエンドポイントを使用します。
  • 65行目 (Bonded aggregate transaction partial in 1 seconds): ボンデッドアグリゲートは現在、連署がオンチェーンで送信されるのを待っています。
  • 69行目 ([Account B] Verifying transaction: 2 embedded transactions): アカウント B は連署する前にトランザクション内容を検査し、すべての操作に同意できるか確認しています。
  • 71行目 (Announcing cosignature to /transactions/cosignature): 連署がネットワークに送信されます。

アグリゲートトランザクションは、ネットワークによって単一のアトミックな単位として扱われます。スワップは完全に実行される(アカウント A がカスタムモザイクを受け取り、アカウント B が XYM を受け取る)か、トランザクション全体が失敗してアセットが一切転送されないかのどちらかとなります。

結論⚓︎

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

ステップ 関連ドキュメント
埋め込みトランザクションの作成
TransferTransactionV1
アグリゲートの構築
AggregateBondedTransactionV3
ボンデッドトランザクションの署名
ハッシュロックの作成
HashLockTransactionV1
/transactions PUT
ボンデッドトランザクションのアナウンス /transactions/partial PUT
トランザクションの復元 /transactions/partial GET
トランザクションの検証 /transactions/partial/{transactionId} GET
トランザクションへの連署
/transactions/cosignature PUT
承認の待機 /transactionStatus/{hash} GET

次のステップ⚓︎