A bonded aggregate transaction remains pending on the network until all required cosignatures have been
announced.
This works well when off-chain coordination is impractical. For example:
No shared infrastructure: Parties cannot coordinate through a common system, so the blockchain serves
as the common interface.
Asynchronous workflows: Cosigners are not available at the same time or cannot coordinate in real-time.
To prevent spam, bonded aggregates require a hash lock (a deposit of 10 XYM).
The network returns this deposit when all cosignatures arrive and the transaction reaches confirmation.
If the transaction times out, the deposit is forfeited.
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.
import{Hash256,PrivateKey}from'symbol-sdk';import{SymbolFacade,descriptors,generateMosaicAliasId,models}from'symbol-sdk/symbol';constNODE_URL=process.env.NODE_URL||'https://reference.symboltest.net:3001';console.log('Using node',NODE_URL);// Helper function to announce transactionasyncfunctionannounceTransaction(payload,endpoint,label){console.log(`Announcing ${label} to ${endpoint}`);constresponse=awaitfetch(`${NODE_URL}${endpoint}`,{method:'PUT',headers:{'Content-Type':'application/json'},body:payload});console.log(' Response:',awaitresponse.text());}// Helper function to wait for transaction statusasyncfunctionwaitForStatus(hash,expectedStatus,label){console.log(`Waiting for ${label} to reach ${expectedStatus} status...`);letattempts=0;constmaxAttempts=60;while(attempts<maxAttempts){try{consturl=`${NODE_URL}/transactionStatus/${hash}`;constresponse=awaitfetch(url);if(!response.ok){consterror=newError(`HTTP ${response.status}: ${response.statusText}`);error.status=response.status;throwerror;}conststatus=awaitresponse.json();console.log(' Transaction status:',status.group);if('failed'===status.group)thrownewError(`${label} failed: ${status.code}`);if(status.group===expectedStatus){console.log(`${label}${expectedStatus} in ${attempts} seconds`);return;}}catch(error){if(404===error.status)console.log(' Transaction status: not yet available');elsethrowerror;}attempts++;awaitnewPromise(resolve=>{setTimeout(resolve,1000);});}thrownewError(`${label} not ${expectedStatus} after ${maxAttempts} attempts`);}// 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 recommended feesconstfeePath='/network/fees/transaction';console.log('Fetching recommended fees from',feePath);constfeeResponse=awaitfetch(`${NODE_URL}${feePath}`);constfeeJSON=awaitfeeResponse.json();constmedianMultiplier=feeJSON.medianFeeMultiplier;constminimumMultiplier=feeJSON.minFeeMultiplier;constfeeMultiplier=Math.max(medianMultiplier,minimumMultiplier);console.log(' Fee multiplier:',feeMultiplier);// Embedded tx 1: Account A transfers 10 XYM to Account BconstembeddedTransaction1=facade.createEmbeddedTransactionFromTypedDescriptor(newdescriptors.TransferTransactionV1Descriptor(accountBAddress,[newdescriptors.UnresolvedMosaicDescriptor(generateMosaicAliasId('symbol.xym'),newmodels.Amount(10_000_000n))// 10 XYM],undefined),accountAKeyPair.publicKey);// Embedded tx 2: Account B transfers 1 custom mosaic to Account AconstcustomMosaicId=0x6D1314BE751B62C2n;constembeddedTransaction2=facade.createEmbeddedTransactionFromTypedDescriptor(newdescriptors.TransferTransactionV1Descriptor(accountAAddress,[newdescriptors.UnresolvedMosaicDescriptor(customMosaicId,newmodels.Amount(1n))// 1 custom mosaic],undefined),accountBKeyPair.publicKey);// Build the bonded aggregate transactionconstembeddedTransactions=[embeddedTransaction1,embeddedTransaction2];constbondedTransaction=facade.createTransactionFromTypedDescriptor(newdescriptors.AggregateBondedTransactionV3Descriptor(facade.static.hashEmbeddedTransactions(embeddedTransactions),embeddedTransactions,undefined),accountAKeyPair.publicKey,feeMultiplier,2*60*60,1);console.log('Built aggregate without signatures:');console.log(JSON.stringify(bondedTransaction.toJson(),null,2));// --- ACCOUNT A (Initiator) ---// Sign the bonded aggregate transactionconsole.log('[Account A] Signing the bonded aggregate...');constbondedSignature=facade.signTransaction(accountAKeyPair,bondedTransaction);constbondedJsonPayload=facade.transactionFactory.static.attachSignature(bondedTransaction,bondedSignature);constbondedHash=facade.hashTransaction(bondedTransaction).toString();console.log('Bonded aggregate transaction hash:',bondedHash);// Create hash lock transactionconsole.log('Creating hash lock transaction...');consthashLock=facade.createTransactionFromTypedDescriptor(newdescriptors.HashLockTransactionV1Descriptor(newdescriptors.UnresolvedMosaicDescriptor(generateMosaicAliasId('symbol.xym'),newmodels.Amount(10_000_000n)),// 10 XYM depositnewmodels.BlockDuration(100n),newHash256(bondedHash)),accountAKeyPair.publicKey,feeMultiplier,2*60*60);// Sign hash lockconsole.log('[Account A] Signing the hash lock...');consthashLockSignature=facade.signTransaction(accountAKeyPair,hashLock);consthashLockPayload=facade.transactionFactory.static.attachSignature(hashLock,hashLockSignature);consthashLockHash=facade.hashTransaction(hashLock).toString();console.log('Hash lock transaction hash:',hashLockHash);// Announce hash lock and wait for confirmationawaitannounceTransaction(hashLockPayload,'/transactions','Hash lock');awaitwaitForStatus(hashLockHash,'confirmed','Hash lock');// Announce bonded aggregate and wait for partial status //awaitannounceTransaction(bondedJsonPayload,'/transactions/partial','Bonded aggregate transaction');awaitwaitForStatus(bondedHash,'partial','Bonded aggregate transaction');// --- ACCOUNT B (Cosigner) ---// Retrieves partial transactions waiting for signatureconstpartialPath=`/transactions/partial?address=${accountBAddress}`;console.log('[Account B] Checking for partial transactions from '+'/transactions/partial');constpartialResponse=awaitfetch(`${NODE_URL}${partialPath}`);constpartialTxs=awaitpartialResponse.json();if(!partialTxs.data||0===partialTxs.data.length)thrownewError('No partial transactions found');console.log(`Found ${partialTxs.data.length} partial transaction(s)`);// Find the transaction matching the expected hashconstfound=partialTxs.data.some(tx=>tx.meta.hash===bondedHash);if(!found){thrownewError(`Expected transaction ${bondedHash} not found in `+'partial transactions');}console.log(`Found matching transaction: ${bondedHash}`);// Fetch full transaction details using the hashconstdetailPath=`/transactions/partial/${bondedHash}`;constdetailResponse=awaitfetch(`${NODE_URL}${detailPath}`);constpartialTxJson=awaitdetailResponse.json();// Verify transaction content before cosigningconsttxData=partialTxJson.transaction;console.log('[Account B] Verifying transaction: '+`${txData.transactions.length} embedded transactions`);// Submit Account B's cosignature using the transaction hashconstcosignaturePath='/transactions/cosignature';console.log('[Account B] Cosigning the bonded aggregate...');constcosignature=SymbolFacade.cosignTransactionHash(accountBKeyPair,newHash256(bondedHash),true);constcosignaturePayload=JSON.stringify({version:cosignature.version.toString(),signerPublicKey:cosignature.signerPublicKey.toString(),signature:cosignature.signature.toString(),parentHash:cosignature.parentHash.toString()});// Announce cosignatureawaitannounceTransaction(cosignaturePayload,cosignaturePath,'cosignature');// Wait for final confirmationawaitwaitForStatus(newHash256(bondedHash),'confirmed','Bonded aggregate transaction');}catch(e){console.error(e.message,'| Cause:',e.cause?.code??'unknown');}
A bonded aggregate transaction involves two distinct roles: an initiator (Account A) that builds, signs, and
announces the aggregate, and one or more cosigners (Account B, and any additional cosigners) that poll for pending
transactions and add their signatures after verifying the transaction.
In practice, each role runs as a separate program on a separate machine.
This tutorial combines both roles in a single script for simplicity.
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());
// Account A (initiates the aggregate tx and sends XYM to// Account B)finalStringaccountAPrivateKey=System.getenv().getOrDefault("ACCOUNT_A_PRIVATE_KEY","0".repeat(64));accountAKeyPair=newKeyPair(newCryptoTypes.PrivateKey(accountAPrivateKey));// Account B (sends custom mosaic to Account A)finalStringaccountBPrivateKey=System.getenv().getOrDefault("ACCOUNT_B_PRIVATE_KEY","1".repeat(64));accountBKeyPair=newKeyPair(newCryptoTypes.PrivateKey(accountBPrivateKey));accountAAddress=facade.network.publicKeyToAddress(accountAKeyPair.getPublicKey());accountBAddress=facade.network.publicKeyToAddress(accountBKeyPair.getPublicKey());System.out.printf("Account A: %s%n",accountAAddress);System.out.printf("Account B: %s%n",accountBAddress);
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.
// Embedded tx 1: Account A transfers 10 XYM to Account BfinalEmbeddedTransactionembeddedTransaction1=facade.createEmbeddedTransactionFromTypedDescriptor(newTransferTransactionV1Descriptor(accountBAddress,List.of(newUnresolvedMosaicDescriptor(newUnresolvedMosaicId(IdGenerator.generateMosaicAliasId("symbol.xym")),newAmount(10_000_000))),// 10 XYMnull),accountAKeyPair.getPublicKey());// Embedded tx 2: Account B transfers 1 custom mosaic to Account AfinallongcustomMosaicId=0x6D1314BE751B62C2L;finalEmbeddedTransactionembeddedTransaction2=facade.createEmbeddedTransactionFromTypedDescriptor(newTransferTransactionV1Descriptor(accountAAddress,List.of(newUnresolvedMosaicDescriptor(newUnresolvedMosaicId(customMosaicId),newAmount(1))),// 1 custom mosaicnull),accountBKeyPair.getPublicKey());
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 TransferTransactionV1, 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 bonded aggregate transactionembedded_transactions=[embedded_transaction_1,embedded_transaction_2]bonded_transaction=facade.create_transaction_from_descriptor({'type':'aggregate_bonded_transaction_v3','transactions_hash':facade.hash_embedded_transactions(embedded_transactions),'transactions':embedded_transactions},account_a_key_pair.public_key,fee_multiplier,2*60*60,1)print('Built aggregate without signatures:')print(json.dumps(bonded_transaction.to_json(),indent=2))
// Build the bonded aggregate transactionconstembeddedTransactions=[embeddedTransaction1,embeddedTransaction2];constbondedTransaction=facade.createTransactionFromTypedDescriptor(newdescriptors.AggregateBondedTransactionV3Descriptor(facade.static.hashEmbeddedTransactions(embeddedTransactions),embeddedTransactions,undefined),accountAKeyPair.publicKey,feeMultiplier,2*60*60,1);console.log('Built aggregate without signatures:');console.log(JSON.stringify(bondedTransaction.toJson(),null,2));
// Build the bonded aggregate transactionfinalList<EmbeddedTransaction>embeddedTransactions=List.of(embeddedTransaction1,embeddedTransaction2);finalTransactionbondedTransaction=facade.createTransactionFromTypedDescriptor(newAggregateBondedTransactionV3Descriptor(SymbolFacade.hashEmbeddedTransactions(embeddedTransactions),embeddedTransactions,null),accountAKeyPair.getPublicKey(),feeMultiplier,2*60*60,1);System.out.println("Built aggregate without signatures:");System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(bondedTransaction.toJson()));
Once the embedded transactions are prepared, create the bonded aggregate transaction from the transaction's descriptor.
The descriptor contains:
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.
also receives the signer public key, fee multiplier, deadline
duration, and reserved cosignature count.
The signer initiates the aggregate, announces the transaction, and pays the transaction fee.
calculates the fee based on the aggregate's total size,
which includes all embedded transactions plus space reserved for one cosignature.
The reserved cosignature count is provided as the final argument, set to 1 here.
Before announcing a bonded aggregate, a hash lock transaction must be created and confirmed.
The hash lock serves as a deposit to prevent spam and ensure network resources are not exhausted by unfinished
partial transactions.
Mosaic: The deposit amount (10 XYM).
This deposit is locked temporarily while waiting for cosignatures.
Duration: The number of blocks the deposit remains locked (100 blocks in this example).
If all cosignatures are collected and the bonded aggregate confirms before the duration expires,
the deposit is returned.
Otherwise, it is forfeited and sent to the harvester of the block where the transactions expired.
Hash: The hash of the bonded aggregate transaction being locked.
The hash lock is signed using and announced using the
helper function.
It must be confirmed before the bonded aggregate can be announced.
Then, the helper function polls the transaction status until confirmation.
// Announce bonded aggregate and wait for partial status //awaitannounceTransaction(bondedJsonPayload,'/transactions/partial','Bonded aggregate transaction');awaitwaitForStatus(bondedHash,'partial','Bonded aggregate transaction');
// Announce bonded aggregate and wait for partial statusannounceTransaction(bondedJsonPayload,"/transactions/partial","Bonded aggregate transaction");waitForStatus(bondedHash,"partial","Bonded aggregate transaction");
Once the hash lock is confirmed, the bonded aggregate is announced to /transactions/partialPUT using the
helper.
The node validates the transaction, checks that a valid hash lock exists, and places it in a partial state.
The helper monitors the transaction until it reaches this state, at which
point it can collect cosignatures.
# --- ACCOUNT B (Cosigner) ---# Retrieves partial transactions waiting for signaturepartial_path=f'/transactions/partial?address={account_b_address}'print('[Account B] Checking for partial transactions from ''/transactions/partial')withurllib.request.urlopen(f'{NODE_URL}{partial_path}')asresponse:partial_txs=json.loads(response.read().decode())ifnotpartial_txs['data']:raiseRuntimeError('No partial transactions found')print(f'Found {len(partial_txs["data"])} partial transaction(s)')# Find the transaction matching the expected hashfound=any(tx['meta']['hash']==str(bonded_hash)fortxinpartial_txs['data'])ifnotfound:raiseRuntimeError(f'Expected transaction {bonded_hash} not found in 'f'partial transactions')print(f'Found matching transaction: {bonded_hash}')
// --- ACCOUNT B (Cosigner) ---// Retrieves partial transactions waiting for signatureconstpartialPath=`/transactions/partial?address=${accountBAddress}`;console.log('[Account B] Checking for partial transactions from '+'/transactions/partial');constpartialResponse=awaitfetch(`${NODE_URL}${partialPath}`);constpartialTxs=awaitpartialResponse.json();if(!partialTxs.data||0===partialTxs.data.length)thrownewError('No partial transactions found');console.log(`Found ${partialTxs.data.length} partial transaction(s)`);// Find the transaction matching the expected hashconstfound=partialTxs.data.some(tx=>tx.meta.hash===bondedHash);if(!found){thrownewError(`Expected transaction ${bondedHash} not found in `+'partial transactions');}console.log(`Found matching transaction: ${bondedHash}`);
// --- ACCOUNT B (Cosigner) ---// Retrieves partial transactions waiting for signaturefinalStringpartialPath="/transactions/partial?address="+accountBAddress;System.out.println("[Account B] Checking for partial transactions from "+"/transactions/partial");finalHttpRequestpartialRequest=HttpRequest.newBuilder(URI.create(nodeUrl+partialPath)).GET().build();finalHttpResponse<String>partialResponse=HTTP_CLIENT.send(partialRequest,BodyHandlers.ofString());finalJsonNodepartialTxs=JSON_MAPPER.readTree(partialResponse.body());if(!partialTxs.has("data")||partialTxs.get("data").isEmpty())thrownewIOException("No partial transactions found");System.out.printf("Found %d partial transaction(s)%n",partialTxs.get("data").size());// Find the transaction matching the expected hashbooleanfound=false;for(finalJsonNodetx:partialTxs.get("data"))found|=bondedHash.equals(tx.get("meta").get("hash").asText());if(!found){thrownewIOException("Expected transaction "+bondedHash+" not found in partial transactions");}System.out.printf("Found matching transaction: %s%n",bondedHash);
Unlike complete aggregates where the transaction payload is shared off-chain, bonded aggregates enable coordination
through the network.
First, Account B polls /transactions/partialGET with the address parameter to find transactions waiting for its
signature.
This returns a list of partial transactions involving Account B.
This example looks for a specific transaction hash because both accounts run in the same script.
In practice, Account B would discover pending transactions by polling and decide which ones to cosign based on
their content.
# Fetch full transaction details using the hashdetail_path=f'/transactions/partial/{bonded_hash}'withurllib.request.urlopen(f'{NODE_URL}{detail_path}')asresponse:partial_tx_json=json.loads(response.read().decode())# Verify transaction content before cosigningtx_data=partial_tx_json['transaction']print(f'[Account B] Verifying transaction: 'f'{len(tx_data["transactions"])} embedded transactions')
// Fetch full transaction details using the hashconstdetailPath=`/transactions/partial/${bondedHash}`;constdetailResponse=awaitfetch(`${NODE_URL}${detailPath}`);constpartialTxJson=awaitdetailResponse.json();// Verify transaction content before cosigningconsttxData=partialTxJson.transaction;console.log('[Account B] Verifying transaction: '+`${txData.transactions.length} embedded transactions`);
// Fetch full transaction details using the hashfinalStringdetailPath="/transactions/partial/"+bondedHash;finalHttpRequestdetailRequest=HttpRequest.newBuilder(URI.create(nodeUrl+detailPath)).GET().build();finalHttpResponse<String>detailResponse=HTTP_CLIENT.send(detailRequest,BodyHandlers.ofString());finalJsonNodepartialTxJson=JSON_MAPPER.readTree(detailResponse.body());// Verify transaction content before cosigningfinalJsonNodetxData=partialTxJson.get("transaction");System.out.println("[Account B] Verifying transaction: "+txData.get("transactions").size()+" embedded transactions");
Before cosigning, Account B should verify that the embedded transactions match the expected operations.
This example simply logs the number of embedded transactions, but it could also check amounts,
recipients, and mosaics to ensure the swap terms are correct.
Verify before cosigning
Always inspect transaction content before cosigning.
Cosignatures are binding and cannot be undone.
// Submit Account B's cosignature using the transaction hashfinalStringcosignaturePath="/transactions/cosignature";System.out.println("[Account B] Cosigning the bonded aggregate...");finalDetachedCosignaturecosignature=SymbolFacade.cosignTransactionHashDetached(accountBKeyPair,newCryptoTypes.Hash256(bondedHash));finalStringcosignaturePayload=JSON_MAPPER.writeValueAsString(cosignature.toJson());// Announce cosignatureannounceTransaction(cosignaturePayload,cosignaturePath,"cosignature");
Account B cosigns the transaction using with the transaction hash and the
detached parameter set to .
A detached cosignature is a standalone object that can be submitted independently to the network.
This is required for bonded aggregates because the cosigner submits directly to the node.
The resulting detached cosignature payload includes:
Version: The cosignature format version.
Signer public key: Account B's public key, identifying who cosigned.
Signature: The cryptographic signature computed from the transaction hash and Account B's private key.
Parent hash: The hash of the bonded transaction being cosigned.
The cosignature payload is submitted using the helper function to
/transactions/cosignaturePUT.
The network validates the cosignature and attaches it to the partial transaction.
Once enough cosignatures are collected to satisfy all embedded transactions,
the network automatically processes the bonded aggregate and includes it in a block.
If all required cosignatures are collected before the deadline, the transaction confirms, both transfers execute,
and the hash lock deposit is returned to Account A.
If the deadline expires or any cosignature is invalid, the transaction fails and the deposit is forfeited.
Line 16 ("transactions"): Contains the two embedded transfers that will execute atomically.
Line 46 ("cosignatures": []): Initially empty. Cosignatures are submitted after announcement.
Line 49 (Bonded aggregate transaction hash:): The hash of the bonded aggregate, required for creating the hash
lock and announcing the transaction.
Line 53 (Announcing Hash lock to /transactions): A hash lock must be announced and confirmed before the bonded
aggregate.
Line 61 (Announcing Bonded aggregate transaction to /transactions/partial): Bonded aggregates use a different
endpoint than regular transactions.
Line 65 (Bonded aggregate transaction partial in 1 seconds): The bonded aggregate is now waiting for
cosignatures to be submitted to the network.
Line 69 ([Account B] Verifying transaction: 2 embedded transactions): Account B inspects the transaction content
before cosigning to ensure they agree with all operations.
Line 71 (Announcing cosignature to /transactions/cosignature): The cosignature is submitted to the network.
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.