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');}
# 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);
この例では、簡略化のため1つのスクリプトに両方の 秘密鍵 を含めています。実際には、各当事者が自身のマシンで署名します。
アカウント A は、埋め込みトランザクションの署名者としてアカウント B を設定し、B の アドレス を派生させるために、アカウント B の 公開鍵 のみを必要とします。
環境変数 ACCOUNT_A_PRIVATE_KEY と ACCOUNT_B_PRIVATE_KEY で各アカウントの鍵を設定します。提供されない場合は、デフォルトのテストキーが使用されます。
自身の鍵を使用する場合は、アカウント A が XYM を持ち、アカウント B がスワップ用のカスタムモザイクを保持していることを確認してください。
# 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()));
// 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 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);
# 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");