コンテンツにスキップ
上級

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

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

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

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

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

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.sc import Amount
from symbolchain.symbol.IdGenerator import generate_mosaic_alias_id
from symbolchain.symbol.Network import NetworkTimestamp

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

# 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 current network time
    time_path = '/node/time'
    print(f'Fetching current network time from {time_path}')
    with urllib.request.urlopen(f'{NODE_URL}{time_path}') as response:
        response_json = json.loads(response.read().decode())
        receive_timestamp = (
            response_json['communicationTimestamps']['receiveTimestamp'])
        timestamp = NetworkTimestamp(int(receive_timestamp))
        print(f'  Network time: {timestamp.timestamp} ms since nemesis')

    # 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_mult = response_json['medianFeeMultiplier']
        minimum_mult = response_json['minFeeMultiplier']
        fee_mult = max(median_mult, minimum_mult)
        print(f'  Fee multiplier: {fee_mult}')

    # Embedded tx 1: Account A transfers 10 XYM to Account B
    embedded_transaction_1 = facade.transaction_factory.create_embedded({
        'type': 'transfer_transaction_v1',
        'signer_public_key': account_a_key_pair.public_key,
        'recipient_address': account_b_address,
        'mosaics': [{
            'mosaic_id': generate_mosaic_alias_id('symbol.xym'),
            'amount': 10_000_000  # 10 XYM (divisibility = 6)
        }]
    })

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

    # Build the aggregate transaction
    embedded_transactions = [
        embedded_transaction_1, embedded_transaction_2]
    transaction = facade.transaction_factory.create({
        'type': 'aggregate_complete_transaction_v3',
        'signer_public_key': account_a_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        'transactions_hash': facade.hash_embedded_transactions(
            embedded_transactions),
        'transactions': embedded_transactions
    })
    # Reserve space for one cosignature (104 bytes)
    # and calculate fee for the final transaction size
    transaction.fee = Amount(fee_mult * (transaction.size + 104))
    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.attach_signature(
        transaction, signature_a)
    json_payload = transaction_payload
    print('[Account A] Ready to announce')

    # Announce the transaction
    announce_path = '/transactions'
    print(f'Announcing transaction to {announce_path}')
    announce_request = urllib.request.Request(
        f'{NODE_URL}{announce_path}',
        data=json_payload.encode(),
        headers={ 'Content-Type': 'application/json' },
        method='PUT'
    )
    with urllib.request.urlopen(announce_request) as response:
        print(f'  Response: {response.read().decode()}')

    # Compute hash of final transaction (with cosignatures)
    transaction_hash = facade.hash_transaction(transaction)

    # Wait for confirmation
    status_path = f'/transactionStatus/{transaction_hash}'
    print(f'Waiting for confirmation from {status_path}')
    for attempt in range(60):
        time.sleep(1)
        try:
            with urllib.request.urlopen(
                f'{NODE_URL}{status_path}'
            ) as response:
                status = json.loads(response.read().decode())
                print(f'  Transaction status: {status['group']}')
                if status['group'] == 'confirmed':
                    print(f'Transaction confirmed in {attempt} seconds')
                    break
                if status['group'] == 'failed':
                    print(f'Transaction failed: {status['code']}')
                    break
        except urllib.error.HTTPError as e:
            print(f'  Transaction status: unknown | Cause: ({e.msg})')
    else:
        print('Confirmation took too long.')

except Exception as e:
    print(e)

Download source

import { PrivateKey } from 'symbol-sdk';
import {
    generateMosaicAliasId,
    models,
    NetworkTimestamp,
    SymbolFacade
} from 'symbol-sdk/symbol';

const NODE_URL = process.env.NODE_URL ||
    'https://reference.symboltest.net:3001';
console.log('Using node', NODE_URL);

// 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 current network time
    const timePath = '/node/time';
    console.log('Fetching current network time from', timePath);
    const timeResponse = await fetch(`${NODE_URL}${timePath}`);
    const timeJSON = await timeResponse.json();
    const timestamp = new NetworkTimestamp(
        timeJSON.communicationTimestamps.receiveTimestamp);
    console.log('  Network time:', timestamp.timestamp,
        'ms since nemesis');

    // 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 medianMult = feeJSON.medianFeeMultiplier;
    const minimumMult = feeJSON.minFeeMultiplier;
    const feeMult = Math.max(medianMult, minimumMult);
    console.log('  Fee multiplier:', feeMult);

    // Embedded tx 1: Account A transfers 10 XYM to Account B
    const embeddedTransaction1 = facade.transactionFactory
        .createEmbedded({
            type: 'transfer_transaction_v1',
            signerPublicKey: accountAKeyPair.publicKey.toString(),
            recipientAddress: accountBAddress.toString(),
            mosaics: [{
                mosaicId: generateMosaicAliasId('symbol.xym'),
                amount: 10_000_000n  // 10 XYM (divisibility = 6)
            }]
        });

    // Embedded tx 2: Account B transfers 1 custom mosaic to Account A
    const customMosaicId = 0x6D1314BE751B62C2n;
    const embeddedTransaction2 = facade.transactionFactory
        .createEmbedded({
            type: 'transfer_transaction_v1',
            signerPublicKey: accountBKeyPair.publicKey.toString(),
            recipientAddress: accountAAddress.toString(),
            mosaics: [{
                mosaicId: customMosaicId,
                amount: 1n  // 1 custom mosaic (divisibility = 0)
            }]
        });

    // Build the aggregate transaction
    const embeddedTransactions = [
        embeddedTransaction1, embeddedTransaction2];
    const transaction = facade.transactionFactory.create({
        type: 'aggregate_complete_transaction_v3',
        signerPublicKey: accountAKeyPair.publicKey.toString(),
        deadline: timestamp.addHours(2).timestamp,
        transactionsHash: facade.static.hashEmbeddedTransactions(
            embeddedTransactions),
        transactions: embeddedTransactions
    });
    // Reserve space for one cosignature (104 bytes)
    // and calculate fee for the final transaction size
    transaction.fee = new models.Amount(
        feeMult * (transaction.size + 104)
    );
    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
        .attachSignature(transaction, signatureA);
    const jsonPayload = transactionPayloadFinal;
    console.log('[Account A] Ready to announce');

    // Announce the transaction
    const announcePath = '/transactions';
    console.log('Announcing transaction to', announcePath);
    const announceResponse = await fetch(`${NODE_URL}${announcePath}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: jsonPayload
    });
    console.log('  Response:', await announceResponse.text());

    // Compute hash of final transaction (with cosignatures)
    const transactionHash =
        facade.hashTransaction(transaction).toString();

    // Wait for confirmation
    const statusPath = `/transactionStatus/${transactionHash}`;
    console.log('Waiting for confirmation from', statusPath);

    for (let attempt = 0; attempt < 60; attempt++) {
        await new Promise(resolve => setTimeout(resolve, 1000));
        try {
            const statusResponse = await fetch(
                `${NODE_URL}${statusPath}`);
            const status = await statusResponse.json();
            console.log('  Transaction status:', status.group);
            if (status.group === 'confirmed') {
                console.log('Transaction confirmed in', attempt,
                    'seconds');
                break;
            }
            if (status.group === 'failed') {
                console.log('Transaction failed:', status.code);
                break;
            }
        } catch (e) {
            console.log('  Transaction status: unknown | Cause:',
                e.message);
        }
    }
} catch (e) {
    console.error(e.message, '| Cause:', e.cause?.code ?? 'unknown');
}

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

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

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

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

ネットワーク時間と手数料の取得⚓︎

    # Fetch current network time
    time_path = '/node/time'
    print(f'Fetching current network time from {time_path}')
    with urllib.request.urlopen(f'{NODE_URL}{time_path}') as response:
        response_json = json.loads(response.read().decode())
        receive_timestamp = (
            response_json['communicationTimestamps']['receiveTimestamp'])
        timestamp = NetworkTimestamp(int(receive_timestamp))
        print(f'  Network time: {timestamp.timestamp} ms since nemesis')

    # 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_mult = response_json['medianFeeMultiplier']
        minimum_mult = response_json['minFeeMultiplier']
        fee_mult = max(median_mult, minimum_mult)
        print(f'  Fee multiplier: {fee_mult}')
    // Fetch current network time
    const timePath = '/node/time';
    console.log('Fetching current network time from', timePath);
    const timeResponse = await fetch(`${NODE_URL}${timePath}`);
    const timeJSON = await timeResponse.json();
    const timestamp = new NetworkTimestamp(
        timeJSON.communicationTimestamps.receiveTimestamp);
    console.log('  Network time:', timestamp.timestamp,
        'ms since nemesis');

    // 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 medianMult = feeJSON.medianFeeMultiplier;
    const minimumMult = feeJSON.minFeeMultiplier;
    const feeMult = Math.max(medianMult, minimumMult);
    console.log('  Fee multiplier:', feeMult);

ネットワーク時間と推奨手数料をそれぞれ /node/time GET および /network/fees/transaction GET から取得します。

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

    # Embedded tx 1: Account A transfers 10 XYM to Account B
    embedded_transaction_1 = facade.transaction_factory.create_embedded({
        'type': 'transfer_transaction_v1',
        'signer_public_key': account_a_key_pair.public_key,
        'recipient_address': account_b_address,
        'mosaics': [{
            'mosaic_id': generate_mosaic_alias_id('symbol.xym'),
            'amount': 10_000_000  # 10 XYM (divisibility = 6)
        }]
    })

    ## Embedded tx 2: Account B transfers 1 custom mosaic to Account A
    custom_mosaic_id = 0x6D1314BE751B62C2
    embedded_transaction_2 = facade.transaction_factory.create_embedded({
        'type': 'transfer_transaction_v1',
        'signer_public_key': account_b_key_pair.public_key,
        'recipient_address': account_a_address,
        'mosaics': [{
            'mosaic_id': custom_mosaic_id,
            'amount': 1  # 1 custom mosaic (divisibility = 0)
        }]
    })
    // Embedded tx 1: Account A transfers 10 XYM to Account B
    const embeddedTransaction1 = facade.transactionFactory
        .createEmbedded({
            type: 'transfer_transaction_v1',
            signerPublicKey: accountAKeyPair.publicKey.toString(),
            recipientAddress: accountBAddress.toString(),
            mosaics: [{
                mosaicId: generateMosaicAliasId('symbol.xym'),
                amount: 10_000_000n  // 10 XYM (divisibility = 6)
            }]
        });

    // Embedded tx 2: Account B transfers 1 custom mosaic to Account A
    const customMosaicId = 0x6D1314BE751B62C2n;
    const embeddedTransaction2 = facade.transactionFactory
        .createEmbedded({
            type: 'transfer_transaction_v1',
            signerPublicKey: accountBKeyPair.publicKey.toString(),
            recipientAddress: accountAAddress.toString(),
            mosaics: [{
                mosaicId: customMosaicId,
                amount: 1n  // 1 custom mosaic (divisibility = 0)
            }]
        });

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

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

埋め込みトランザクションには、手数料(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.transaction_factory.create({
        'type': 'aggregate_complete_transaction_v3',
        'signer_public_key': account_a_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        'transactions_hash': facade.hash_embedded_transactions(
            embedded_transactions),
        'transactions': embedded_transactions
    })
    # Reserve space for one cosignature (104 bytes)
    # and calculate fee for the final transaction size
    transaction.fee = Amount(fee_mult * (transaction.size + 104))
    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.transactionFactory.create({
        type: 'aggregate_complete_transaction_v3',
        signerPublicKey: accountAKeyPair.publicKey.toString(),
        deadline: timestamp.addHours(2).timestamp,
        transactionsHash: facade.static.hashEmbeddedTransactions(
            embeddedTransactions),
        transactions: embeddedTransactions
    });
    // Reserve space for one cosignature (104 bytes)
    // and calculate fee for the final transaction size
    transaction.fee = new models.Amount(
        feeMult * (transaction.size + 104)
    );
    console.log('Built aggregate transaction without signatures:');
    console.log(JSON.stringify(transaction.toJson(), null, 2));

埋め込みトランザクションの準備ができたら、それらをラップするアグリゲートコンプリートトランザクションを作成します。

  • タイプ: aggregate_complete_transaction_v3 を使用します。
  • 署名者の公開鍵: アグリゲートを開始するアカウントです。このアカウントがトランザクションをアナウンスし、トランザクション手数料を支払います。

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

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

  • 有効期限 (Deadline): タイムスタンプです。これ以降はトランザクションが失効し、承認されなくなります。

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

手数料は、アグリゲートの総サイズに基づいて計算されます。これには、すべての埋め込みトランザクションと、1つの連署(104バイト)用に予約されたスペースが含まれます。

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

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

アカウント 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)');

アカウント 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.attach_signature(
        transaction, signature_a)
    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
        .attachSignature(transaction, signatureA);
    const jsonPayload = transactionPayloadFinal;
    console.log('[Account A] Ready to announce');

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

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

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

    # Announce the transaction
    announce_path = '/transactions'
    print(f'Announcing transaction to {announce_path}')
    announce_request = urllib.request.Request(
        f'{NODE_URL}{announce_path}',
        data=json_payload.encode(),
        headers={ 'Content-Type': 'application/json' },
        method='PUT'
    )
    with urllib.request.urlopen(announce_request) as response:
        print(f'  Response: {response.read().decode()}')

    # Compute hash of final transaction (with cosignatures)
    transaction_hash = facade.hash_transaction(transaction)
    // Announce the transaction
    const announcePath = '/transactions';
    console.log('Announcing transaction to', announcePath);
    const announceResponse = await fetch(`${NODE_URL}${announcePath}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: jsonPayload
    });
    console.log('  Response:', await announceResponse.text());

    // Compute hash of final transaction (with cosignatures)
    const transactionHash =
        facade.hashTransaction(transaction).toString();

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

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

承認の待機⚓︎

    # Wait for confirmation
    status_path = f'/transactionStatus/{transaction_hash}'
    print(f'Waiting for confirmation from {status_path}')
    for attempt in range(60):
        time.sleep(1)
        try:
            with urllib.request.urlopen(
                f'{NODE_URL}{status_path}'
            ) as response:
                status = json.loads(response.read().decode())
                print(f'  Transaction status: {status['group']}')
                if status['group'] == 'confirmed':
                    print(f'Transaction confirmed in {attempt} seconds')
                    break
                if status['group'] == 'failed':
                    print(f'Transaction failed: {status['code']}')
                    break
        except urllib.error.HTTPError as e:
            print(f'  Transaction status: unknown | Cause: ({e.msg})')
    else:
        print('Confirmation took too long.')
    // Wait for confirmation
    const statusPath = `/transactionStatus/${transactionHash}`;
    console.log('Waiting for confirmation from', statusPath);

    for (let attempt = 0; attempt < 60; attempt++) {
        await new Promise(resolve => setTimeout(resolve, 1000));
        try {
            const statusResponse = await fetch(
                `${NODE_URL}${statusPath}`);
            const status = await statusResponse.json();
            console.log('  Transaction status:', status.group);
            if (status.group === 'confirmed') {
                console.log('Transaction confirmed in', attempt,
                    'seconds');
                break;
            }
            if (status.group === 'failed') {
                console.log('Transaction failed:', status.code);
                break;
            }
        } catch (e) {
            console.log('  Transaction status: unknown | Cause:',
                e.message);
        }
    }

アナウンス後、トランザクションステータスを監視します。

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

出力⚓︎

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

Using node https://reference.symboltest.net:3001
Account A: TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
Account B: TCWYXKVYBMO4NBCUF3AXKJMXCGVSYQOS7ZG2TLI
Fetching current network time from /node/time
  Network time: 96601724742 ms since nemesis
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
Announcing transaction to /transactions
  Response: {"message":"packet 9 was pushed to the network via /transactions"}
Waiting for confirmation from /transactionStatus/7F8BE244311CEB28C77CBACE81469DDAD231A48C954961F0E04E38A2B7AA2016
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  Transaction status: confirmed
Transaction confirmed in 14 seconds

出力の主なポイント:

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

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

結論⚓︎

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

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

次のステップ⚓︎