Skip to content

Batching Transactions⚓︎

A complete aggregate transaction can bundle multiple transactions from a single account into one atomic operation, with one fee and one confirmation.

This is useful for distributing rewards, splitting payments, or funding several accounts at once, for example.

This tutorial shows how to batch two transfer transactions that send XYM to different recipients.

%3clusterAggregateAggregate Complete TransactionclusterT1Embedded Transfer 1clusterT2Embedded Transfer 2S1SignerR1Recipient 1S1->R15 XYMS2SignerR2Recipient 2S2->R23 XYM

Because all embedded transactions share the same signer, no cosignatures are needed. The aggregate can be signed and announced by a single account. For examples requiring the collection of signatures from multiple accounts, see the Complete Aggregate and Bonded Aggregate tutorials.

Prerequisites⚓︎

Before you start, make sure to set up your development environment. See Setting Up a Development Environment.

You also need an account with enough XYM to cover the transfers and the transaction fee. Although a pre-funded test account is provided for convenience, it is not maintained and may run out of funds at any time.

To use your own account, complete the following steps:

Additionally, review the Transfer transaction tutorial to understand how transactions are announced and confirmed.

Full Code⚓︎

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 Address, NetworkTimestamp

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

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

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

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

try:
    # Fetch 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: Send 5 XYM to Recipient 1
    xym_mosaic_id = generate_mosaic_alias_id('symbol.xym')
    embedded_tx_1 = facade.transaction_factory.create_embedded({
        'type': 'transfer_transaction_v1',
        'signer_public_key': signer_key_pair.public_key,
        'recipient_address': RECIPIENT_1,
        'mosaics': [{
            'mosaic_id': xym_mosaic_id,
            'amount': 5_000_000  # 5 XYM
        }]
    })

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

    # Build the aggregate transaction
    embedded_transactions = [embedded_tx_1, embedded_tx_2]
    transaction = facade.transaction_factory.create({
        'type': 'aggregate_complete_transaction_v3',
        'signer_public_key': signer_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        'transactions_hash':
            facade.hash_embedded_transactions(embedded_transactions),
        'transactions': embedded_transactions
    })
    transaction.fee = Amount(fee_mult * transaction.size)
    print('Built aggregate transaction:')
    print(json.dumps(transaction.to_json(), indent=2))

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

    # Announce the transaction
    announce_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()}')

    # Wait for confirmation
    transaction_hash = facade.hash_transaction(transaction)
    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 {
    Address,
    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);

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

const facade = new SymbolFacade('testnet');
const signerAddress = facade.network.publicKeyToAddress(
    signerKeyPair.publicKey);
console.log('Signer public key:', signerKeyPair.publicKey.toString());
console.log('Signer address:', signerAddress.toString());

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

try {
    // Fetch 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: Send 5 XYM to Recipient 1
    const xymMosaicId = generateMosaicAliasId('symbol.xym');
    const embeddedTx1 = facade.transactionFactory.createEmbedded({
        type: 'transfer_transaction_v1',
        signerPublicKey: signerKeyPair.publicKey.toString(),
        recipientAddress: RECIPIENT_1,
        mosaics: [{
            mosaicId: xymMosaicId,
            amount: 5_000_000n  // 5 XYM
        }]
    });

    // Embedded tx 2: Send 3 XYM to Recipient 2
    const embeddedTx2 = facade.transactionFactory.createEmbedded({
        type: 'transfer_transaction_v1',
        signerPublicKey: signerKeyPair.publicKey.toString(),
        recipientAddress: RECIPIENT_2,
        mosaics: [{
            mosaicId: xymMosaicId,
            amount: 3_000_000n  // 3 XYM
        }]
    });

    // Build the aggregate transaction
    const embeddedTransactions = [embeddedTx1, embeddedTx2];
    const transaction = facade.transactionFactory.create({
        type: 'aggregate_complete_transaction_v3',
        signerPublicKey: signerKeyPair.publicKey.toString(),
        deadline: timestamp.addHours(2).timestamp,
        transactionsHash: facade.static.hashEmbeddedTransactions(
            embeddedTransactions),
        transactions: embeddedTransactions
    });
    transaction.fee = new models.Amount(
        feeMult * transaction.size);
    console.log('Built aggregate transaction:');
    console.log(JSON.stringify(transaction.toJson(), null, 2));

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

    // Announce the transaction
    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());

    // Wait for confirmation
    const transactionHash =
        facade.hashTransaction(transaction).toString();
    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

Code Explanation⚓︎

Setting Up the Account⚓︎

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

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

RECIPIENT_1 = os.getenv(
    'RECIPIENT_1', 'TCWYXKVYBMO4NBCUF3AXKJMXCGVSYQOS7ZG2TLI')
RECIPIENT_2 = os.getenv(
    'RECIPIENT_2', 'TCD4NC5VIE2EEB3BCV5JRLBNJXYDW5Q5JK547MI')
recipient1_hex = Address(RECIPIENT_1).bytes.hex().upper()
recipient2_hex = Address(RECIPIENT_2).bytes.hex().upper()
print(f'Recipient 1: {RECIPIENT_1} ({recipient1_hex})')
print(f'Recipient 2: {RECIPIENT_2} ({recipient2_hex})')
const SIGNER_PRIVATE_KEY =
    process.env.SIGNER_PRIVATE_KEY ||
    '0000000000000000000000000000000000000000000000000000000000000000';
const signerKeyPair = new SymbolFacade.KeyPair(
    new PrivateKey(SIGNER_PRIVATE_KEY));

const facade = new SymbolFacade('testnet');
const signerAddress = facade.network.publicKeyToAddress(
    signerKeyPair.publicKey);
console.log('Signer public key:', signerKeyPair.publicKey.toString());
console.log('Signer address:', signerAddress.toString());

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

The signer account is loaded from the SIGNER_PRIVATE_KEY environment variable. If not provided, a test key is used as default.

The two recipient addresses are loaded from the RECIPIENT_1 and RECIPIENT_2 environment variables. If not provided, test addresses are used as defaults.

Fetching Network Time and Fees⚓︎

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

Network time and recommended fees are fetched from /node/time GET and /network/fees/transaction GET respectively, following the process described in the Transfer Transaction tutorial.

Creating Embedded Transactions⚓︎

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

    # Embedded tx 2: Send 3 XYM to Recipient 2
    embedded_tx_2 = facade.transaction_factory.create_embedded({
        'type': 'transfer_transaction_v1',
        'signer_public_key': signer_key_pair.public_key,
        'recipient_address': RECIPIENT_2,
        'mosaics': [{
            'mosaic_id': xym_mosaic_id,
            'amount': 3_000_000  # 3 XYM
        }]
    })
    // Embedded tx 1: Send 5 XYM to Recipient 1
    const xymMosaicId = generateMosaicAliasId('symbol.xym');
    const embeddedTx1 = facade.transactionFactory.createEmbedded({
        type: 'transfer_transaction_v1',
        signerPublicKey: signerKeyPair.publicKey.toString(),
        recipientAddress: RECIPIENT_1,
        mosaics: [{
            mosaicId: xymMosaicId,
            amount: 5_000_000n  // 5 XYM
        }]
    });

    // Embedded tx 2: Send 3 XYM to Recipient 2
    const embeddedTx2 = facade.transactionFactory.createEmbedded({
        type: 'transfer_transaction_v1',
        signerPublicKey: signerKeyPair.publicKey.toString(),
        recipientAddress: RECIPIENT_2,
        mosaics: [{
            mosaicId: xymMosaicId,
            amount: 3_000_000n  // 3 XYM
        }]
    });

Each transfer is created as an embedded transaction that will be wrapped inside the aggregate. All embedded transactions use the same signer_public_key because they all originate from the same account.

The example creates two transfer transactions:

  • The first transfer sends 5 XYM to Recipient 1.
  • The second transfer sends 3 XYM to Recipient 2.

The signer_public_key is still required on each embedded transaction, even when all share the same signer.

Embedded transactions do not include fee or deadline fields. These are inherited from the enclosing aggregate transaction.

Batching other transaction types

Although this example batches transfer transactions, any transaction type can be embedded within an aggregate (except other aggregates). For example, you could batch mosaic creation with a namespace alias registration in a single atomic operation.

Building the Aggregate Transaction⚓︎

    # Build the aggregate transaction
    embedded_transactions = [embedded_tx_1, embedded_tx_2]
    transaction = facade.transaction_factory.create({
        'type': 'aggregate_complete_transaction_v3',
        'signer_public_key': signer_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        'transactions_hash':
            facade.hash_embedded_transactions(embedded_transactions),
        'transactions': embedded_transactions
    })
    transaction.fee = Amount(fee_mult * transaction.size)
    print('Built aggregate transaction:')
    print(json.dumps(transaction.to_json(), indent=2))
    // Build the aggregate transaction
    const embeddedTransactions = [embeddedTx1, embeddedTx2];
    const transaction = facade.transactionFactory.create({
        type: 'aggregate_complete_transaction_v3',
        signerPublicKey: signerKeyPair.publicKey.toString(),
        deadline: timestamp.addHours(2).timestamp,
        transactionsHash: facade.static.hashEmbeddedTransactions(
            embeddedTransactions),
        transactions: embeddedTransactions
    });
    transaction.fee = new models.Amount(
        feeMult * transaction.size);
    console.log('Built aggregate transaction:');
    console.log(JSON.stringify(transaction.toJson(), null, 2));

The aggregate transaction wraps all embedded transactions:

The fee is calculated based on the aggregate's total size. Since no cosignatures are needed, there is no need to reserve extra space for cosignature bytes.

Signing and Announcing⚓︎

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

    # Announce the transaction
    announce_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()}')
    // Sign transaction and generate final payload
    const signature = facade.signTransaction(
        signerKeyPair, transaction);
    const jsonPayload = facade.transactionFactory.static
        .attachSignature(transaction, signature);

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

The aggregate is signed with and serialized into a payload using . The signed payload is then announced to a node using the /transactions PUT endpoint, following the same process as regular transactions described in the Transfer Transaction tutorial.

Waiting for Confirmation⚓︎

    # Wait for confirmation
    transaction_hash = facade.hash_transaction(transaction)
    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 transactionHash =
        facade.hashTransaction(transaction).toString();
    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);
        }
    }

After announcement, the transaction status is monitored using /transactionStatus/{hash} GET. The polling loop checks the status every second until the transaction is confirmed or fails.

Output⚓︎

The output shown below corresponds to a typical run of the program.

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

Key points in the output:

  • Line 16 ("type": 16705): Identifies this as an AggregateCompleteTransactionV3.
  • Lines 26 and 40 ("recipient_address"): The two embedded transfers target different accounts. These are the hex-encoded forms of the Base32 addresses printed on lines 4-5.
  • Lines 29-30 and 43-44 ("mosaic_id", "amount"): Each transfer sends XYM (mosaic alias ID 16666583871264174062). The amounts 5000000 and 3000000 correspond to 5 and 3 XYM because this mosaic has divisibility 6.
  • Line 50 ("cosignatures": []): Empty because all embedded transactions share the same signer. No additional signatures are required.

The aggregate transaction executes atomically: both recipients receive their XYM transfers, or neither does.

The transaction hash printed in the output (line 54) can be used to search for the transaction in the Symbol Testnet Explorer.

Conclusion⚓︎

This tutorial showed how to:

Step Related documentation
Create embedded transactions
Build the aggregate
Sign and announce

Next Steps⚓︎