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.environ.get('NODE_URL','https://001-sai-dual.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://001-sai-dual.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.
# 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 to demonstrate the complete workflow, but in practice
each party would sign on their own machine without sharing private keys.
The snippet reads the private keys from the ACCOUNT_A_PRIVATE_KEY and ACCOUNT_B_PRIVATE_KEY
environment variables, which default to test keys if not set.
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:
Type: Use aggregate_complete_transaction_v3.
Signer public key: The account initiating the aggregate.
This account announces the transaction and pays the transaction fee.
Sharing transaction fees
While the signer pays the entire fee upfront, other participants can contribute to the cost by including
XYM transfers back to the signer within the aggregate.
For example, Account B could add XYM to its existing transfer to Account A, or include a separate embedded
transfer transaction for the fee contribution.
This technique allows parties to split costs or even enables one account to send transactions
without holding XYM, since another account covers the fee.
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 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')
// --- 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');
With the aggregate transaction built, both accounts must sign it off-chain before it can be announced.
The snippet above separates the process by machine:
Account A (Initiator) signs the transaction using .
It then uses which normally produces a fully announce-ready payload.
In this case, however, the payload is still missing Account B’s cosignature.
Account A sends this intermediate payload to Account B through an off-chain channel.
Account B (Cosignatory) 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.
Account A receives the Account B's cosignature, adds it to the transaction object's cosignatures array, and
rebuilds the payload for announcement.
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.
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.
Using node https://001-sai-dual.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
Key points in the output:
Line 10 ("signature": "0000..."): Shows all zeros initially because the transaction hasn't been signed yet.
Line 14 ("type": 16705): Indicates this is an aggregate_complete_transaction_v3.
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.