Batching Transactions
INTERMEDIATE
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.
clusterAggregate Aggregate Complete Transaction clusterT2 Embedded Transfer 2 clusterT1 Embedded Transfer 1 S2 Signer R2 Recipient 2 S2->R2 3 XYM S1 Signer R1 Recipient 1 S1->R1 5 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
The following is the complete code listing for this tutorial.
A detailed, step-by-step explanation follows in the next section.
import json
import os
import time
import urllib.request
from symbolchain.CryptoTypes import PrivateKey
from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.symbol.IdGenerator import generate_mosaic_alias_id
from symbolchain.symbol.Network import Address
NODE_URL = os . getenv ( 'NODE_URL' , 'https://reference.symboltest.net:3001' )
print ( f 'Using node { NODE_URL } ' )
# Helper function to announce a transaction
def announce_transaction ( payload , label ):
print ( f 'Announcing { label } to /transactions' )
request = urllib . request . Request (
f ' { NODE_URL } /transactions' ,
data = payload . encode (),
headers = { 'Content-Type' : 'application/json' },
method = 'PUT'
)
with urllib . request . urlopen ( request ) as announce_response :
print ( f ' Response: { announce_response . read () . decode () } ' )
# Helper function to wait for transaction confirmation
def wait_for_confirmation ( tx_hash , label ):
print ( f 'Waiting for { label } confirmation...' )
for attempt in range ( 60 ):
time . sleep ( 1 )
try :
url = f ' { NODE_URL } /transactionStatus/ { tx_hash } '
with urllib . request . urlopen ( url ) as confirm_response :
status = json . loads ( confirm_response . read () . decode ())
print ( f ' Transaction status: { status [ "group" ] } ' )
if status [ 'group' ] == 'confirmed' :
print ( f ' { label } confirmed in { attempt } seconds' )
return
if status [ 'group' ] == 'failed' :
raise RuntimeError (
f ' { label } failed: { status [ "code" ] } ' )
except urllib . error . HTTPError :
print ( ' Transaction status: unknown' )
raise TimeoutError ( f ' { label } not confirmed after 60 seconds' )
SIGNER_PRIVATE_KEY = os . getenv (
'SIGNER_PRIVATE_KEY' ,
'0000000000000000000000000000000000000000000000000000000000000000' )
signer_key_pair = SymbolFacade . KeyPair (
PrivateKey ( SIGNER_PRIVATE_KEY ))
facade = SymbolFacade ( 'testnet' )
signer_address = facade . network . public_key_to_address (
signer_key_pair . public_key )
print ( f 'Signer public key: { signer_key_pair . public_key } ' )
print ( f 'Signer address: { signer_address } ' )
RECIPIENT_1 = os . getenv (
'RECIPIENT_1' , 'TCWYXKVYBMO4NBCUF3AXKJMXCGVSYQOS7ZG2TLI' )
RECIPIENT_2 = os . getenv (
'RECIPIENT_2' , 'TCD4NC5VIE2EEB3BCV5JRLBNJXYDW5Q5JK547MI' )
recipient1_hex = Address ( RECIPIENT_1 ) . bytes . hex () . upper ()
recipient2_hex = Address ( RECIPIENT_2 ) . bytes . hex () . upper ()
print ( f 'Recipient 1: { RECIPIENT_1 } ( { recipient1_hex } )' )
print ( f 'Recipient 2: { RECIPIENT_2 } ( { recipient2_hex } )' )
try :
# Fetch recommended fees
fee_path = '/network/fees/transaction'
print ( f 'Fetching recommended fees from { fee_path } ' )
with urllib . request . urlopen ( f ' { NODE_URL }{ fee_path } ' ) as response :
response_json = json . loads ( response . read () . decode ())
median_multiplier = response_json [ 'medianFeeMultiplier' ]
minimum_multiplier = response_json [ 'minFeeMultiplier' ]
fee_multiplier = max ( median_multiplier , minimum_multiplier )
print ( f ' Fee multiplier: { fee_multiplier } ' )
# Embedded tx 1: Send 5 XYM to Recipient 1
xym_mosaic_id = generate_mosaic_alias_id ( 'symbol.xym' )
embedded_tx_1 = facade . create_embedded_transaction_from_descriptor (
{
'type' : 'transfer_transaction_v1' ,
'recipient_address' : Address ( RECIPIENT_1 ),
'mosaics' : [{
'mosaic_id' : xym_mosaic_id ,
'amount' : 5_000_000 # 5 XYM
}]
},
signer_key_pair . public_key )
# Embedded tx 2: Send 3 XYM to Recipient 2
embedded_tx_2 = facade . create_embedded_transaction_from_descriptor (
{
'type' : 'transfer_transaction_v1' ,
'recipient_address' : Address ( RECIPIENT_2 ),
'mosaics' : [{
'mosaic_id' : xym_mosaic_id ,
'amount' : 3_000_000 # 3 XYM
}]
},
signer_key_pair . public_key )
# Build the aggregate transaction
embedded_transactions = [ embedded_tx_1 , embedded_tx_2 ]
transaction = facade . create_transaction_from_descriptor (
{
'type' : 'aggregate_complete_transaction_v3' ,
'transactions_hash' :
facade . hash_embedded_transactions ( embedded_transactions ),
'transactions' : embedded_transactions
},
signer_key_pair . public_key ,
fee_multiplier ,
2 * 60 * 60 )
print ( 'Built aggregate transaction:' )
print ( json . dumps ( transaction . to_json (), indent = 2 ))
# Sign transaction and generate final payload
signature = facade . sign_transaction ( signer_key_pair , transaction )
json_payload = facade . transaction_factory . attach_signature (
transaction , signature )
# Announce the transaction
announce_transaction ( json_payload , 'transaction' )
# Wait for confirmation
transaction_hash = facade . hash_transaction ( transaction )
print ( f 'Transaction hash: { transaction_hash } ' )
wait_for_confirmation ( transaction_hash , 'transaction' )
except Exception as e :
print ( e )
Download source
import { PrivateKey } from 'symbol-sdk' ;
import {
Address ,
SymbolFacade ,
descriptors ,
generateMosaicAliasId ,
models
} from 'symbol-sdk/symbol' ;
const NODE_URL = process . env . NODE_URL ||
'https://reference.symboltest.net:3001' ;
console . log ( 'Using node' , NODE_URL );
// Helper function to announce a transaction
async function announceTransaction ( payload , label ) {
console . log ( `Announcing ${ label } to /transactions` );
const response = await fetch ( ` ${ NODE_URL } /transactions` , {
method : 'PUT' ,
headers : { 'Content-Type' : 'application/json' },
body : payload
});
console . log ( ' Response:' , await response . text ());
}
// Helper function to wait for transaction confirmation
async function waitForConfirmation ( transactionHash , label ) {
console . log ( `Waiting for ${ label } confirmation...` );
for ( let attempt = 0 ; 60 > attempt ; attempt ++ ) {
await new Promise ( resolve => { setTimeout ( resolve , 1000 ); });
const response = await fetch (
` ${ NODE_URL } /transactionStatus/ ${ transactionHash } ` );
if ( ! response . ok ) {
if ( 404 === response . status ) {
console . log ( ' Transaction status: unknown' );
continue ;
}
throw new Error ( `HTTP ${ response . status } ` );
}
const status = await response . json ();
console . log ( ' Transaction status:' , status . group );
if ( 'confirmed' === status . group ) {
console . log ( ` ${ label } confirmed in` , attempt , 'seconds' );
return ;
}
if ( 'failed' === status . group )
throw new Error ( ` ${ label } failed: ${ status . code } ` );
}
throw new Error ( ` ${ label } not confirmed after 60 seconds` );
}
const SIGNER_PRIVATE_KEY = process . env . SIGNER_PRIVATE_KEY ||
'0000000000000000000000000000000000000000000000000000000000000000' ;
const signerKeyPair = new SymbolFacade . KeyPair (
new PrivateKey ( SIGNER_PRIVATE_KEY ));
const facade = new SymbolFacade ( 'testnet' );
const signerAddress = facade . network . publicKeyToAddress (
signerKeyPair . publicKey );
console . log ( 'Signer public key:' , signerKeyPair . publicKey . toString ());
console . log ( 'Signer address:' , signerAddress . toString ());
const RECIPIENT_1 = process . env . RECIPIENT_1 ||
'TCWYXKVYBMO4NBCUF3AXKJMXCGVSYQOS7ZG2TLI' ;
const RECIPIENT_2 = process . env . RECIPIENT_2 ||
'TCD4NC5VIE2EEB3BCV5JRLBNJXYDW5Q5JK547MI' ;
const recipient1Hex = Buffer . from (
new Address ( RECIPIENT_1 ). bytes ). toString ( 'hex' ). toUpperCase ();
const recipient2Hex = Buffer . from (
new Address ( RECIPIENT_2 ). bytes ). toString ( 'hex' ). toUpperCase ();
console . log ( `Recipient 1: ${ RECIPIENT_1 } ( ${ recipient1Hex } )` );
console . log ( `Recipient 2: ${ RECIPIENT_2 } ( ${ recipient2Hex } )` );
try {
// Fetch recommended fees
const feePath = '/network/fees/transaction' ;
console . log ( 'Fetching recommended fees from' , feePath );
const feeResponse = await fetch ( ` ${ NODE_URL }${ feePath } ` );
const feeJSON = await feeResponse . json ();
const medianMultiplier = feeJSON . medianFeeMultiplier ;
const minimumMultiplier = feeJSON . minFeeMultiplier ;
const feeMultiplier = Math . max ( medianMultiplier , minimumMultiplier );
console . log ( ' Fee multiplier:' , feeMultiplier );
// Embedded tx 1: Send 5 XYM to Recipient 1
const xymMosaicId = generateMosaicAliasId ( 'symbol.xym' );
const embeddedTx1 =
facade . createEmbeddedTransactionFromTypedDescriptor (
new descriptors . TransferTransactionV1Descriptor (
new Address ( RECIPIENT_1 ),
[
new descriptors . UnresolvedMosaicDescriptor (
xymMosaicId ,
new models . Amount ( 5 _000_000n )) // 5 XYM
],
undefined ),
signerKeyPair . publicKey );
// Embedded tx 2: Send 3 XYM to Recipient 2
const embeddedTx2 =
facade . createEmbeddedTransactionFromTypedDescriptor (
new descriptors . TransferTransactionV1Descriptor (
new Address ( RECIPIENT_2 ),
[
new descriptors . UnresolvedMosaicDescriptor (
xymMosaicId ,
new models . Amount ( 3 _000_000n )) // 3 XYM
],
undefined ),
signerKeyPair . publicKey );
// Build the aggregate transaction
const embeddedTransactions = [ embeddedTx1 , embeddedTx2 ];
const transaction = facade . createTransactionFromTypedDescriptor (
new descriptors . AggregateCompleteTransactionV3Descriptor (
facade . static . hashEmbeddedTransactions ( embeddedTransactions ),
embeddedTransactions ,
undefined ),
signerKeyPair . publicKey ,
feeMultiplier ,
2 * 60 * 60 );
console . log ( 'Built aggregate transaction:' );
console . log ( JSON . stringify ( transaction . toJson (), null , 2 ));
// Sign transaction and generate final payload
const signature = facade . signTransaction (
signerKeyPair , transaction );
const jsonPayload = facade . transactionFactory . static
. attachSignature ( transaction , signature );
// Announce the transaction
await announceTransaction ( jsonPayload , 'transaction' );
// Wait for confirmation
const transactionHash =
facade . hashTransaction ( transaction ). toString ();
console . log ( 'Transaction hash:' , transactionHash );
await waitForConfirmation ( transactionHash , 'transaction' );
} catch ( e ) {
console . error ( e . message , '| Cause:' , e . cause ? . code ?? 'unknown' );
}
Download source
//JAVA 21+
//DEPS org.symbol:symbol-sdk:3.3.1
import java.io.IOException ;
import java.net.URI ;
import java.net.http.HttpClient ;
import java.net.http.HttpRequest ;
import java.net.http.HttpResponse ;
import java.net.http.HttpResponse.BodyHandlers ;
import java.util.HexFormat ;
import java.util.List ;
import com.fasterxml.jackson.databind.JsonNode ;
import com.fasterxml.jackson.databind.ObjectMapper ;
import org.symbol.sdk.CryptoTypes ;
import org.symbol.sdk.facade.SymbolFacade ;
import org.symbol.sdk.symbol.Address ;
import org.symbol.sdk.symbol.IdGenerator ;
import org.symbol.sdk.symbol.KeyPair ;
import org.symbol.sdk.symbol.SymbolTransactionFactory ;
import org.symbol.sdk.symbol.descriptors.* ;
import org.symbol.sdk.symbol.models.* ;
public final class TransactionBatching {
private static final ObjectMapper JSON_MAPPER = new ObjectMapper ();
private static final HttpClient HTTP_CLIENT =
HttpClient . newHttpClient ();
private final String nodeUrl = System . getenv (). getOrDefault (
"NODE_URL" , "https://reference.symboltest.net:3001" );
private final SymbolFacade facade = new SymbolFacade ( "testnet" );
private void announceTransaction (
final String payload ,
final String label
) throws IOException , InterruptedException {
System . out . printf ( "Announcing %s to /transactions%n" , label );
final HttpRequest request = HttpRequest . newBuilder (
URI . create ( nodeUrl + "/transactions" ))
. header ( "Content-Type" , "application/json" )
. PUT ( HttpRequest . BodyPublishers . ofString ( payload ))
. build ();
final HttpResponse < String > response = HTTP_CLIENT . send (
request , BodyHandlers . ofString ());
System . out . printf ( " Response: %s%n" , response . body ());
}
private void waitForConfirmation (
final String transactionHash ,
final String label
) throws IOException , InterruptedException {
System . out . printf ( "Waiting for %s confirmation...%n" , label );
for ( int attempt = 0 ; 60 > attempt ; ++ attempt ) {
Thread . sleep ( 1000 );
final String statusPath =
"/transactionStatus/" + transactionHash ;
final HttpRequest statusRequest = HttpRequest . newBuilder (
URI . create ( nodeUrl + statusPath )). GET (). build ();
final HttpResponse < String > statusResponse = HTTP_CLIENT
. send ( statusRequest , BodyHandlers . ofString ());
if ( 404 == statusResponse . statusCode ()) {
System . out . println ( " Transaction status: unknown" );
continue ;
}
if ( 2 != statusResponse . statusCode () / 100 )
throw new IOException (
"HTTP " + statusResponse . statusCode ());
final JsonNode status =
JSON_MAPPER . readTree ( statusResponse . body ());
final String group = status . get ( "group" ). asText ();
System . out . printf ( " Transaction status: %s%n" , group );
if ( "confirmed" . equals ( group )) {
System . out . printf ( "%s confirmed in %d seconds%n" ,
label , attempt );
return ;
}
if ( "failed" . equals ( group ))
throw new IOException ( String . format ( "%s failed: %s" ,
label , status . get ( "code" ). asText ()));
}
throw new IOException ( String . format (
"%s not confirmed after 60 seconds" , label ));
}
public static void main ( final String [] args ) {
try {
new TransactionBatching (). run ();
} catch ( final Exception ex ) {
System . out . println ( null == ex . getMessage ()
? ex . toString ()
: ex . getMessage ());
}
}
private void run () throws IOException , InterruptedException {
System . out . printf ( "Using node %s%n" , nodeUrl );
final String signerPrivateKey = System . getenv (). getOrDefault (
"SIGNER_PRIVATE_KEY" , "0" . repeat ( 64 ));
final KeyPair signerKeyPair = new KeyPair (
new CryptoTypes . PrivateKey ( signerPrivateKey ));
final Address signerAddress = facade . network . publicKeyToAddress (
signerKeyPair . getPublicKey ());
System . out . printf ( "Signer public key: %s%n" ,
signerKeyPair . getPublicKey ());
System . out . printf ( "Signer address: %s%n" , signerAddress );
final String recipient1String = System . getenv (). getOrDefault (
"RECIPIENT_1" , "TCWYXKVYBMO4NBCUF3AXKJMXCGVSYQOS7ZG2TLI" );
final String recipient2String = System . getenv (). getOrDefault (
"RECIPIENT_2" , "TCD4NC5VIE2EEB3BCV5JRLBNJXYDW5Q5JK547MI" );
final Address recipient1 = new Address ( recipient1String );
final Address recipient2 = new Address ( recipient2String );
final String recipient1Hex = HexFormat . of (). formatHex (
recipient1 . bytes ()). toUpperCase ();
final String recipient2Hex = HexFormat . of (). formatHex (
recipient2 . bytes ()). toUpperCase ();
System . out . printf ( "Recipient 1: %s (%s)%n" ,
recipient1String , recipient1Hex );
System . out . printf ( "Recipient 2: %s (%s)%n" ,
recipient2String , recipient2Hex );
// Fetch recommended fees
final String feePath = "/network/fees/transaction" ;
System . out . printf ( "Fetching recommended fees from %s%n" , feePath );
final HttpRequest feeRequest = HttpRequest . newBuilder (
URI . create ( nodeUrl + feePath )). GET (). build ();
final HttpResponse < String > feeResponse = HTTP_CLIENT . send (
feeRequest , BodyHandlers . ofString ());
final JsonNode feeJSON = JSON_MAPPER . readTree ( feeResponse . body ());
final long medianMultiplier =
feeJSON . get ( "medianFeeMultiplier" ). asLong ();
final long minimumMultiplier =
feeJSON . get ( "minFeeMultiplier" ). asLong ();
final long feeMultiplier = Math . max (
medianMultiplier , minimumMultiplier );
System . out . printf ( " Fee multiplier: %d%n" , feeMultiplier );
// Embedded tx 1: Send 5 XYM to Recipient 1
final long xymMosaicId = IdGenerator . generateMosaicAliasId (
"symbol.xym" );
final EmbeddedTransaction embeddedTx1 =
facade . createEmbeddedTransactionFromTypedDescriptor (
new TransferTransactionV1Descriptor (
recipient1 ,
List . of ( new UnresolvedMosaicDescriptor (
new UnresolvedMosaicId ( xymMosaicId ),
new Amount ( 5_000_000 ))), // 5 XYM
null ),
signerKeyPair . getPublicKey ());
// Embedded tx 2: Send 3 XYM to Recipient 2
final EmbeddedTransaction embeddedTx2 =
facade . createEmbeddedTransactionFromTypedDescriptor (
new TransferTransactionV1Descriptor (
recipient2 ,
List . of ( new UnresolvedMosaicDescriptor (
new UnresolvedMosaicId ( xymMosaicId ),
new Amount ( 3_000_000 ))), // 3 XYM
null ),
signerKeyPair . getPublicKey ());
// Build the aggregate transaction
final List < EmbeddedTransaction > embeddedTransactions =
List . of ( embeddedTx1 , embeddedTx2 );
final Transaction transaction =
facade . createTransactionFromTypedDescriptor (
new AggregateCompleteTransactionV3Descriptor (
SymbolFacade . hashEmbeddedTransactions (
embeddedTransactions ),
embeddedTransactions ,
null ),
signerKeyPair . getPublicKey (),
feeMultiplier ,
2 * 60 * 60 );
System . out . println ( "Built aggregate transaction:" );
System . out . println ( JSON_MAPPER . writerWithDefaultPrettyPrinter ()
. writeValueAsString ( transaction . toJson ()));
// Sign transaction and generate final payload
final CryptoTypes . Signature signature = facade . signTransaction (
signerKeyPair , transaction );
final String jsonPayload = SymbolTransactionFactory
. attachSignature ( transaction , signature );
// Announce the transaction
announceTransaction ( jsonPayload , "transaction" );
// Wait for confirmation
final String transactionHash =
facade . hashTransaction ( transaction ). toString ();
System . out . printf ( "Transaction hash: %s%n" , transactionHash );
waitForConfirmation ( transactionHash , "transaction" );
}
}
Download source
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 } )` );
final String signerPrivateKey = System . getenv (). getOrDefault (
"SIGNER_PRIVATE_KEY" , "0" . repeat ( 64 ));
final KeyPair signerKeyPair = new KeyPair (
new CryptoTypes . PrivateKey ( signerPrivateKey ));
final Address signerAddress = facade . network . publicKeyToAddress (
signerKeyPair . getPublicKey ());
System . out . printf ( "Signer public key: %s%n" ,
signerKeyPair . getPublicKey ());
System . out . printf ( "Signer address: %s%n" , signerAddress );
final String recipient1String = System . getenv (). getOrDefault (
"RECIPIENT_1" , "TCWYXKVYBMO4NBCUF3AXKJMXCGVSYQOS7ZG2TLI" );
final String recipient2String = System . getenv (). getOrDefault (
"RECIPIENT_2" , "TCD4NC5VIE2EEB3BCV5JRLBNJXYDW5Q5JK547MI" );
final Address recipient1 = new Address ( recipient1String );
final Address recipient2 = new Address ( recipient2String );
final String recipient1Hex = HexFormat . of (). formatHex (
recipient1 . bytes ()). toUpperCase ();
final String recipient2Hex = HexFormat . of (). formatHex (
recipient2 . bytes ()). toUpperCase ();
System . out . printf ( "Recipient 1: %s (%s)%n" ,
recipient1String , recipient1Hex );
System . out . printf ( "Recipient 2: %s (%s)%n" ,
recipient2String , recipient2Hex );
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 Recommended Fees
Recommended fees are fetched from /network/fees/transaction GET ,
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 . create_embedded_transaction_from_descriptor (
{
'type' : 'transfer_transaction_v1' ,
'recipient_address' : Address ( RECIPIENT_1 ),
'mosaics' : [{
'mosaic_id' : xym_mosaic_id ,
'amount' : 5_000_000 # 5 XYM
}]
},
signer_key_pair . public_key )
# Embedded tx 2: Send 3 XYM to Recipient 2
embedded_tx_2 = facade . create_embedded_transaction_from_descriptor (
{
'type' : 'transfer_transaction_v1' ,
'recipient_address' : Address ( RECIPIENT_2 ),
'mosaics' : [{
'mosaic_id' : xym_mosaic_id ,
'amount' : 3_000_000 # 3 XYM
}]
},
signer_key_pair . public_key )
// Embedded tx 1: Send 5 XYM to Recipient 1
const xymMosaicId = generateMosaicAliasId ( 'symbol.xym' );
const embeddedTx1 =
facade . createEmbeddedTransactionFromTypedDescriptor (
new descriptors . TransferTransactionV1Descriptor (
new Address ( RECIPIENT_1 ),
[
new descriptors . UnresolvedMosaicDescriptor (
xymMosaicId ,
new models . Amount ( 5 _000_000n )) // 5 XYM
],
undefined ),
signerKeyPair . publicKey );
// Embedded tx 2: Send 3 XYM to Recipient 2
const embeddedTx2 =
facade . createEmbeddedTransactionFromTypedDescriptor (
new descriptors . TransferTransactionV1Descriptor (
new Address ( RECIPIENT_2 ),
[
new descriptors . UnresolvedMosaicDescriptor (
xymMosaicId ,
new models . Amount ( 3 _000_000n )) // 3 XYM
],
undefined ),
signerKeyPair . publicKey );
// Embedded tx 1: Send 5 XYM to Recipient 1
final long xymMosaicId = IdGenerator . generateMosaicAliasId (
"symbol.xym" );
final EmbeddedTransaction embeddedTx1 =
facade . createEmbeddedTransactionFromTypedDescriptor (
new TransferTransactionV1Descriptor (
recipient1 ,
List . of ( new UnresolvedMosaicDescriptor (
new UnresolvedMosaicId ( xymMosaicId ),
new Amount ( 5_000_000 ))), // 5 XYM
null ),
signerKeyPair . getPublicKey ());
// Embedded tx 2: Send 3 XYM to Recipient 2
final EmbeddedTransaction embeddedTx2 =
facade . createEmbeddedTransactionFromTypedDescriptor (
new TransferTransactionV1Descriptor (
recipient2 ,
List . of ( new UnresolvedMosaicDescriptor (
new UnresolvedMosaicId ( xymMosaicId ),
new Amount ( 3_000_000 ))), // 3 XYM
null ),
signerKeyPair . getPublicKey ());
Each transfer is created as an embedded transaction that will be wrapped inside the aggregate.
All embedded transactions use the same signer_public_keysignerPublicKeysignerPublicKey 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_keysignerPublicKeysignerPublicKey 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 . create_transaction_from_descriptor (
{
'type' : 'aggregate_complete_transaction_v3' ,
'transactions_hash' :
facade . hash_embedded_transactions ( embedded_transactions ),
'transactions' : embedded_transactions
},
signer_key_pair . public_key ,
fee_multiplier ,
2 * 60 * 60 )
print ( 'Built aggregate transaction:' )
print ( json . dumps ( transaction . to_json (), indent = 2 ))
// Build the aggregate transaction
const embeddedTransactions = [ embeddedTx1 , embeddedTx2 ];
const transaction = facade . createTransactionFromTypedDescriptor (
new descriptors . AggregateCompleteTransactionV3Descriptor (
facade . static . hashEmbeddedTransactions ( embeddedTransactions ),
embeddedTransactions ,
undefined ),
signerKeyPair . publicKey ,
feeMultiplier ,
2 * 60 * 60 );
console . log ( 'Built aggregate transaction:' );
console . log ( JSON . stringify ( transaction . toJson (), null , 2 ));
// Build the aggregate transaction
final List < EmbeddedTransaction > embeddedTransactions =
List . of ( embeddedTx1 , embeddedTx2 );
final Transaction transaction =
facade . createTransactionFromTypedDescriptor (
new AggregateCompleteTransactionV3Descriptor (
SymbolFacade . hashEmbeddedTransactions (
embeddedTransactions ),
embeddedTransactions ,
null ),
signerKeyPair . getPublicKey (),
feeMultiplier ,
2 * 60 * 60 );
System . out . println ( "Built aggregate transaction:" );
System . out . println ( JSON_MAPPER . writerWithDefaultPrettyPrinter ()
. writeValueAsString ( transaction . toJson ()));
The aggregate transaction is created from the transaction's descriptor, which contains:
SymbolFacade.create_transaction_from_descriptor SymbolFacade.createTransactionFromTypedDescriptor SymbolFacade.createTransactionFromTypedDescriptor also receives the signer public key, fee multiplier, and
deadline duration.
The signer signs the aggregate and pays the transaction fee.
SymbolFacade.create_transaction_from_descriptor SymbolFacade.createTransactionFromTypedDescriptor SymbolFacade.createTransactionFromTypedDescriptor calculates the fee based on the aggregate's total size.
Since no cosignatures are needed, no extra cosignature count is provided.
Signing and Announcing
The aggregate is signed with SymbolFacade.sign_transaction SymbolFacade.signTransaction SymbolFacade.signTransaction and serialized into a payload using
TransactionFactory.attach_signature SymbolTransactionFactory.attachSignature SymbolTransactionFactory.attachSignature .
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
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 recommended fees from /network/fees/transaction
Fee multiplier: 100
Built aggregate transaction:
{
"signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
"version": 3,
"network": 152,
"type": 16705,
"fee": "36000",
"deadline": "107382791542",
"transactions_hash": "006E8D5F5AC08E7D8EEAB2265569A68FF2921722DCE69D7AA61684A8EE5722C0",
"transactions": [
{
"signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
"version": 1,
"network": 152,
"type": 16724,
"recipient_address": "98AD8BAAB80B1DC684542EC175259711AB2C41D2FE4DA9AD",
"mosaics": [
{
"mosaic_id": "16666583871264174062",
"amount": "5000000"
}
],
"message": ""
},
{
"signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
"version": 1,
"network": 152,
"type": 16724,
"recipient_address": "9887C68BB54134420761157A98AC2D4DF03B761D4ABBCFB1",
"mosaics": [
{
"mosaic_id": "16666583871264174062",
"amount": "3000000"
}
],
"message": ""
}
],
"cosignatures": []
}
Announcing transaction to /transactions
Response: {"message":"packet 9 was pushed to the network via /transactions"}
Transaction hash: 5390D8FAC80B9F76275DA857A3A18B6704FEA8B84026C2893F0041326B8C23D2
Waiting for transaction confirmation...
Transaction status: unconfirmed
Transaction status: confirmed
transaction confirmed in 6 seconds
Key points in the output:
Line 14 ("type": 16705): Identifies this as an AggregateCompleteTransactionV3 .
Lines 24 and 38 ("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 27-28 and 41-42 ("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 48 ("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 52) can be used to search for the transaction in the
Symbol Testnet Explorer .
Conclusion
This tutorial showed how to:
Next Steps