You also need two accounts with XYM and one custom mosaic to complete the swap.
Although pre-funded accounts are provided for convenience, they are not maintained and may run out of funds.
To use your own accounts, complete the following steps:
Create an account (Account A) to initiate the aggregate transaction, either
from code or
by using a wallet.
Create a second account (Account B) to participate in the swap.
importjsonimportosimporttimeimporturllib.requestfromsymbolchain.CryptoTypesimportPrivateKeyfromsymbolchain.facade.SymbolFacadeimportSymbolFacadefromsymbolchain.scimportAmountfromsymbolchain.symbol.IdGeneratorimportgenerate_mosaic_alias_idfromsymbolchain.symbol.NetworkimportNetworkTimestampNODE_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 timetime_path='/node/time'print(f'Fetching current network time from {time_path}')withurllib.request.urlopen(f'{NODE_URL}{time_path}')asresponse: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 feesfee_path='/network/fees/transaction'print(f'Fetching recommended fees from {fee_path}')withurllib.request.urlopen(f'{NODE_URL}{fee_path}')asresponse: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 Bembedded_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 Acustom_mosaic_id=0x6D1314BE751B62C2embedded_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 transactionembedded_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 sizetransaction.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 Bshared_payload=transaction_payloadprint('[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 Ashared_cosignature=cosignature_bprint('[Account B] <== Cosignature sent back to Account A (offchain)')# --- ACCOUNT A (Initiator) ---# Add cosignature to the transaction and rebuild payloadtransaction.cosignatures.append(shared_cosignature)transaction_payload=facade.transaction_factory.attach_signature(transaction,signature_a)json_payload=transaction_payloadprint('[Account A] Ready to announce')# Announce the transactionannounce_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')withurllib.request.urlopen(announce_request)asresponse:print(f' Response: {response.read().decode()}')# Compute hash of final transaction (with cosignatures)transaction_hash=facade.hash_transaction(transaction)# Wait for confirmationstatus_path=f'/transactionStatus/{transaction_hash}'print(f'Waiting for confirmation from {status_path}')forattemptinrange(60):time.sleep(1)try:withurllib.request.urlopen(f'{NODE_URL}{status_path}')asresponse:status=json.loads(response.read().decode())print(f' Transaction status: {status['group']}')ifstatus['group']=='confirmed':print(f'Transaction confirmed in {attempt} seconds')breakifstatus['group']=='failed':print(f'Transaction failed: {status['code']}')breakexcepturllib.error.HTTPErrorase:print(f' Transaction status: unknown | Cause: ({e.msg})')else:print('Confirmation took too long.')exceptExceptionase:print(e)
import{PrivateKey}from'symbol-sdk';import{generateMosaicAliasId,models,NetworkTimestamp,SymbolFacade}from'symbol-sdk/symbol';constNODE_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)constACCOUNT_A_PRIVATE_KEY=process.env.ACCOUNT_A_PRIVATE_KEY||('0000000000000000000000000000000000000000000000000000000000000000');constaccountAKeyPair=newSymbolFacade.KeyPair(newPrivateKey(ACCOUNT_A_PRIVATE_KEY));// Account B (sends custom mosaic to Account A)constACCOUNT_B_PRIVATE_KEY=process.env.ACCOUNT_B_PRIVATE_KEY||('1111111111111111111111111111111111111111111111111111111111111111');constaccountBKeyPair=newSymbolFacade.KeyPair(newPrivateKey(ACCOUNT_B_PRIVATE_KEY));constfacade=newSymbolFacade('testnet');constaccountAAddress=facade.network.publicKeyToAddress(accountAKeyPair.publicKey);constaccountBAddress=facade.network.publicKeyToAddress(accountBKeyPair.publicKey);console.log('Account A:',accountAAddress.toString());console.log('Account B:',accountBAddress.toString());try{// Fetch current network timeconsttimePath='/node/time';console.log('Fetching current network time from',timePath);consttimeResponse=awaitfetch(`${NODE_URL}${timePath}`);consttimeJSON=awaittimeResponse.json();consttimestamp=newNetworkTimestamp(timeJSON.communicationTimestamps.receiveTimestamp);console.log(' Network time:',timestamp.timestamp,'ms since nemesis');// Fetch recommended feesconstfeePath='/network/fees/transaction';console.log('Fetching recommended fees from',feePath);constfeeResponse=awaitfetch(`${NODE_URL}${feePath}`);constfeeJSON=awaitfeeResponse.json();constmedianMult=feeJSON.medianFeeMultiplier;constminimumMult=feeJSON.minFeeMultiplier;constfeeMult=Math.max(medianMult,minimumMult);console.log(' Fee multiplier:',feeMult);// Embedded tx 1: Account A transfers 10 XYM to Account BconstembeddedTransaction1=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 AconstcustomMosaicId=0x6D1314BE751B62C2n;constembeddedTransaction2=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 transactionconstembeddedTransactions=[embeddedTransaction1,embeddedTransaction2];consttransaction=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 sizetransaction.fee=newmodels.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...');constsignatureA=facade.signTransaction(accountAKeyPair,transaction);consttransactionPayload=facade.transactionFactory.static.attachSignature(transaction,signatureA);constpayloadFormatted=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 BconstsharedPayload=transactionPayload;console.log('[Account A] ==> Payload sent to Account B (offchain)');// --- ACCOUNT B (Cosignatory) ---constpayloadHex=JSON.parse(sharedPayload).payload;constreceivedTransaction=facade.transactionFactory.static.deserialize(Buffer.from(payloadHex,'hex'));console.log('[Account B] Cosigning...');constcosignatureB=facade.cosignTransaction(accountBKeyPair,receivedTransaction);constcosignatureFormatted=JSON.stringify(cosignatureB.toJson(),null,2);console.log('[Account B] Cosignature created:',cosignatureFormatted);// --- OFF-CHAIN COORDINATION ---// Account B sends the cosignature back to Account AconstsharedCosignature=cosignatureB;console.log('[Account B] <== Cosignature sent back to Account A','(offchain)');// --- ACCOUNT A (Initiator) ---// Add cosignature to the transaction and rebuild payloadtransaction.cosignatures.push(sharedCosignature);consttransactionPayloadFinal=facade.transactionFactory.static.attachSignature(transaction,signatureA);constjsonPayload=transactionPayloadFinal;console.log('[Account A] Ready to announce');// Announce the transactionconstannouncePath='/transactions';console.log('Announcing transaction to',announcePath);constannounceResponse=awaitfetch(`${NODE_URL}${announcePath}`,{method:'PUT',headers:{'Content-Type':'application/json'},body:jsonPayload});console.log(' Response:',awaitannounceResponse.text());// Compute hash of final transaction (with cosignatures)consttransactionHash=facade.hashTransaction(transaction).toString();// Wait for confirmationconststatusPath=`/transactionStatus/${transactionHash}`;console.log('Waiting for confirmation from',statusPath);for(letattempt=0;attempt<60;attempt++){awaitnewPromise(resolve=>setTimeout(resolve,1000));try{conststatusResponse=awaitfetch(`${NODE_URL}${statusPath}`);conststatus=awaitstatusResponse.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');}
The whole code is wrapped in a single try block to provide simple error handling,
but applications will probably want to use more fine-grained control.
A complete aggregate transaction may involve two distinct roles: an initiator (Account A) that builds and announces
the aggregate, and one or more cosigners (Account B, and any additional cosigners) that receive the transaction
payload off-chain and add their signatures after verifying the transaction.
When only one account is involved, no cosignatures are needed.
In practice, each role runs as a separate program on a separate machine.
This tutorial demonstrates the multi-party case but combines both roles in a single script for simplicity.
# 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)constACCOUNT_A_PRIVATE_KEY=process.env.ACCOUNT_A_PRIVATE_KEY||('0000000000000000000000000000000000000000000000000000000000000000');constaccountAKeyPair=newSymbolFacade.KeyPair(newPrivateKey(ACCOUNT_A_PRIVATE_KEY));// Account B (sends custom mosaic to Account A)constACCOUNT_B_PRIVATE_KEY=process.env.ACCOUNT_B_PRIVATE_KEY||('1111111111111111111111111111111111111111111111111111111111111111');constaccountBKeyPair=newSymbolFacade.KeyPair(newPrivateKey(ACCOUNT_B_PRIVATE_KEY));constfacade=newSymbolFacade('testnet');constaccountAAddress=facade.network.publicKeyToAddress(accountAKeyPair.publicKey);constaccountBAddress=facade.network.publicKeyToAddress(accountBKeyPair.publicKey);console.log('Account A:',accountAAddress.toString());console.log('Account B:',accountBAddress.toString());
This example includes both private keys in one script for simplicity.
In practice, each party signs on their own machine.
Account A only needs Account B's public key to build the aggregate, because B's public key is required to set B as
the signer of an embedded transaction and to derive B's address.
The ACCOUNT_A_PRIVATE_KEY and ACCOUNT_B_PRIVATE_KEY environment variables set the keys for each account.
If not provided, test keys are used as defaults.
If using your own keys, ensure Account A has XYM and Account B holds a custom mosaic for the swap.
The addresses for both accounts are derived from their public keys using the facade's network configuration.
# Fetch current network timetime_path='/node/time'print(f'Fetching current network time from {time_path}')withurllib.request.urlopen(f'{NODE_URL}{time_path}')asresponse: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 feesfee_path='/network/fees/transaction'print(f'Fetching recommended fees from {fee_path}')withurllib.request.urlopen(f'{NODE_URL}{fee_path}')asresponse: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 BconstembeddedTransaction1=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 AconstcustomMosaicId=0x6D1314BE751B62C2n;constembeddedTransaction2=facade.transactionFactory.createEmbedded({type:'transfer_transaction_v1',signerPublicKey:accountBKeyPair.publicKey.toString(),recipientAddress:accountAAddress.toString(),mosaics:[{mosaicId:customMosaicId,amount:1n// 1 custom mosaic (divisibility = 0)}]});
The embedded transactions define the operations to execute atomically.
Each embedded transaction specifies:
Type: All transaction types can be embedded within aggregates (except other aggregates).
For embedded transfers, use transfer_transaction_v1, the same as for basic transfer transactions.
Signer public key: The account that would sign this transaction if it were announced
independently.
Transaction-specific fields: All fields specific to the transaction type must be provided.
For transfers, this includes the recipient address and the mosaics to send.
Note that embedded transactions do not include fee or deadline fields.
These are inherited from the enclosing aggregate transaction.
The first transfer sends 10 XYM from Account A to Account B.
The second transfer sends 1 custom mosaic from Account B to Account A.
About the custom mosaic
The custom mosaic with ID 0x6D1314BE751B62C2 was created for this tutorial.
The default Account B has been seeded with this mosaic so the swap can execute successfully.
If using your own accounts, ensure Account B holds a custom mosaic and update the mosaic ID in the code.
# Build the aggregate transactionembedded_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 sizetransaction.fee=Amount(fee_mult*(transaction.size+104))print('Built aggregate transaction without signatures:')print(json.dumps(transaction.to_json(),indent=2))
// Build the aggregate transactionconstembeddedTransactions=[embeddedTransaction1,embeddedTransaction2];consttransaction=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 sizetransaction.fee=newmodels.Amount(feeMult*(transaction.size+104));console.log('Built aggregate transaction without signatures:');console.log(JSON.stringify(transaction.toJson(),null,2));
Once the embedded transactions are prepared, create the complete aggregate transaction that wraps them:
Deadline: The timestamp, in network time, after which the transaction
expires and can no longer be confirmed.
Transactions hash: A hash computed from all embedded transactions.
This ensures the embedded transactions cannot be modified after signing.
Use to compute this value.
Transactions: The array of embedded transactions to execute.
The fee is calculated based on the aggregate's total size, which includes all embedded transactions plus
space reserved for one cosignature (104 bytes).
# --- 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 Bshared_payload=transaction_payloadprint('[Account A] ==> Payload sent to Account B (offchain)')
// --- ACCOUNT A (Initiator) ---console.log('[Account A] Signing the aggregate...');constsignatureA=facade.signTransaction(accountAKeyPair,transaction);consttransactionPayload=facade.transactionFactory.static.attachSignature(transaction,signatureA);constpayloadFormatted=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 BconstsharedPayload=transactionPayload;console.log('[Account A] ==> Payload sent to Account B (offchain)');
Account A signs the transaction using and produces an intermediate payload using
.
This payload is not yet ready to announce because it is missing Account B's cosignature.
Account A sends this intermediate payload to Account B through an off-chain channel.
Signatures in aggregate transactions
An account only signs once, even if it appears as the signer in multiple embedded transactions.
In this tutorial, Account A signs the aggregate transaction, which covers both the aggregate itself and the
first embedded transaction where Account A is the signer.
When all embedded transactions share the same signer (batching multiple operations from one account),
cosignatures are not required. The aggregate can be announced immediately after signing, and the fee
calculation does not need to reserve space for cosignatures.
# --- 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 Ashared_cosignature=cosignature_bprint('[Account B] <== Cosignature sent back to Account A (offchain)')
// --- ACCOUNT B (Cosignatory) ---constpayloadHex=JSON.parse(sharedPayload).payload;constreceivedTransaction=facade.transactionFactory.static.deserialize(Buffer.from(payloadHex,'hex'));console.log('[Account B] Cosigning...');constcosignatureB=facade.cosignTransaction(accountBKeyPair,receivedTransaction);constcosignatureFormatted=JSON.stringify(cosignatureB.toJson(),null,2);console.log('[Account B] Cosignature created:',cosignatureFormatted);// --- OFF-CHAIN COORDINATION ---// Account B sends the cosignature back to Account AconstsharedCosignature=cosignatureB;console.log('[Account B] <== Cosignature sent back to Account A','(offchain)');
Account B receives the payload and deserializes it using to reconstruct the
transaction object.
Account B should verify that the embedded transactions match what it expects to sign.
It then cosigns using , which computes the transaction hash and produces a
cosignature object.
Only this cosignature is sent back to Account A.
Verify before cosigning
Always inspect transaction content before cosigning.
Cosignatures are binding and cannot be undone.
# --- ACCOUNT A (Initiator) ---# Add cosignature to the transaction and rebuild payloadtransaction.cosignatures.append(shared_cosignature)transaction_payload=facade.transaction_factory.attach_signature(transaction,signature_a)json_payload=transaction_payloadprint('[Account A] Ready to announce')
// --- ACCOUNT A (Initiator) ---// Add cosignature to the transaction and rebuild payloadtransaction.cosignatures.push(sharedCosignature);consttransactionPayloadFinal=facade.transactionFactory.static.attachSignature(transaction,signatureA);constjsonPayload=transactionPayloadFinal;console.log('[Account A] Ready to announce');
Account A receives Account B's cosignature, adds it to the transaction object's cosignatures array, and rebuilds the
payload for announcement.
Now that the transaction is ready to be announced, it follows the same process as regular, non-aggregate transactions,
as shown in the Transfer Transaction tutorial.
# Announce the transactionannounce_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')withurllib.request.urlopen(announce_request)asresponse:print(f' Response: {response.read().decode()}')# Compute hash of final transaction (with cosignatures)transaction_hash=facade.hash_transaction(transaction)
// Announce the transactionconstannouncePath='/transactions';console.log('Announcing transaction to',announcePath);constannounceResponse=awaitfetch(`${NODE_URL}${announcePath}`,{method:'PUT',headers:{'Content-Type':'application/json'},body:jsonPayload});console.log(' Response:',awaitannounceResponse.text());// Compute hash of final transaction (with cosignatures)consttransactionHash=facade.hashTransaction(transaction).toString();
Once all signatures are collected, the transaction is announced to a node using the /transactionsPUT endpoint.
The node validates that all required signatures are present and valid before accepting the transaction.
If validation passes, the transaction is added to the unconfirmed pool and broadcast to other nodes.
# Wait for confirmationstatus_path=f'/transactionStatus/{transaction_hash}'print(f'Waiting for confirmation from {status_path}')forattemptinrange(60):time.sleep(1)try:withurllib.request.urlopen(f'{NODE_URL}{status_path}')asresponse:status=json.loads(response.read().decode())print(f' Transaction status: {status['group']}')ifstatus['group']=='confirmed':print(f'Transaction confirmed in {attempt} seconds')breakifstatus['group']=='failed':print(f'Transaction failed: {status['code']}')breakexcepturllib.error.HTTPErrorase:print(f' Transaction status: unknown | Cause: ({e.msg})')else:print('Confirmation took too long.')
The polling loop checks the status every second until the transaction is confirmed or fails.
Once confirmed, the swap is complete and both transfers have executed.
Line 18 ("transactions"): Contains the two embedded transfers that will execute atomically.
Line 48 ("cosignatures": []): Initially empty. Account B's cosignature is added before announcement.
Note how Account A's signature is only needed once, even though it appears as signer in both the aggregate and the
first embedded transaction.
Line 53 ("payload": "6801..."): The transaction payload computed from the aggregate transaction and its embedded
transactions.
Line 60 ("signature": "7037..."): Account B's cosignature for the aggregate transaction.
Line 66 (Waiting for confirmation ...): The hash shown in the confirmation check can be used to search for the
transaction in the Symbol Testnet Explorer.
The aggregate transaction is treated as a single atomic unit by the network.
The swap executes completely: Account A receives the custom mosaic and Account B receives the XYM,
or the entire transaction fails and no assets are transferred.
Batch from one account: If all transactions share the same signer and no cosignatures are needed, see the
Batching Transactions tutorial for a simpler flow.