importjsonimportosimporttimeimporturllib.requestfromsymbolchain.CryptoTypesimportPrivateKeyfromsymbolchain.facade.SymbolFacadeimportSymbolFacadefromsymbolchain.scimportAmountfromsymbolchain.symbol.IdGeneratorimportgenerate_mosaic_alias_idfromsymbolchain.symbol.NetworkimportNetworkTimestampNODE_URL=os.getenv('NODE_URL','https://reference.symboltest.net:3001')print(f'Using node {NODE_URL}')# Account A (initiates the aggregate tx and sends XYM to Account B)ACCOUNT_A_PRIVATE_KEY=os.getenv('ACCOUNT_A_PRIVATE_KEY','0000000000000000000000000000000000000000000000000000000000000000')account_a_key_pair=SymbolFacade.KeyPair(PrivateKey(ACCOUNT_A_PRIVATE_KEY))# Account B (sends custom mosaic to Account A)ACCOUNT_B_PRIVATE_KEY=os.getenv('ACCOUNT_B_PRIVATE_KEY','1111111111111111111111111111111111111111111111111111111111111111')account_b_key_pair=SymbolFacade.KeyPair(PrivateKey(ACCOUNT_B_PRIVATE_KEY))facade=SymbolFacade('testnet')account_a_address=facade.network.public_key_to_address(account_a_key_pair.public_key)account_b_address=facade.network.public_key_to_address(account_b_key_pair.public_key)print(f'Account A: {account_a_address}')print(f'Account B: {account_b_address}')try:# Fetch current network timetime_path='/node/time'print(f'Fetching current network time from {time_path}')withurllib.request.urlopen(f'{NODE_URL}{time_path}')asresponse:response_json=json.loads(response.read().decode())receive_timestamp=(response_json['communicationTimestamps']['receiveTimestamp'])timestamp=NetworkTimestamp(int(receive_timestamp))print(f' Network time: {timestamp.timestamp} ms since nemesis')# Fetch recommended feesfee_path='/network/fees/transaction'print(f'Fetching recommended fees from {fee_path}')withurllib.request.urlopen(f'{NODE_URL}{fee_path}')asresponse:response_json=json.loads(response.read().decode())median_mult=response_json['medianFeeMultiplier']minimum_mult=response_json['minFeeMultiplier']fee_mult=max(median_mult,minimum_mult)print(f' Fee multiplier: {fee_mult}')# Embedded tx 1: Account A transfers 10 XYM to Account Bembedded_transaction_1=facade.transaction_factory.create_embedded({'type':'transfer_transaction_v1','signer_public_key':account_a_key_pair.public_key,'recipient_address':account_b_address,'mosaics':[{'mosaic_id':generate_mosaic_alias_id('symbol.xym'),'amount':10_000_000# 10 XYM (divisibility = 6)}]})## Embedded tx 2: Account B transfers 1 custom mosaic to Account Acustom_mosaic_id=0x6D1314BE751B62C2embedded_transaction_2=facade.transaction_factory.create_embedded({'type':'transfer_transaction_v1','signer_public_key':account_b_key_pair.public_key,'recipient_address':account_a_address,'mosaics':[{'mosaic_id':custom_mosaic_id,'amount':1# 1 custom mosaic (divisibility = 0)}]})# Build the aggregate transactionembedded_transactions=[embedded_transaction_1,embedded_transaction_2]transaction=facade.transaction_factory.create({'type':'aggregate_complete_transaction_v3','signer_public_key':account_a_key_pair.public_key,'deadline':timestamp.add_hours(2).timestamp,'transactions_hash':facade.hash_embedded_transactions(embedded_transactions),'transactions':embedded_transactions})# Reserve space for one cosignature (104 bytes)# and calculate fee for the final transaction sizetransaction.fee=Amount(fee_mult*(transaction.size+104))print('Built aggregate transaction without signatures:')print(json.dumps(transaction.to_json(),indent=2))# --- ACCOUNT A (Initiator) ---print('[Account A] Signing the aggregate...')signature_a=facade.sign_transaction(account_a_key_pair,transaction)transaction_payload=facade.transaction_factory.attach_signature(transaction,signature_a)payload_formatted=json.dumps(json.loads(transaction_payload),indent=2)print(f'[Account A] Payload ready to share:\n{payload_formatted}')# --- OFF-CHAIN COORDINATION ---# Account A sends the payload to Account Bshared_payload=transaction_payloadprint('[Account A] ==> Payload sent to Account B (offchain)')# --- ACCOUNT B (Cosignatory) ---received_transaction=facade.transaction_factory.deserialize(bytes.fromhex(json.loads(shared_payload)['payload']))print('[Account B] Cosigning...')cosignature_b=facade.cosign_transaction(account_b_key_pair,received_transaction)cosignature_formatted=json.dumps(cosignature_b.to_json(),indent=2)print(f'[Account B] Cosignature created: {cosignature_formatted}')# --- OFF-CHAIN COORDINATION ---# Account B sends the cosignature back to Account Ashared_cosignature=cosignature_bprint('[Account B] <== Cosignature sent back to Account A (offchain)')# --- ACCOUNT A (Initiator) ---# Add cosignature to the transaction and rebuild payloadtransaction.cosignatures.append(shared_cosignature)transaction_payload=facade.transaction_factory.attach_signature(transaction,signature_a)json_payload=transaction_payloadprint('[Account A] Ready to announce')# Announce the transactionannounce_path='/transactions'print(f'Announcing transaction to {announce_path}')announce_request=urllib.request.Request(f'{NODE_URL}{announce_path}',data=json_payload.encode(),headers={'Content-Type':'application/json'},method='PUT')withurllib.request.urlopen(announce_request)asresponse:print(f' Response: {response.read().decode()}')# Compute hash of final transaction (with cosignatures)transaction_hash=facade.hash_transaction(transaction)# Wait for confirmationstatus_path=f'/transactionStatus/{transaction_hash}'print(f'Waiting for confirmation from {status_path}')forattemptinrange(60):time.sleep(1)try:withurllib.request.urlopen(f'{NODE_URL}{status_path}')asresponse:status=json.loads(response.read().decode())print(f' Transaction status: {status['group']}')ifstatus['group']=='confirmed':print(f'Transaction confirmed in {attempt} seconds')breakifstatus['group']=='failed':print(f'Transaction failed: {status['code']}')breakexcepturllib.error.HTTPErrorase:print(f' Transaction status: unknown | Cause: ({e.msg})')else:print('Confirmation took too long.')exceptExceptionase:print(e)
import{PrivateKey}from'symbol-sdk';import{generateMosaicAliasId,models,NetworkTimestamp,SymbolFacade}from'symbol-sdk/symbol';constNODE_URL=process.env.NODE_URL||'https://reference.symboltest.net:3001';console.log('Using node',NODE_URL);// Account A (initiates the aggregate tx and sends XYM to Account B)constACCOUNT_A_PRIVATE_KEY=process.env.ACCOUNT_A_PRIVATE_KEY||('0000000000000000000000000000000000000000000000000000000000000000');constaccountAKeyPair=newSymbolFacade.KeyPair(newPrivateKey(ACCOUNT_A_PRIVATE_KEY));// Account B (sends custom mosaic to Account A)constACCOUNT_B_PRIVATE_KEY=process.env.ACCOUNT_B_PRIVATE_KEY||('1111111111111111111111111111111111111111111111111111111111111111');constaccountBKeyPair=newSymbolFacade.KeyPair(newPrivateKey(ACCOUNT_B_PRIVATE_KEY));constfacade=newSymbolFacade('testnet');constaccountAAddress=facade.network.publicKeyToAddress(accountAKeyPair.publicKey);constaccountBAddress=facade.network.publicKeyToAddress(accountBKeyPair.publicKey);console.log('Account A:',accountAAddress.toString());console.log('Account B:',accountBAddress.toString());try{// Fetch current network timeconsttimePath='/node/time';console.log('Fetching current network time from',timePath);consttimeResponse=awaitfetch(`${NODE_URL}${timePath}`);consttimeJSON=awaittimeResponse.json();consttimestamp=newNetworkTimestamp(timeJSON.communicationTimestamps.receiveTimestamp);console.log(' Network time:',timestamp.timestamp,'ms since nemesis');// Fetch recommended feesconstfeePath='/network/fees/transaction';console.log('Fetching recommended fees from',feePath);constfeeResponse=awaitfetch(`${NODE_URL}${feePath}`);constfeeJSON=awaitfeeResponse.json();constmedianMult=feeJSON.medianFeeMultiplier;constminimumMult=feeJSON.minFeeMultiplier;constfeeMult=Math.max(medianMult,minimumMult);console.log(' Fee multiplier:',feeMult);// Embedded tx 1: Account A transfers 10 XYM to Account BconstembeddedTransaction1=facade.transactionFactory.createEmbedded({type:'transfer_transaction_v1',signerPublicKey:accountAKeyPair.publicKey.toString(),recipientAddress:accountBAddress.toString(),mosaics:[{mosaicId:generateMosaicAliasId('symbol.xym'),amount:10_000_000n// 10 XYM (divisibility = 6)}]});// Embedded tx 2: Account B transfers 1 custom mosaic to Account AconstcustomMosaicId=0x6D1314BE751B62C2n;constembeddedTransaction2=facade.transactionFactory.createEmbedded({type:'transfer_transaction_v1',signerPublicKey:accountBKeyPair.publicKey.toString(),recipientAddress:accountAAddress.toString(),mosaics:[{mosaicId:customMosaicId,amount:1n// 1 custom mosaic (divisibility = 0)}]});// Build the aggregate transactionconstembeddedTransactions=[embeddedTransaction1,embeddedTransaction2];consttransaction=facade.transactionFactory.create({type:'aggregate_complete_transaction_v3',signerPublicKey:accountAKeyPair.publicKey.toString(),deadline:timestamp.addHours(2).timestamp,transactionsHash:facade.static.hashEmbeddedTransactions(embeddedTransactions),transactions:embeddedTransactions});// Reserve space for one cosignature (104 bytes)// and calculate fee for the final transaction sizetransaction.fee=newmodels.Amount(feeMult*(transaction.size+104));console.log('Built aggregate transaction without signatures:');console.log(JSON.stringify(transaction.toJson(),null,2));// --- ACCOUNT A (Initiator) ---console.log('[Account A] Signing the aggregate...');constsignatureA=facade.signTransaction(accountAKeyPair,transaction);consttransactionPayload=facade.transactionFactory.static.attachSignature(transaction,signatureA);constpayloadFormatted=JSON.stringify(JSON.parse(transactionPayload),null,2);console.log('[Account A] Payload ready to share:\n',payloadFormatted);// --- OFF-CHAIN COORDINATION ---// Account A sends the payload to Account BconstsharedPayload=transactionPayload;console.log('[Account A] ==> Payload sent to Account B (offchain)');// --- ACCOUNT B (Cosignatory) ---constpayloadHex=JSON.parse(sharedPayload).payload;constreceivedTransaction=facade.transactionFactory.static.deserialize(Buffer.from(payloadHex,'hex'));console.log('[Account B] Cosigning...');constcosignatureB=facade.cosignTransaction(accountBKeyPair,receivedTransaction);constcosignatureFormatted=JSON.stringify(cosignatureB.toJson(),null,2);console.log('[Account B] Cosignature created:',cosignatureFormatted);// --- OFF-CHAIN COORDINATION ---// Account B sends the cosignature back to Account AconstsharedCosignature=cosignatureB;console.log('[Account B] <== Cosignature sent back to Account A','(offchain)');// --- ACCOUNT A (Initiator) ---// Add cosignature to the transaction and rebuild payloadtransaction.cosignatures.push(sharedCosignature);consttransactionPayloadFinal=facade.transactionFactory.static.attachSignature(transaction,signatureA);constjsonPayload=transactionPayloadFinal;console.log('[Account A] Ready to announce');// Announce the transactionconstannouncePath='/transactions';console.log('Announcing transaction to',announcePath);constannounceResponse=awaitfetch(`${NODE_URL}${announcePath}`,{method:'PUT',headers:{'Content-Type':'application/json'},body:jsonPayload});console.log(' Response:',awaitannounceResponse.text());// Compute hash of final transaction (with cosignatures)consttransactionHash=facade.hashTransaction(transaction).toString();// Wait for confirmationconststatusPath=`/transactionStatus/${transactionHash}`;console.log('Waiting for confirmation from',statusPath);for(letattempt=0;attempt<60;attempt++){awaitnewPromise(resolve=>setTimeout(resolve,1000));try{conststatusResponse=awaitfetch(`${NODE_URL}${statusPath}`);conststatus=awaitstatusResponse.json();console.log(' Transaction status:',status.group);if(status.group==='confirmed'){console.log('Transaction confirmed in',attempt,'seconds');break;}if(status.group==='failed'){console.log('Transaction failed:',status.code);break;}}catch(e){console.log(' Transaction status: unknown | Cause:',e.message);}}}catch(e){console.error(e.message,'| Cause:',e.cause?.code??'unknown');}
# 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());
この例では、簡略化のため1つのスクリプトに両方の 秘密鍵 を含めています。実際には、各当事者が自身のマシンで署名します。
アカウント A は、 埋め込みトランザクション の署名者としてアカウント B を設定し、B の アドレス を派生させるために、アカウント B の 公開鍵 のみを必要とします 。
環境変数 ACCOUNT_A_PRIVATE_KEY と ACCOUNT_B_PRIVATE_KEY で各アカウントの鍵を設定します。提供されない場合は、デフォルトのテストキーが使用されます。自身の鍵を使用する場合は、アカウント A が XYM を持ち、アカウント B がスワップ用のカスタムモザイクを保持していることを確認してください。
# 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 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));
# --- ACCOUNT A (Initiator) ---print('[Account A] Signing the aggregate...')signature_a=facade.sign_transaction(account_a_key_pair,transaction)transaction_payload=facade.transaction_factory.attach_signature(transaction,signature_a)payload_formatted=json.dumps(json.loads(transaction_payload),indent=2)print(f'[Account A] Payload ready to share:\n{payload_formatted}')# --- OFF-CHAIN COORDINATION ---# Account A sends the payload to Account Bshared_payload=transaction_payloadprint('[Account A] ==> Payload sent to Account B (offchain)')
// --- ACCOUNT A (Initiator) ---console.log('[Account A] Signing the aggregate...');constsignatureA=facade.signTransaction(accountAKeyPair,transaction);consttransactionPayload=facade.transactionFactory.static.attachSignature(transaction,signatureA);constpayloadFormatted=JSON.stringify(JSON.parse(transactionPayload),null,2);console.log('[Account A] Payload ready to share:\n',payloadFormatted);// --- OFF-CHAIN COORDINATION ---// Account A sends the payload to Account BconstsharedPayload=transactionPayload;console.log('[Account A] ==> Payload sent to Account B (offchain)');
アカウント A は SDK を使用してトランザクションに署名し、中間ペイロードを生成します。アカウント B の連署が欠けているため、このペイロードはまだアナウンスできる状態ではありません。アカウント A はオフチェーンのチャネルを通じて、この中間ペイロードをアカウント B に送信します。
アグリゲートトランザクションの署名
複数の埋め込みトランザクションで署名者として表示される場合でも、アカウントは一度だけ署名します。このチュートリアルでは、アカウント A がアグリゲートトランザクションに署名しますが、これはアグリゲート自体と、アカウント A が署名者である最初の埋め込みトランザクションの両方をカバーします。
# --- ACCOUNT B (Cosignatory) ---received_transaction=facade.transaction_factory.deserialize(bytes.fromhex(json.loads(shared_payload)['payload']))print('[Account B] Cosigning...')cosignature_b=facade.cosign_transaction(account_b_key_pair,received_transaction)cosignature_formatted=json.dumps(cosignature_b.to_json(),indent=2)print(f'[Account B] Cosignature created: {cosignature_formatted}')# --- OFF-CHAIN COORDINATION ---# Account B sends the cosignature back to Account Ashared_cosignature=cosignature_bprint('[Account B] <== Cosignature sent back to Account A (offchain)')
// --- ACCOUNT B (Cosignatory) ---constpayloadHex=JSON.parse(sharedPayload).payload;constreceivedTransaction=facade.transactionFactory.static.deserialize(Buffer.from(payloadHex,'hex'));console.log('[Account B] Cosigning...');constcosignatureB=facade.cosignTransaction(accountBKeyPair,receivedTransaction);constcosignatureFormatted=JSON.stringify(cosignatureB.toJson(),null,2);console.log('[Account B] Cosignature created:',cosignatureFormatted);// --- OFF-CHAIN COORDINATION ---// Account B sends the cosignature back to Account AconstsharedCosignature=cosignatureB;console.log('[Account B] <== Cosignature sent back to Account A','(offchain)');
アカウント B はペイロードを受け取り、デシリアライズしてトランザクションオブジェクトを再構築します。アカウント B は、埋め込みトランザクションが自身が署名しようとしている内容と一致しているか検証する必要があります。
その後、連署します。これによりトランザクションハッシュが計算され、連署オブジェクトが生成されます。この連署のみがアカウント A に送り返されます。
# --- ACCOUNT A (Initiator) ---# Add cosignature to the transaction and rebuild payloadtransaction.cosignatures.append(shared_cosignature)transaction_payload=facade.transaction_factory.attach_signature(transaction,signature_a)json_payload=transaction_payloadprint('[Account A] Ready to announce')
// --- ACCOUNT A (Initiator) ---// Add cosignature to the transaction and rebuild payloadtransaction.cosignatures.push(sharedCosignature);consttransactionPayloadFinal=facade.transactionFactory.static.attachSignature(transaction,signatureA);constjsonPayload=transactionPayloadFinal;console.log('[Account A] Ready to announce');
アカウント A はアカウント B の連署を受け取り、トランザクションオブジェクトの cosignatures 配列に追加し、アナウンス用のペイロードを再構築します。
# 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();
# 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.')