中級
トランザクションのバッチ処理
アグリゲートコンプリートトランザクション を使用すると、単一の アカウント からの複数のトランザクションを1つのアトミックな操作に結論、1回の手数料と1回の承認で済ませることができます。
これは、例えば報酬の分配、支払いの分割、または複数のアカウントへの同時資金供給などに役立ちます。
このチュートリアルでは、異なる受信者に XYM を送信する2つの 転送トランザクション をバッチ処理する方法を説明します。
%3 clusterAggregate アグリゲートコンプリートトランザクション clusterT1 埋め込み転送 1 clusterT2 埋め込み転送 2 S1 署名者 R1 受信者 1 S1->R1 5 XYM S2 署名者 R2 受信者 2 S2->R2 3 XYM
すべての埋め込みトランザクションが同じ署名者を共有するため、cosignatures:|連署 は必要ありません。
アグリゲートは単一のアカウントによって署名され、アナウンスされます。
複数のアカウントから署名を収集する必要がある例については、アグリゲートコンプリート および アグリゲートボンデッド のチュートリアルを参照してください。
前提条件
開始する前に、開発環境がセットアップされていることを確認してください。
開発環境のセットアップ を参照してください。
また、転送とトランザクション手数料をカバーするのに十分な XYM を持つ アカウント も必要です。
便宜上、事前に資金供給されたテストアカウントが提供していますが、これはメンテナンスされておらず、いつでも資金が不足する可能性があります。
自身のアカウントを使用する場合は、以下の手順を完了してください。
さらに、トランザクションがどのようにアナウンスされ、承認されるかを理解するために、転送トランザクション のチュートリアルを確認してください。
完全なコード
このチュートリアルの完全なコード一覧を以下に示します。
詳細な手順ごとの説明は次のセクションで行います。
import json
import os
import time
import urllib.request
from symbolchain.CryptoTypes import PrivateKey
from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.sc import Amount
from symbolchain.symbol.IdGenerator import generate_mosaic_alias_id
from symbolchain.symbol.Network import Address , NetworkTimestamp
NODE_URL = os . getenv (
'NODE_URL' , 'https://reference.symboltest.net:3001' )
print ( f 'Using node { NODE_URL } ' )
SIGNER_PRIVATE_KEY = os . getenv (
'SIGNER_PRIVATE_KEY' ,
'0000000000000000000000000000000000000000000000000000000000000000' )
signer_key_pair = SymbolFacade . KeyPair (
PrivateKey ( SIGNER_PRIVATE_KEY ))
facade = SymbolFacade ( 'testnet' )
signer_address = facade . network . public_key_to_address (
signer_key_pair . public_key )
print ( f 'Signer public key: { signer_key_pair . public_key } ' )
print ( f 'Signer address: { signer_address } ' )
RECIPIENT_1 = os . getenv (
'RECIPIENT_1' , 'TCWYXKVYBMO4NBCUF3AXKJMXCGVSYQOS7ZG2TLI' )
RECIPIENT_2 = os . getenv (
'RECIPIENT_2' , 'TCD4NC5VIE2EEB3BCV5JRLBNJXYDW5Q5JK547MI' )
recipient1_hex = Address ( RECIPIENT_1 ) . bytes . hex () . upper ()
recipient2_hex = Address ( RECIPIENT_2 ) . bytes . hex () . upper ()
print ( f 'Recipient 1: { RECIPIENT_1 } ( { recipient1_hex } )' )
print ( f 'Recipient 2: { RECIPIENT_2 } ( { recipient2_hex } )' )
try :
# Fetch current network time
time_path = '/node/time'
print ( f 'Fetching current network time from { time_path } ' )
with urllib . request . urlopen ( f ' { NODE_URL }{ time_path } ' ) as response :
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 fees
fee_path = '/network/fees/transaction'
print ( f 'Fetching recommended fees from { fee_path } ' )
with urllib . request . urlopen ( f ' { NODE_URL }{ fee_path } ' ) as response :
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: Send 5 XYM to Recipient 1
xym_mosaic_id = generate_mosaic_alias_id ( 'symbol.xym' )
embedded_tx_1 = facade . transaction_factory . create_embedded ({
'type' : 'transfer_transaction_v1' ,
'signer_public_key' : signer_key_pair . public_key ,
'recipient_address' : RECIPIENT_1 ,
'mosaics' : [{
'mosaic_id' : xym_mosaic_id ,
'amount' : 5_000_000 # 5 XYM
}]
})
# Embedded tx 2: Send 3 XYM to Recipient 2
embedded_tx_2 = facade . transaction_factory . create_embedded ({
'type' : 'transfer_transaction_v1' ,
'signer_public_key' : signer_key_pair . public_key ,
'recipient_address' : RECIPIENT_2 ,
'mosaics' : [{
'mosaic_id' : xym_mosaic_id ,
'amount' : 3_000_000 # 3 XYM
}]
})
# Build the aggregate transaction
embedded_transactions = [ embedded_tx_1 , embedded_tx_2 ]
transaction = facade . transaction_factory . create ({
'type' : 'aggregate_complete_transaction_v3' ,
'signer_public_key' : signer_key_pair . public_key ,
'deadline' : timestamp . add_hours ( 2 ) . timestamp ,
'transactions_hash' :
facade . hash_embedded_transactions ( embedded_transactions ),
'transactions' : embedded_transactions
})
transaction . fee = Amount ( fee_mult * transaction . size )
print ( 'Built aggregate transaction:' )
print ( json . dumps ( transaction . to_json (), indent = 2 ))
# Sign transaction and generate final payload
signature = facade . sign_transaction ( signer_key_pair , transaction )
json_payload = ( facade . transaction_factory . attach_signature (
transaction , signature ))
# Announce the transaction
announce_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'
)
with urllib . request . urlopen ( announce_request ) as response :
print ( f ' Response: { response . read () . decode () } ' )
# Wait for confirmation
transaction_hash = facade . hash_transaction ( transaction )
status_path = f '/transactionStatus/ { transaction_hash } '
print ( f 'Waiting for confirmation from { status_path } ' )
for attempt in range ( 60 ):
time . sleep ( 1 )
try :
with urllib . request . urlopen (
f ' { NODE_URL }{ status_path } '
) as response :
status = json . loads ( response . read () . decode ())
print ( f ' Transaction status: { status [ "group" ] } ' )
if status [ 'group' ] == 'confirmed' :
print ( f 'Transaction confirmed in { attempt } seconds' )
break
if status [ 'group' ] == 'failed' :
print ( f 'Transaction failed: { status [ "code" ] } ' )
break
except urllib . error . HTTPError as e :
print ( f ' Transaction status: unknown | Cause: ( { e . msg } )' )
else :
print ( 'Confirmation took too long.' )
except Exception as e :
print ( e )
Download source
import { PrivateKey } from 'symbol-sdk' ;
import {
Address ,
generateMosaicAliasId ,
models ,
NetworkTimestamp ,
SymbolFacade
} from 'symbol-sdk/symbol' ;
const NODE_URL = process . env . NODE_URL ||
'https://reference.symboltest.net:3001' ;
console . log ( 'Using node' , NODE_URL );
const SIGNER_PRIVATE_KEY =
process . env . SIGNER_PRIVATE_KEY ||
'0000000000000000000000000000000000000000000000000000000000000000' ;
const signerKeyPair = new SymbolFacade . KeyPair (
new PrivateKey ( SIGNER_PRIVATE_KEY ));
const facade = new SymbolFacade ( 'testnet' );
const signerAddress = facade . network . publicKeyToAddress (
signerKeyPair . publicKey );
console . log ( 'Signer public key:' , signerKeyPair . publicKey . toString ());
console . log ( 'Signer address:' , signerAddress . toString ());
const RECIPIENT_1 = process . env . RECIPIENT_1 ||
'TCWYXKVYBMO4NBCUF3AXKJMXCGVSYQOS7ZG2TLI' ;
const RECIPIENT_2 = process . env . RECIPIENT_2 ||
'TCD4NC5VIE2EEB3BCV5JRLBNJXYDW5Q5JK547MI' ;
const recipient1Hex = Buffer . from (
new Address ( RECIPIENT_1 ). bytes ). toString ( 'hex' ). toUpperCase ();
const recipient2Hex = Buffer . from (
new Address ( RECIPIENT_2 ). bytes ). toString ( 'hex' ). toUpperCase ();
console . log ( `Recipient 1: ${ RECIPIENT_1 } ( ${ recipient1Hex } )` );
console . log ( `Recipient 2: ${ RECIPIENT_2 } ( ${ recipient2Hex } )` );
try {
// Fetch current network time
const timePath = '/node/time' ;
console . log ( 'Fetching current network time from' , timePath );
const timeResponse = await fetch ( ` ${ NODE_URL }${ timePath } ` );
const timeJSON = await timeResponse . json ();
const timestamp = new NetworkTimestamp (
timeJSON . communicationTimestamps . receiveTimestamp );
console . log ( ' Network time:' , timestamp . timestamp ,
'ms since nemesis' );
// Fetch recommended fees
const feePath = '/network/fees/transaction' ;
console . log ( 'Fetching recommended fees from' , feePath );
const feeResponse = await fetch ( ` ${ NODE_URL }${ feePath } ` );
const feeJSON = await feeResponse . json ();
const medianMult = feeJSON . medianFeeMultiplier ;
const minimumMult = feeJSON . minFeeMultiplier ;
const feeMult = Math . max ( medianMult , minimumMult );
console . log ( ' Fee multiplier:' , feeMult );
// Embedded tx 1: Send 5 XYM to Recipient 1
const xymMosaicId = generateMosaicAliasId ( 'symbol.xym' );
const embeddedTx1 = facade . transactionFactory . createEmbedded ({
type : 'transfer_transaction_v1' ,
signerPublicKey : signerKeyPair . publicKey . toString (),
recipientAddress : RECIPIENT_1 ,
mosaics : [{
mosaicId : xymMosaicId ,
amount : 5 _000_000n // 5 XYM
}]
});
// Embedded tx 2: Send 3 XYM to Recipient 2
const embeddedTx2 = facade . transactionFactory . createEmbedded ({
type : 'transfer_transaction_v1' ,
signerPublicKey : signerKeyPair . publicKey . toString (),
recipientAddress : RECIPIENT_2 ,
mosaics : [{
mosaicId : xymMosaicId ,
amount : 3 _000_000n // 3 XYM
}]
});
// Build the aggregate transaction
const embeddedTransactions = [ embeddedTx1 , embeddedTx2 ];
const transaction = facade . transactionFactory . create ({
type : 'aggregate_complete_transaction_v3' ,
signerPublicKey : signerKeyPair . publicKey . toString (),
deadline : timestamp . addHours ( 2 ). timestamp ,
transactionsHash : facade . static . hashEmbeddedTransactions (
embeddedTransactions ),
transactions : embeddedTransactions
});
transaction . fee = new models . Amount (
feeMult * transaction . size );
console . log ( 'Built aggregate transaction:' );
console . log ( JSON . stringify ( transaction . toJson (), null , 2 ));
// Sign transaction and generate final payload
const signature = facade . signTransaction (
signerKeyPair , transaction );
const jsonPayload = facade . transactionFactory . static
. attachSignature ( transaction , signature );
// Announce the transaction
const announcePath = '/transactions' ;
console . log ( 'Announcing transaction to' , announcePath );
const announceResponse = await fetch (
` ${ NODE_URL }${ announcePath } ` , {
method : 'PUT' ,
headers : { 'Content-Type' : 'application/json' },
body : jsonPayload
});
console . log ( ' Response:' , await announceResponse . text ());
// Wait for confirmation
const transactionHash =
facade . hashTransaction ( transaction ). toString ();
const statusPath = `/transactionStatus/ ${ transactionHash } ` ;
console . log ( 'Waiting for confirmation from' , statusPath );
for ( let attempt = 0 ; attempt < 60 ; attempt ++ ) {
await new Promise ( resolve => setTimeout ( resolve , 1000 ));
try {
const statusResponse = await fetch (
` ${ NODE_URL }${ statusPath } ` );
const status = await statusResponse . 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' );
}
Download source
コード解説
アカウントの設定
署名者アカウントは、 SIGNER_PRIVATE_KEY 環境変数から読み込まれます。
指定されていない場合は、デフォルトでテストキーが使用されます。
2つの受信者アドレスは、 RECIPIENT_1 および RECIPIENT_2 環境変数から読み込まれます。
指定されていない場合は、デフォルトでテストアドレスが使用されます。
ネットワーク時間と手数料の取得
ネットワーク時間と推奨手数料は、転送トランザクション のチュートリアルで説明されているプロセスに従い、それぞれ /node/time GET と /network/fees/transaction GET から取得されます。
埋め込みトランザクションの作成
# Embedded tx 1: Send 5 XYM to Recipient 1
xym_mosaic_id = generate_mosaic_alias_id ( 'symbol.xym' )
embedded_tx_1 = facade . transaction_factory . create_embedded ({
'type' : 'transfer_transaction_v1' ,
'signer_public_key' : signer_key_pair . public_key ,
'recipient_address' : RECIPIENT_1 ,
'mosaics' : [{
'mosaic_id' : xym_mosaic_id ,
'amount' : 5_000_000 # 5 XYM
}]
})
# Embedded tx 2: Send 3 XYM to Recipient 2
embedded_tx_2 = facade . transaction_factory . create_embedded ({
'type' : 'transfer_transaction_v1' ,
'signer_public_key' : signer_key_pair . public_key ,
'recipient_address' : RECIPIENT_2 ,
'mosaics' : [{
'mosaic_id' : xym_mosaic_id ,
'amount' : 3_000_000 # 3 XYM
}]
})
// Embedded tx 1: Send 5 XYM to Recipient 1
const xymMosaicId = generateMosaicAliasId ( 'symbol.xym' );
const embeddedTx1 = facade . transactionFactory . createEmbedded ({
type : 'transfer_transaction_v1' ,
signerPublicKey : signerKeyPair . publicKey . toString (),
recipientAddress : RECIPIENT_1 ,
mosaics : [{
mosaicId : xymMosaicId ,
amount : 5 _000_000n // 5 XYM
}]
});
// Embedded tx 2: Send 3 XYM to Recipient 2
const embeddedTx2 = facade . transactionFactory . createEmbedded ({
type : 'transfer_transaction_v1' ,
signerPublicKey : signerKeyPair . publicKey . toString (),
recipientAddress : RECIPIENT_2 ,
mosaics : [{
mosaicId : xymMosaicId ,
amount : 3 _000_000n // 3 XYM
}]
});
各転送は、アグリゲート内にラップされる埋め込みトランザクション として作成されます。
すべての埋め込みトランザクションは同じアカウントから発生するため、同じ signer_public_key を使用します。
この例では、2つの 転送トランザクション を作成します。
最初の転送では、受信者 1 に 5 XYM を送信します。
2番目の転送では、受信者 2 に 3 XYM を送信します。
すべてが同じ署名者を共有している場合でも、各埋め込みトランザクションで signer_public_key が必要です。
埋め込みトランザクションには、手数料や有効期限のフィールドは含まれません 。
これらは、それを囲むアグリゲートトランザクションから継承されます。
他のトランザクションタイプのバッチ処理
この例では転送トランザクションをバッチ処理していますが、(他のアグリゲートを除く)任意のトランザクションタイプをアグリゲート内に埋め込むことができます。
例えば、モザイクの作成とネームスペースエイリアスの登録を単一のアトミックな操作としてバッチ処理することができます。
アグリゲートトランザクションの構築
# Build the aggregate transaction
embedded_transactions = [ embedded_tx_1 , embedded_tx_2 ]
transaction = facade . transaction_factory . create ({
'type' : 'aggregate_complete_transaction_v3' ,
'signer_public_key' : signer_key_pair . public_key ,
'deadline' : timestamp . add_hours ( 2 ) . timestamp ,
'transactions_hash' :
facade . hash_embedded_transactions ( embedded_transactions ),
'transactions' : embedded_transactions
})
transaction . fee = Amount ( fee_mult * transaction . size )
print ( 'Built aggregate transaction:' )
print ( json . dumps ( transaction . to_json (), indent = 2 ))
// Build the aggregate transaction
const embeddedTransactions = [ embeddedTx1 , embeddedTx2 ];
const transaction = facade . transactionFactory . create ({
type : 'aggregate_complete_transaction_v3' ,
signerPublicKey : signerKeyPair . publicKey . toString (),
deadline : timestamp . addHours ( 2 ). timestamp ,
transactionsHash : facade . static . hashEmbeddedTransactions (
embeddedTransactions ),
transactions : embeddedTransactions
});
transaction . fee = new models . Amount (
feeMult * transaction . size );
console . log ( 'Built aggregate transaction:' );
console . log ( JSON . stringify ( transaction . toJson (), null , 2 ));
手数料は、アグリゲートの合計サイズに基づいて計算されます。
連署は必要ないため、連署バイト用に余分なスペースを確保する必要はありません。
署名とアナウンス
# Sign transaction and generate final payload
signature = facade . sign_transaction ( signer_key_pair , transaction )
json_payload = ( facade . transaction_factory . attach_signature (
transaction , signature ))
# Announce the transaction
announce_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'
)
with urllib . request . urlopen ( announce_request ) as response :
print ( f ' Response: { response . read () . decode () } ' )
// Sign transaction and generate final payload
const signature = facade . signTransaction (
signerKeyPair , transaction );
const jsonPayload = facade . transactionFactory . static
. attachSignature ( transaction , signature );
// Announce the transaction
const announcePath = '/transactions' ;
console . log ( 'Announcing transaction to' , announcePath );
const announceResponse = await fetch (
` ${ NODE_URL }${ announcePath } ` , {
method : 'PUT' ,
headers : { 'Content-Type' : 'application/json' },
body : jsonPayload
});
console . log ( ' Response:' , await announceResponse . text ());
アグリゲートは SymbolFacade.sign_transaction SymbolFacade.signTransaction で署名され、 TransactionFactory.attach_signature SymbolTransactionFactory.attachSignature を使用してペイロードにシリアライズされます。
署名されたペイロードはその後、転送トランザクション チュートリアルで説明されている通常のトランザクションと同じプロセスに従って、 /transactions PUT エンドポイントを使用して ノード にアナウンスされます。
承認の待機
アナウンス後、 /transactionStatus/{hash} GET を使用してトランザクションステータスが監視されます。
ポーリングループは、トランザクションが承認されるか失敗するまで、毎秒ステータスを確認します。
出力
以下に示す出力は、プログラムの典型的な実行結果に対応しています。
Using node https://reference.symboltest.net:3001
Signer public key: 3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29
Signer address: TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
Recipient 1: TCWYXKVYBMO4NBCUF3AXKJMXCGVSYQOS7ZG2TLI (98AD8BAAB80B1DC684542EC175259711AB2C41D2FE4DA9AD)
Recipient 2: TCD4NC5VIE2EEB3BCV5JRLBNJXYDW5Q5JK547MI (9887C68BB54134420761157A98AC2D4DF03B761D4ABBCFB1)
Fetching current network time from /node/time
Network time: 107375591542 ms since nemesis
Fetching recommended fees from /network/fees/transaction
Fee multiplier: 100
Built aggregate transaction:
{
"signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
"version": 3,
"network": 152,
"type": 16705,
"fee": "36000",
"deadline": "107382791542",
"transactions_hash": "006E8D5F5AC08E7D8EEAB2265569A68FF2921722DCE69D7AA61684A8EE5722C0",
"transactions": [
{
"signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
"version": 1,
"network": 152,
"type": 16724,
"recipient_address": "98AD8BAAB80B1DC684542EC175259711AB2C41D2FE4DA9AD",
"mosaics": [
{
"mosaic_id": "16666583871264174062",
"amount": "5000000"
}
],
"message": ""
},
{
"signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
"version": 1,
"network": 152,
"type": 16724,
"recipient_address": "9887C68BB54134420761157A98AC2D4DF03B761D4ABBCFB1",
"mosaics": [
{
"mosaic_id": "16666583871264174062",
"amount": "3000000"
}
],
"message": ""
}
],
"cosignatures": []
}
Announcing transaction to /transactions
Response: {"message":"packet 9 was pushed to the network via /transactions"}
Waiting for confirmation from /transactionStatus/5390D8FAC80B9F76275DA857A3A18B6704FEA8B84026C2893F0041326B8C23D2
Transaction status: unconfirmed
Transaction status: confirmed
Transaction confirmed in 6 seconds
出力の主なポイント:
16行目 ("type": 16705): これが AggregateCompleteTransactionV3 であることを識別します。
26行目と40行目 ("recipient_address"): 2つの埋め込み転送は異なるアカウントをターゲットにしています。
これらは、4〜5行目に出力された Base32 アドレスの16進数エンコード形式です。
29-30行目と43-44行目 ("mosaic_id", "amount"): 各転送は XYM(モザイクエイリアス ID 16666583871264174062)を送信します。
このモザイクの 可分性 は 6 であるため、金額 5000000 と 3000000 はそれぞれ 5 および 3 XYM に対応します。
50行目 ("cosignatures": []): すべての埋め込みトランザクションが同じ署名者を共有しているため、空です。
追加の署名は必要ありません。
アグリゲートトランザクションはアトミックに実行されます。つまり、両方の受信者が XYM の転送を受け取るか、どちらも受け取らないかのいずれかになります。
出力されたトランザクションハッシュ(54行目)を使用して、 Symbol Testnet Explorer でトランザクションを検索できます。
結論
このチュートリアルでは、以下の方法を説明しました。
次のステップ
連署者の追加: 埋め込みトランザクションに複数の署名者が関与し、アナウンス前にオフチェーンで連署できる場合は、アグリゲートコンプリート のチュートリアルを参照してください。
オンチェーンでの署名収集: トランザクションがアナウンスされた後に連署者が署名する必要がある場合は、アグリゲートボンデッド のチュートリアルを参照してください。
手数料のスポンサー: 他のアカウントの代理での手数料支払い のチュートリアルを使用して、あるアカウントが別のアカウントの代わりにトランザクション手数料を支払うことができるようにします。