importjsonimportosimporttimeimporturllib.requestfromsymbolchain.CryptoTypesimportHash256,PrivateKeyfromsymbolchain.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}')# Helper function to announce transactiondefannounce_transaction(payload,endpoint,label):print(f'Announcing {label} to {endpoint}')request=urllib.request.Request(f'{NODE_URL}{endpoint}',data=payload.encode(),headers={'Content-Type':'application/json'},method='PUT')withurllib.request.urlopen(request)asresponse:print(f' Response: {response.read().decode()}')# Helper function to wait for transaction statusdefwait_for_status(hash_value,expected_status,label):print(f'Waiting for {label} to reach {expected_status} status...')attempts=0max_attempts=60whileattempts<max_attempts:try:url=f'{NODE_URL}/transactionStatus/{hash_value}'withurllib.request.urlopen(url)asresponse:status=json.loads(response.read().decode())print(f' Transaction status: {status["group"]}')ifstatus['group']=='failed':raiseException(f'{label} failed: {status["code"]}')ifstatus['group']==expected_status:print(f'{label}{expected_status} '+f'in {attempts} seconds')returnexcepturllib.error.HTTPErrorase:ife.code!=404:raiseprint(' Transaction status: not yet available')attempts+=1time.sleep(1)raiseException(f'{label} not {expected_status} after {max_attempts} attempts')# 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 bonded aggregate transactionembedded_transactions=[embedded_transaction_1,embedded_transaction_2]bonded_transaction=facade.transaction_factory.create({'type':'aggregate_bonded_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 sizebonded_transaction.fee=Amount(fee_mult*(bonded_transaction.size+104))print('Built aggregate without signatures:')print(json.dumps(bonded_transaction.to_json(),indent=2))# --- ACCOUNT A (Initiator) ---# Sign the bonded aggregate transactionprint('[Account A] Signing the bonded aggregate...')bonded_signature=facade.sign_transaction(account_a_key_pair,bonded_transaction)bonded_json_payload=facade.transaction_factory.attach_signature(bonded_transaction,bonded_signature)bonded_hash=facade.hash_transaction(bonded_transaction)print(f'Bonded aggregate transaction hash: {bonded_hash}')# Create hash lock transactionprint('Creating hash lock transaction...')hash_lock=facade.transaction_factory.create({'type':'hash_lock_transaction_v1','signer_public_key':account_a_key_pair.public_key,'deadline':timestamp.add_hours(2).timestamp,'mosaic':{'mosaic_id':generate_mosaic_alias_id('symbol.xym'),'amount':10_000_000# 10 XYM deposit},'duration':100,# Lock duration in blocks'hash':bonded_hash})hash_lock.fee=Amount(fee_mult*hash_lock.size)# Sign hash lockprint('[Account A] Signing the hash lock...')hash_lock_signature=facade.sign_transaction(account_a_key_pair,hash_lock)hash_lock_payload=facade.transaction_factory.attach_signature(hash_lock,hash_lock_signature)hash_lock_hash=facade.hash_transaction(hash_lock)print(f'Hash lock transaction hash: {hash_lock_hash}')# Announce hash lock and wait for confirmationannounce_transaction(hash_lock_payload,'/transactions','Hash lock')wait_for_status(hash_lock_hash,'confirmed','Hash lock')# Announce bonded aggregate and wait for partial statusannounce_transaction(bonded_json_payload,'/transactions/partial','Bonded aggregate transaction')wait_for_status(bonded_hash,'partial','Bonded aggregate transaction')# --- 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']:raiseException('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:raiseException(f'Expected transaction {bonded_hash} not found in 'f'partial transactions')print(f'Found matching transaction: {bonded_hash}')# 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')# Submit Account B's cosignature using the transaction hashcosignature_path='/transactions/cosignature'print('[Account B] Cosigning the bonded aggregate...')cosignature=facade.cosign_transaction_hash(account_b_key_pair,bonded_hash,True)cosignature_payload=json.dumps({'version':str(cosignature.version),'signerPublicKey':str(cosignature.signer_public_key),'signature':str(cosignature.signature),'parentHash':str(cosignature.parent_hash)})# Announce cosignatureannounce_transaction(cosignature_payload,cosignature_path,'cosignature')# Wait for final confirmationwait_for_status(bonded_hash,'confirmed','Bonded aggregate transaction')exceptExceptionase:print(e)
import{Hash256,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);// 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(status.group==='failed'){thrownewError(`${label} failed: ${status.code}`);}if(status.group===expectedStatus){console.log(`${label}${expectedStatus} in ${attempts} seconds`);return;}}catch(error){if(error.status===404){console.log(' Transaction status: not yet available');}else{throwerror;}}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 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 bonded aggregate transactionconstembeddedTransactions=[embeddedTransaction1,embeddedTransaction2];constbondedTransaction=facade.transactionFactory.create({type:'aggregate_bonded_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 sizebondedTransaction.fee=newmodels.Amount(feeMult*(bondedTransaction.size+104));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.transactionFactory.create({type:'hash_lock_transaction_v1',signerPublicKey:accountAKeyPair.publicKey.toString(),deadline:timestamp.addHours(2).timestamp,mosaic:{mosaicId:generateMosaicAliasId('symbol.xym'),amount:10_000_000n// 10 XYM deposit},duration:100n,// Lock duration in blockshash:bondedHash});hashLock.fee=newmodels.Amount(feeMult*hashLock.size);// 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 statusawaitannounceTransaction(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||partialTxs.data.length===0){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');}
# 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}')
# 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}')
# Build the bonded aggregate transactionembedded_transactions=[embedded_transaction_1,embedded_transaction_2]bonded_transaction=facade.transaction_factory.create({'type':'aggregate_bonded_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 sizebonded_transaction.fee=Amount(fee_mult*(bonded_transaction.size+104))print('Built aggregate without signatures:')print(json.dumps(bonded_transaction.to_json(),indent=2))
constembeddedTransactions=[embeddedTransaction1,embeddedTransaction2];constbondedTransaction=facade.transactionFactory.create({type:'aggregate_bonded_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 sizebondedTransaction.fee=newmodels.Amount(feeMult*(bondedTransaction.size+104));console.log('Built aggregate without signatures:');console.log(JSON.stringify(bondedTransaction.toJson(),null,2));
# --- 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']:raiseException('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:raiseException(f'Expected transaction {bonded_hash} not found in 'f'partial transactions')print(f'Found matching transaction: {bonded_hash}')
// 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||partialTxs.data.length===0){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 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')