ボンデッドトランザクションフローのリスニング
上級
ボンデッドアグリゲートトランザクション は、通常の トランザクション よりも複雑なライフサイクルをたどります。
アナウンス後、ネットワークが必要なすべての参加者から 連署 を受け取る partial状態に入ります。
すべての連署が届いて初めて、トランザクションは標準の unconfirmedおよび confirmed状態へと進みます。
このチュートリアルでは、ボンデッドアグリゲートトランザクション チュートリアルのアセットスワップを行いますが、ポーリングの代わりに WebSocket チャネルを使用してボンデッドのライフサイクル全体を監視します。
アカウント A がアグリゲートを構築してアナウンスする一方、アカウント B は WebSocket チャネルを購読し、連署を行い、承認を待ちます。
前提条件
開始する前に、開発環境がセットアップされていることを確認してください。
開発環境のセットアップ を参照してください。
さらに、言語に応じた WebSocket ライブラリをインストールしてください。
また、スワップを完了させるために、 XYM を持つ2つの アカウント と1つのカスタム モザイク が必要です。便宜上、事前に資金供給されたアカウントが提供されていますが、これらはメンテナンスされておらず資金が不足している可能性があります。
自身のアカウントを使用する場合は、以下の手順を完了してください。
アグリゲートトランザクションを開始するためのアカウント(アカウント A)を、 コード または ウォレット を使用して作成します。
スワップに参加するための2つ目のアカウント(アカウント B)を作成します。
トランザクション手数料、転送量、および ハッシュロック のデポジットを支払うための XYM をアカウント A で入手します。
蛇口 (Faucet) からテストネットの通貨を入手する を参照してください。
スワップのためにアカウント B が所有するモザイクを作成します。
モザイクの作成 を参照してください。
完全なコード
このチュートリアルの完全なコード一覧を以下に示します。
詳細な手順ごとの説明は次のセクションで行います。
import asyncio
import json
import os
import urllib.request
from symbolchain.CryptoTypes import PrivateKey
from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.symbol.IdGenerator import generate_mosaic_alias_id
from websockets import connect
NODE_URL = os . getenv ( 'NODE_URL' , 'https://reference.symboltest.net:3001' )
WS_URL = NODE_URL . replace ( 'http' , 'ws' , 1 ) + '/ws'
print ( f 'Using node { NODE_URL } ' )
def announce_transaction ( payload , endpoint , label ):
request = urllib . request . Request (
f ' { NODE_URL }{ endpoint } ' ,
data = payload . encode (),
headers = { 'Content-Type' : 'application/json' },
method = 'PUT'
)
with urllib . request . urlopen ( request ) as response :
response . read ()
print ( label )
ACCOUNT_A_PRIVATE_KEY = os . getenv (
'ACCOUNT_A_PRIVATE_KEY' ,
'0000000000000000000000000000000000000000000000000000000000000000' )
ACCOUNT_B_PRIVATE_KEY = os . getenv (
'ACCOUNT_B_PRIVATE_KEY' ,
'1111111111111111111111111111111111111111111111111111111111111111' )
facade = SymbolFacade ( 'testnet' )
account_a_key_pair = SymbolFacade . KeyPair (
PrivateKey ( ACCOUNT_A_PRIVATE_KEY ))
account_b_key_pair = SymbolFacade . KeyPair (
PrivateKey ( ACCOUNT_B_PRIVATE_KEY ))
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 } ' )
async def main ():
# Fetch recommended fees
with urllib . request . urlopen (
f ' { NODE_URL } /network/fees/transaction'
) as resp :
fee_json = json . loads ( resp . read () . decode ())
fee_multiplier = max (
fee_json [ 'medianFeeMultiplier' ],
fee_json [ 'minFeeMultiplier' ])
# [Account A] Build embedded transactions for the swap
embedded_tx_1 = (
facade . create_embedded_transaction_from_descriptor (
{
'type' : 'transfer_transaction_v1' ,
'recipient_address' : account_b_address ,
'mosaics' : [{
'mosaic_id' : generate_mosaic_alias_id ( 'symbol.xym' ),
'amount' : 10_000_000
}]
},
account_a_key_pair . public_key ))
custom_mosaic_id = 0x6D1314BE751B62C2
embedded_tx_2 = (
facade . create_embedded_transaction_from_descriptor (
{
'type' : 'transfer_transaction_v1' ,
'recipient_address' : account_a_address ,
'mosaics' : [{
'mosaic_id' : custom_mosaic_id ,
'amount' : 1
}]
},
account_b_key_pair . public_key ))
# Build the bonded aggregate transaction
embedded_txs = [ embedded_tx_1 , embedded_tx_2 ]
bonded_tx = facade . create_transaction_from_descriptor (
{
'type' : 'aggregate_bonded_transaction_v3' ,
'transactions_hash' : facade . hash_embedded_transactions (
embedded_txs ),
'transactions' : embedded_txs
},
account_a_key_pair . public_key ,
fee_multiplier ,
2 * 60 * 60 ,
1 )
# Sign the bonded aggregate
bonded_signature = facade . sign_transaction (
account_a_key_pair , bonded_tx )
bonded_payload = facade . transaction_factory . attach_signature (
bonded_tx , bonded_signature )
bonded_hash = facade . hash_transaction ( bonded_tx )
print (
f '[Account A] Bonded aggregate hash: { str ( bonded_hash )[: 16 ] } ...' )
# Create the hash lock transaction
hash_lock = facade . create_transaction_from_descriptor (
{
'type' : 'hash_lock_transaction_v1' ,
'mosaic' : {
'mosaic_id' : generate_mosaic_alias_id ( 'symbol.xym' ),
'amount' : 10_000_000
},
'duration' : 100 ,
'hash' : bonded_hash
},
account_a_key_pair . public_key ,
fee_multiplier ,
2 * 60 * 60 )
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 )
# Confirm hash lock via WebSocket
async with connect ( WS_URL ) as websocket :
response = json . loads (
await websocket . recv ())
uid = response [ 'uid' ]
lock_channels = [
f 'confirmedAdded/ { account_a_address } ' ,
f 'status/ { account_a_address } ' ,
]
for channel in lock_channels :
await websocket . send ( json . dumps (
{ 'uid' : uid , 'subscribe' : channel }
))
# Announce hash lock
announce_transaction (
hash_lock_payload , '/transactions' ,
f '[Account A] Announced hash lock '
f ' { str ( hash_lock_hash )[: 16 ] } ...'
)
# Wait for hash lock confirmation
async for raw_message in websocket :
message = json . loads ( raw_message )
name = message [ 'topic' ] . split ( '/' )[ 0 ]
if name == 'confirmedAdded' :
message_hash = message [ 'data' ][ 'meta' ][ 'hash' ]
if message_hash == str ( hash_lock_hash ):
print ( 'Hash lock confirmed' )
break
if name == 'status' :
status_hash = message [ 'data' ][ 'hash' ]
if status_hash == str ( hash_lock_hash ):
raise RuntimeError (
'Hash lock failed: ' + message [ 'data' ][ 'code' ])
for channel in lock_channels :
await websocket . send ( json . dumps ({
'uid' : uid ,
'unsubscribe' : channel
}))
# [Account B] Connect to WebSocket for bonded flow
async with connect ( WS_URL ) as websocket :
response = json . loads ( await websocket . recv ())
uid = response [ 'uid' ]
print ( f '[Account B] Connected to { WS_URL } with uid { uid } ' )
# Subscribe to bonded transaction channels
channels = [
f 'partialAdded/ { account_b_address } ' ,
f 'partialRemoved/ { account_b_address } ' ,
f 'cosignature/ { account_b_address } ' ,
f 'unconfirmedAdded/ { account_b_address } ' ,
f 'unconfirmedRemoved/ { account_b_address } ' ,
f 'confirmedAdded/ { account_b_address } ' ,
f 'status/ { account_b_address } ' ,
]
for channel in channels :
await websocket . send ( json . dumps (
{ 'uid' : uid , 'subscribe' : channel }
))
name = channel . split ( '/' )[ 0 ]
print ( f '[Account B] Subscribed to { name } channel' )
# [Account A] Announce bonded aggregate
announce_transaction (
bonded_payload , '/transactions/partial' ,
f '[Account A] Announced bonded { str ( bonded_hash )[: 16 ] } ...'
)
# [Account B] Listen for bonded transaction flow
async for raw_message in websocket :
message = json . loads ( raw_message )
topic = message [ 'topic' ]
name = topic . split ( '/' )[ 0 ]
if name == 'cosignature' :
signer = message [ 'data' ][ 'signerPublicKey' ]
print ( f 'cosignature: signer= { signer [: 16 ] } ...' )
elif name == 'status' :
status_hash = message [ 'data' ][ 'hash' ]
print ( f 'status: hash= { status_hash [: 16 ] } ...' )
if status_hash == str ( bonded_hash ):
raise RuntimeError (
'Transaction failed: ' + message [ 'data' ][ 'code' ])
elif name == 'partialAdded' :
message_hash = message [ 'data' ][ 'meta' ][ 'hash' ]
print ( f 'partialAdded: hash= { message_hash [: 16 ] } ...' )
if message_hash == str ( bonded_hash ):
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_transaction (
cosignature_payload ,
'/transactions/cosignature' ,
'[Account B] Submitted cosignature'
)
elif name == 'confirmedAdded' :
message_hash = message [ 'data' ][ 'meta' ][ 'hash' ]
print ( f 'confirmedAdded: hash= { message_hash [: 16 ] } ...' )
if message_hash == str ( bonded_hash ):
print ( 'Transaction '
f ' { str ( bonded_hash )[: 16 ] } ... confirmed' )
break
else :
message_hash = message [ 'data' ][ 'meta' ][ 'hash' ]
print ( f ' { name } : hash= { message_hash [: 16 ] } ...' )
# Unsubscribe before closing
for channel in channels :
await websocket . send ( json . dumps ({
'uid' : uid ,
'unsubscribe' : channel
}))
print ( '[Account B] Unsubscribed from all channels' )
try :
asyncio . run ( main ())
except Exception as error :
print ( error )
Download source
import { Hash256 , PrivateKey } from 'symbol-sdk' ;
import {
SymbolFacade ,
descriptors ,
generateMosaicAliasId ,
models
} from 'symbol-sdk/symbol' ;
const NODE_URL = process . env . NODE_URL ||
'https://reference.symboltest.net:3001' ;
const WS_URL = ` ${ NODE_URL . replace ( 'http' , 'ws' ) } /ws` ;
console . log ( `Using node ${ NODE_URL } ` );
async function announceTransaction ( payload , endpoint , label ) {
await fetch ( ` ${ NODE_URL }${ endpoint } ` , {
method : 'PUT' ,
headers : { 'Content-Type' : 'application/json' },
body : payload
});
console . log ( label );
}
const ACCOUNT_A_PRIVATE_KEY = process . env . ACCOUNT_A_PRIVATE_KEY ||
'0000000000000000000000000000000000000000000000000000000000000000' ;
const ACCOUNT_B_PRIVATE_KEY = process . env . ACCOUNT_B_PRIVATE_KEY ||
'1111111111111111111111111111111111111111111111111111111111111111' ;
const facade = new SymbolFacade ( 'testnet' );
const accountAKeyPair = new SymbolFacade . KeyPair (
new PrivateKey ( ACCOUNT_A_PRIVATE_KEY ));
const accountBKeyPair = new SymbolFacade . KeyPair (
new PrivateKey ( ACCOUNT_B_PRIVATE_KEY ));
const accountAAddress = facade . network
. publicKeyToAddress ( accountAKeyPair . publicKey );
const accountBAddress = facade . network
. publicKeyToAddress ( accountBKeyPair . publicKey );
console . log ( 'Account A:' , accountAAddress . toString ());
console . log ( 'Account B:' , accountBAddress . toString ());
try {
// Fetch recommended fees
const feeResponse = await fetch (
` ${ NODE_URL } /network/fees/transaction` );
const feeJSON = await feeResponse . json ();
const feeMultiplier = Math . max (
feeJSON . medianFeeMultiplier , feeJSON . minFeeMultiplier );
// [Account A] Build embedded transactions for the swap
const embeddedTx1 =
facade . createEmbeddedTransactionFromTypedDescriptor (
new descriptors . TransferTransactionV1Descriptor (
accountBAddress ,
[
new descriptors . UnresolvedMosaicDescriptor (
generateMosaicAliasId ( 'symbol.xym' ),
new models . Amount ( 10 _000_000n ))
],
undefined ),
accountAKeyPair . publicKey );
const customMosaicId = 0x6D1314BE751B62C2n ;
const embeddedTx2 =
facade . createEmbeddedTransactionFromTypedDescriptor (
new descriptors . TransferTransactionV1Descriptor (
accountAAddress ,
[
new descriptors . UnresolvedMosaicDescriptor (
customMosaicId ,
new models . Amount ( 1n ))
],
undefined ),
accountBKeyPair . publicKey );
// Build the bonded aggregate transaction
const embeddedTxs = [ embeddedTx1 , embeddedTx2 ];
const bondedTx = facade . createTransactionFromTypedDescriptor (
new descriptors . AggregateBondedTransactionV3Descriptor (
facade . static . hashEmbeddedTransactions ( embeddedTxs ),
embeddedTxs ,
undefined ),
accountAKeyPair . publicKey ,
feeMultiplier ,
2 * 60 * 60 ,
1 );
// Sign the bonded aggregate
const bondedSignature = facade . signTransaction (
accountAKeyPair , bondedTx );
const bondedPayload = facade . transactionFactory
. static . attachSignature ( bondedTx , bondedSignature );
const bondedHash = facade
. hashTransaction ( bondedTx ). toString ();
console . log ( '[Account A] Bonded aggregate hash: ' +
` ${ bondedHash . substring ( 0 , 16 ) } ...` );
// Create the hash lock transaction
const hashLock = facade . createTransactionFromTypedDescriptor (
new descriptors . HashLockTransactionV1Descriptor (
new descriptors . UnresolvedMosaicDescriptor (
generateMosaicAliasId ( 'symbol.xym' ),
new models . Amount ( 10 _000_000n )),
new models . BlockDuration ( 100n ),
new Hash256 ( bondedHash )),
accountAKeyPair . publicKey ,
feeMultiplier ,
2 * 60 * 60 );
const hashLockSignature = facade . signTransaction (
accountAKeyPair , hashLock );
const hashLockPayload = facade . transactionFactory
. static . attachSignature ( hashLock , hashLockSignature );
const hashLockHash = facade
. hashTransaction ( hashLock ). toString ();
// Confirm hash lock via WebSocket
const lockWebSocket = new WebSocket ( WS_URL );
const lockUid = await new Promise ( resolve => {
lockWebSocket . addEventListener ( 'message' , event => {
const message = JSON . parse ( event . data );
resolve ( message . uid );
}, { once : true });
});
const addressA = accountAAddress . toString ();
const lockChannels = [
`confirmedAdded/ ${ addressA } ` ,
`status/ ${ addressA } `
];
for ( const channel of lockChannels ) {
lockWebSocket . send ( JSON . stringify ({
uid : lockUid , subscribe : channel
}));
}
// Announce hash lock
await announceTransaction (
hashLockPayload , '/transactions' ,
'[Account A] Announced hash lock ' +
` ${ hashLockHash . substring ( 0 , 16 ) } ...` );
// Wait for hash lock confirmation
await new Promise (( resolve , reject ) => {
lockWebSocket . addEventListener ( 'message' , event => {
const message = JSON . parse ( event . data );
const name = message . topic . split ( '/' )[ 0 ];
if ( 'confirmedAdded' === name &&
message . data . meta . hash === hashLockHash ) {
console . log ( 'Hash lock confirmed' );
resolve ();
}
if ( 'status' === name &&
message . data . hash === hashLockHash ) {
reject ( new Error (
`Hash lock failed: ${ message . data . code } ` ));
}
});
});
for ( const channel of lockChannels ) {
lockWebSocket . send ( JSON . stringify ({
uid : lockUid , unsubscribe : channel
}));
}
lockWebSocket . close ();
// [Account B] Connect to WebSocket for bonded flow
const websocket = new WebSocket ( WS_URL );
const uid = await new Promise ( resolve => {
websocket . addEventListener ( 'message' , event => {
const message = JSON . parse ( event . data );
resolve ( message . uid );
}, { once : true });
});
console . log ( `[Account B] Connected to ${ WS_URL } with uid ${ uid } ` );
// Subscribe to bonded transaction channels
const addressB = accountBAddress . toString ();
const channels = [
`partialAdded/ ${ addressB } ` ,
`partialRemoved/ ${ addressB } ` ,
`cosignature/ ${ addressB } ` ,
`unconfirmedAdded/ ${ addressB } ` ,
`unconfirmedRemoved/ ${ addressB } ` ,
`confirmedAdded/ ${ addressB } ` ,
`status/ ${ addressB } `
];
for ( const channel of channels ) {
websocket . send ( JSON . stringify ({ uid , subscribe : channel }));
const name = channel . split ( '/' )[ 0 ];
console . log ( `[Account B] Subscribed to ${ name } channel` );
}
// [Account B] Listen for bonded transaction flow
const confirmed = new Promise (( resolve , reject ) => {
websocket . addEventListener ( 'message' , event => {
const message = JSON . parse ( event . data );
const topic = message . topic ;
const name = topic . split ( '/' )[ 0 ];
if ( 'cosignature' === name ) {
const signer = message . data . signerPublicKey ;
console . log (
`cosignature: signer= ${ signer . substring ( 0 , 16 ) } ...` );
} else if ( 'status' === name ) {
const statusHash = message . data . hash ;
console . log (
`status: hash= ${ statusHash . substring ( 0 , 16 ) } ...` );
if ( statusHash === bondedHash ) {
reject ( new Error (
`Transaction failed: ${ message . data . code } ` ));
}
} else if ( 'partialAdded' === name ) {
const messageHash = message . data . meta . hash ;
console . log ( 'partialAdded: hash=' +
` ${ messageHash . substring ( 0 , 16 ) } ...` );
if ( messageHash === bondedHash ) {
const cosignature =
SymbolFacade . cosignTransactionHash (
accountBKeyPair ,
new Hash256 ( bondedHash ), true );
const cosignaturePayload = JSON . stringify ({
version : cosignature . version . toString (),
signerPublicKey :
cosignature . signerPublicKey . toString (),
signature : cosignature . signature . toString (),
parentHash : cosignature . parentHash . toString ()
});
announceTransaction (
cosignaturePayload ,
'/transactions/cosignature' ,
'[Account B] Submitted cosignature' )
. catch ( err => console . error (
'Cosignature failed:' , err ));
}
} else if ( 'confirmedAdded' === name ) {
const messageHash = message . data . meta . hash ;
console . log ( 'confirmedAdded: hash=' +
` ${ messageHash . substring ( 0 , 16 ) } ...` );
if ( messageHash === bondedHash ) {
console . log ( `Transaction ${
bondedHash . substring ( 0 , 16 ) } ... confirmed` );
resolve ();
}
} else {
const messageHash = message . data . meta . hash ;
console . log (
` ${ name } : hash= ${ messageHash . substring ( 0 , 16 ) } ...` );
}
});
});
// [Account A] Announce bonded aggregate
await announceTransaction (
bondedPayload , '/transactions/partial' ,
'[Account A] Announced bonded ' +
` ${ bondedHash . substring ( 0 , 16 ) } ...` );
// Wait for confirmation via WebSocket
await confirmed ;
// Unsubscribe before closing
for ( const channel of channels )
websocket . send ( JSON . stringify ({ uid , unsubscribe : channel }));
console . log ( '[Account B] Unsubscribed from all channels' );
websocket . close ();
} catch ( error ) {
console . error ( error );
}
Download source
//JAVA 21+
//DEPS org.symbol:symbol-sdk:3.3.1
//DEPS org.glassfish.tyrus.bundles:tyrus-standalone-client:2.2.0
import java.io.IOException ;
import java.net.URI ;
import java.net.http.HttpClient ;
import java.net.http.HttpRequest ;
import java.net.http.HttpResponse ;
import java.net.http.HttpResponse.BodyHandlers ;
import java.util.List ;
import java.util.concurrent.CompletableFuture ;
import com.fasterxml.jackson.databind.JsonNode ;
import com.fasterxml.jackson.databind.ObjectMapper ;
import jakarta.websocket.ClientEndpoint ;
import jakarta.websocket.ContainerProvider ;
import jakarta.websocket.OnMessage ;
import jakarta.websocket.RemoteEndpoint ;
import jakarta.websocket.Session ;
import jakarta.websocket.WebSocketContainer ;
import org.symbol.sdk.CryptoTypes ;
import org.symbol.sdk.facade.SymbolFacade ;
import org.symbol.sdk.symbol.Address ;
import org.symbol.sdk.symbol.IdGenerator ;
import org.symbol.sdk.symbol.KeyPair ;
import org.symbol.sdk.symbol.SymbolTransactionFactory ;
import org.symbol.sdk.symbol.descriptors.* ;
import org.symbol.sdk.symbol.models.* ;
@ClientEndpoint
public final class ListenBondedTransactionFlow {
private static final ObjectMapper JSON_MAPPER = new ObjectMapper ();
private static final HttpClient HTTP_CLIENT =
HttpClient . newHttpClient ();
private CompletableFuture < String > uidFuture ;
private final CompletableFuture < Void > hashLockConfirmed =
new CompletableFuture <> ();
private final CompletableFuture < Void > confirmed =
new CompletableFuture <> ();
private final String nodeUrl = System . getenv (). getOrDefault (
"NODE_URL" , "https://reference.symboltest.net:3001" );
private final String wsUrl = nodeUrl . replaceFirst ( "http" , "ws" )
+ "/ws" ;
private final SymbolFacade facade = new SymbolFacade ( "testnet" );
private KeyPair accountBKeyPair ;
private String bondedHash ;
private String hashLockHash ;
public static void main ( final String [] args ) {
try {
new ListenBondedTransactionFlow (). run ();
} catch ( final Exception ex ) {
System . out . println ( null == ex . getMessage ()
? ex . toString ()
: ex . getMessage ());
}
}
private void run () throws Exception {
System . out . printf ( "Using node %s%n" , nodeUrl );
final String accountAPrivateKey = System . getenv (). getOrDefault (
"ACCOUNT_A_PRIVATE_KEY" , "0" . repeat ( 64 ));
final String accountBPrivateKey = System . getenv (). getOrDefault (
"ACCOUNT_B_PRIVATE_KEY" , "1" . repeat ( 64 ));
final KeyPair accountAKeyPair = new KeyPair (
new CryptoTypes . PrivateKey ( accountAPrivateKey ));
accountBKeyPair = new KeyPair (
new CryptoTypes . PrivateKey ( accountBPrivateKey ));
final Address accountAAddress = facade . network . publicKeyToAddress (
accountAKeyPair . getPublicKey ());
final Address accountBAddress = facade . network . publicKeyToAddress (
accountBKeyPair . getPublicKey ());
System . out . printf ( "Account A: %s%n" , accountAAddress );
System . out . printf ( "Account B: %s%n" , accountBAddress );
// Fetch recommended fees
final JsonNode feeJSON = getJson ( "/network/fees/transaction" );
final long feeMultiplier = Math . max (
feeJSON . get ( "medianFeeMultiplier" ). asLong (),
feeJSON . get ( "minFeeMultiplier" ). asLong ());
// [Account A] Build embedded transactions for the swap
final EmbeddedTransaction embeddedTx1 =
facade . createEmbeddedTransactionFromTypedDescriptor (
new TransferTransactionV1Descriptor (
accountBAddress ,
List . of ( new UnresolvedMosaicDescriptor (
new UnresolvedMosaicId (
IdGenerator . generateMosaicAliasId (
"symbol.xym" )),
new Amount ( 10_000_000 ))),
null ),
accountAKeyPair . getPublicKey ());
final long customMosaicId = 0x6D1314BE751B62C2L ;
final EmbeddedTransaction embeddedTx2 =
facade . createEmbeddedTransactionFromTypedDescriptor (
new TransferTransactionV1Descriptor (
accountAAddress ,
List . of ( new UnresolvedMosaicDescriptor (
new UnresolvedMosaicId ( customMosaicId ),
new Amount ( 1 ))),
null ),
accountBKeyPair . getPublicKey ());
// Build the bonded aggregate transaction
final List < EmbeddedTransaction > embeddedTxs =
List . of ( embeddedTx1 , embeddedTx2 );
final Transaction bondedTx =
facade . createTransactionFromTypedDescriptor (
new AggregateBondedTransactionV3Descriptor (
SymbolFacade . hashEmbeddedTransactions ( embeddedTxs ),
embeddedTxs ,
null ),
accountAKeyPair . getPublicKey (),
feeMultiplier ,
2 * 60 * 60 ,
1 );
// Sign the bonded aggregate
final CryptoTypes . Signature bondedSignature =
facade . signTransaction ( accountAKeyPair , bondedTx );
final String bondedPayload = SymbolTransactionFactory
. attachSignature ( bondedTx , bondedSignature );
bondedHash = facade . hashTransaction ( bondedTx ). toString ();
System . out . println ( "[Account A] Bonded aggregate hash: "
+ bondedHash . substring ( 0 , 16 ) + "..." );
// Create the hash lock transaction
final Transaction hashLock =
facade . createTransactionFromTypedDescriptor (
new HashLockTransactionV1Descriptor (
new UnresolvedMosaicDescriptor (
new UnresolvedMosaicId (
IdGenerator . generateMosaicAliasId (
"symbol.xym" )),
new Amount ( 10_000_000 )),
new BlockDuration ( 100 ),
new CryptoTypes . Hash256 ( bondedHash )),
accountAKeyPair . getPublicKey (),
feeMultiplier ,
2 * 60 * 60 );
final CryptoTypes . Signature hashLockSignature =
facade . signTransaction ( accountAKeyPair , hashLock );
final String hashLockPayload = SymbolTransactionFactory
. attachSignature ( hashLock , hashLockSignature );
hashLockHash = facade . hashTransaction ( hashLock ). toString ();
// Confirm hash lock via WebSocket
uidFuture = new CompletableFuture <> ();
final WebSocketContainer container =
ContainerProvider . getWebSocketContainer ();
final Session lockSession = container . connectToServer (
this , URI . create ( wsUrl ));
final RemoteEndpoint . Basic lockRemote =
lockSession . getBasicRemote ();
final String lockUid = uidFuture . join ();
final String addressA = accountAAddress . toString ();
final List < String > lockChannels = List . of (
"confirmedAdded/" + addressA ,
"status/" + addressA );
for ( final String channel : lockChannels )
lockRemote . sendText ( JSON_MAPPER . createObjectNode ()
. put ( "uid" , lockUid ). put ( "subscribe" , channel )
. toString ());
// Announce hash lock
announceTransaction (
hashLockPayload , "/transactions" ,
"[Account A] Announced hash lock "
+ hashLockHash . substring ( 0 , 16 ) + "..." );
// Wait for hash lock confirmation
hashLockConfirmed . join ();
for ( final String channel : lockChannels )
lockRemote . sendText ( JSON_MAPPER . createObjectNode ()
. put ( "uid" , lockUid ). put ( "unsubscribe" , channel )
. toString ());
lockSession . close ();
// [Account B] Connect to WebSocket for bonded flow
uidFuture = new CompletableFuture <> ();
final Session session = container . connectToServer (
this , URI . create ( wsUrl ));
final RemoteEndpoint . Basic remote = session . getBasicRemote ();
final String uid = uidFuture . join ();
System . out . printf ( "[Account B] Connected to %s with uid %s%n" ,
wsUrl , uid );
// Subscribe to bonded transaction channels
final String addressB = accountBAddress . toString ();
final List < String > channels = List . of (
"partialAdded/" + addressB ,
"partialRemoved/" + addressB ,
"cosignature/" + addressB ,
"unconfirmedAdded/" + addressB ,
"unconfirmedRemoved/" + addressB ,
"confirmedAdded/" + addressB ,
"status/" + addressB );
for ( final String channel : channels ) {
remote . sendText ( JSON_MAPPER . createObjectNode ()
. put ( "uid" , uid ). put ( "subscribe" , channel )
. toString ());
System . out . printf (
"[Account B] Subscribed to %s channel%n" ,
channel . split ( "/" ) [ 0 ] );
}
// [Account A] Announce bonded aggregate
announceTransaction (
bondedPayload , "/transactions/partial" ,
"[Account A] Announced bonded "
+ bondedHash . substring ( 0 , 16 ) + "..." );
// Wait for confirmation via WebSocket
confirmed . join ();
// Unsubscribe before closing
for ( final String channel : channels )
remote . sendText ( JSON_MAPPER . createObjectNode ()
. put ( "uid" , uid ). put ( "unsubscribe" , channel )
. toString ());
System . out . println ( "[Account B] Unsubscribed from all channels" );
session . close ();
}
private JsonNode getJson ( final String path )
throws IOException , InterruptedException {
final HttpRequest request = HttpRequest . newBuilder (
URI . create ( nodeUrl + path )). GET (). build ();
final HttpResponse < String > response = HTTP_CLIENT . send (
request , BodyHandlers . ofString ());
return JSON_MAPPER . readTree ( response . body ());
}
private void announceTransaction (
final String payload ,
final String endpoint ,
final String label
)
throws IOException , InterruptedException {
final HttpRequest request = HttpRequest . newBuilder (
URI . create ( nodeUrl + endpoint ))
. header ( "Content-Type" , "application/json" )
. PUT ( HttpRequest . BodyPublishers . ofString ( payload ))
. build ();
HTTP_CLIENT . send ( request , BodyHandlers . ofString ());
System . out . println ( label );
}
// [Account B] Listen for bonded transaction flow
@OnMessage
public void onMessage ( final String payload ) throws Exception {
final JsonNode message = JSON_MAPPER . readTree ( payload );
// Special case for the initial handshake message
if ( message . has ( "uid" )) {
uidFuture . complete ( message . get ( "uid" ). asText ());
return ;
}
final String topic = message . get ( "topic" ). asText ();
final String name = topic . split ( "/" ) [ 0 ] ;
if ( "cosignature" . equals ( name )) {
final String signer = message . get ( "data" )
. get ( "signerPublicKey" ). asText ();
System . out . println ( "cosignature: signer="
+ signer . substring ( 0 , 16 ) + "..." );
} else if ( "status" . equals ( name )) {
handleStatus ( message );
} else if ( "partialAdded" . equals ( name )) {
handlePartialAdded ( message );
} else if ( "confirmedAdded" . equals ( name )) {
handleConfirmedAdded ( message );
} else {
final String messageHash = message . get ( "data" )
. get ( "meta" ). get ( "hash" ). asText ();
System . out . printf ( "%s: hash=%s...%n" ,
name , messageHash . substring ( 0 , 16 ));
}
}
private void handleStatus ( final JsonNode message ) {
final String statusHash = message . get ( "data" ). get ( "hash" ). asText ();
System . out . println ( "status: hash="
+ statusHash . substring ( 0 , 16 ) + "..." );
if ( statusHash . equals ( hashLockHash ))
hashLockConfirmed . completeExceptionally ( new IOException (
"Hash lock failed: "
+ message . get ( "data" ). get ( "code" ). asText ()));
if ( statusHash . equals ( bondedHash ))
confirmed . completeExceptionally ( new IOException (
"Transaction failed: "
+ message . get ( "data" ). get ( "code" ). asText ()));
}
private void handlePartialAdded ( final JsonNode message )
throws IOException , InterruptedException {
final String messageHash = message . get ( "data" )
. get ( "meta" ). get ( "hash" ). asText ();
System . out . println ( "partialAdded: hash="
+ messageHash . substring ( 0 , 16 ) + "..." );
if ( messageHash . equals ( bondedHash )) {
final DetachedCosignature cosignature =
SymbolFacade . cosignTransactionHashDetached (
accountBKeyPair ,
new CryptoTypes . Hash256 ( bondedHash ));
final String cosignaturePayload = JSON_MAPPER
. writeValueAsString ( cosignature . toJson ());
announceTransaction (
cosignaturePayload , "/transactions/cosignature" ,
"[Account B] Submitted cosignature" );
}
}
private void handleConfirmedAdded ( final JsonNode message ) {
final String messageHash = message . get ( "data" )
. get ( "meta" ). get ( "hash" ). asText ();
System . out . println ( "confirmedAdded: hash="
+ messageHash . substring ( 0 , 16 ) + "..." );
if ( messageHash . equals ( hashLockHash )) {
System . out . println ( "Hash lock confirmed" );
hashLockConfirmed . complete ( null );
}
if ( messageHash . equals ( bondedHash )) {
System . out . println ( "Transaction "
+ bondedHash . substring ( 0 , 16 ) + "... confirmed" );
confirmed . complete ( null );
}
}
}
Download source
ボンデッドアグリゲートトランザクションには、2つの異なる役割が含まれます。アグリゲートを構築、署名、アナウンスする 開始者 (アカウント A)と、WebSocket チャネルを監視し、トランザクションを検証した後に連署する1人以上の 連署者 (アカウント B)です。
実際には、それぞれの役割は別々のマシンの別々のプログラムとして実行され、すべての連署者は開始者がボンデッドアグリゲートを送信する前にすでにリスニング状態(待ち受け状態)になっている必要があります。
このチュートリアルでは、簡略化のため両方の役割を1つのスクリプトにまとめています。
コード解説
アカウント A: アカウントの設定
この例では、簡略化のため1つのスクリプトに両方の 秘密鍵 を含めています。実際には、各当事者が自身のマシンで 署名 します。
アカウント A は、埋め込みトランザクション の署名者としてアカウント B を設定し、B の アドレス を派生させるために、アカウント B の 公開鍵 のみを必要とします。
環境変数 ACCOUNT_A_PRIVATE_KEY と ACCOUNT_B_PRIVATE_KEY で各アカウントの鍵を設定します。設定されない場合は、デフォルトでテストキーが使用されます。
自身の鍵を使用する場合は、アカウント A が XYM を持ち、アカウント B がスワップ用のカスタムモザイクを保持していることを確認してください。
アドレスは、ファサードのネットワーク設定を使用して公開鍵から派生します。
アカウント A: アグリゲートの構築とハッシュロックのアナウンス
# [Account A] Build embedded transactions for the swap
embedded_tx_1 = (
facade . create_embedded_transaction_from_descriptor (
{
'type' : 'transfer_transaction_v1' ,
'recipient_address' : account_b_address ,
'mosaics' : [{
'mosaic_id' : generate_mosaic_alias_id ( 'symbol.xym' ),
'amount' : 10_000_000
}]
},
account_a_key_pair . public_key ))
custom_mosaic_id = 0x6D1314BE751B62C2
embedded_tx_2 = (
facade . create_embedded_transaction_from_descriptor (
{
'type' : 'transfer_transaction_v1' ,
'recipient_address' : account_a_address ,
'mosaics' : [{
'mosaic_id' : custom_mosaic_id ,
'amount' : 1
}]
},
account_b_key_pair . public_key ))
# Build the bonded aggregate transaction
embedded_txs = [ embedded_tx_1 , embedded_tx_2 ]
bonded_tx = facade . create_transaction_from_descriptor (
{
'type' : 'aggregate_bonded_transaction_v3' ,
'transactions_hash' : facade . hash_embedded_transactions (
embedded_txs ),
'transactions' : embedded_txs
},
account_a_key_pair . public_key ,
fee_multiplier ,
2 * 60 * 60 ,
1 )
# Sign the bonded aggregate
bonded_signature = facade . sign_transaction (
account_a_key_pair , bonded_tx )
bonded_payload = facade . transaction_factory . attach_signature (
bonded_tx , bonded_signature )
bonded_hash = facade . hash_transaction ( bonded_tx )
print (
f '[Account A] Bonded aggregate hash: { str ( bonded_hash )[: 16 ] } ...' )
# Create the hash lock transaction
hash_lock = facade . create_transaction_from_descriptor (
{
'type' : 'hash_lock_transaction_v1' ,
'mosaic' : {
'mosaic_id' : generate_mosaic_alias_id ( 'symbol.xym' ),
'amount' : 10_000_000
},
'duration' : 100 ,
'hash' : bonded_hash
},
account_a_key_pair . public_key ,
fee_multiplier ,
2 * 60 * 60 )
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 )
# Confirm hash lock via WebSocket
async with connect ( WS_URL ) as websocket :
response = json . loads (
await websocket . recv ())
uid = response [ 'uid' ]
lock_channels = [
f 'confirmedAdded/ { account_a_address } ' ,
f 'status/ { account_a_address } ' ,
]
for channel in lock_channels :
await websocket . send ( json . dumps (
{ 'uid' : uid , 'subscribe' : channel }
))
# Announce hash lock
announce_transaction (
hash_lock_payload , '/transactions' ,
f '[Account A] Announced hash lock '
f ' { str ( hash_lock_hash )[: 16 ] } ...'
)
# Wait for hash lock confirmation
async for raw_message in websocket :
message = json . loads ( raw_message )
name = message [ 'topic' ] . split ( '/' )[ 0 ]
if name == 'confirmedAdded' :
message_hash = message [ 'data' ][ 'meta' ][ 'hash' ]
if message_hash == str ( hash_lock_hash ):
print ( 'Hash lock confirmed' )
break
if name == 'status' :
status_hash = message [ 'data' ][ 'hash' ]
if status_hash == str ( hash_lock_hash ):
raise RuntimeError (
'Hash lock failed: ' + message [ 'data' ][ 'code' ])
for channel in lock_channels :
await websocket . send ( json . dumps ({
'uid' : uid ,
'unsubscribe' : channel
}))
// [Account A] Build embedded transactions for the swap
const embeddedTx1 =
facade . createEmbeddedTransactionFromTypedDescriptor (
new descriptors . TransferTransactionV1Descriptor (
accountBAddress ,
[
new descriptors . UnresolvedMosaicDescriptor (
generateMosaicAliasId ( 'symbol.xym' ),
new models . Amount ( 10 _000_000n ))
],
undefined ),
accountAKeyPair . publicKey );
const customMosaicId = 0x6D1314BE751B62C2n ;
const embeddedTx2 =
facade . createEmbeddedTransactionFromTypedDescriptor (
new descriptors . TransferTransactionV1Descriptor (
accountAAddress ,
[
new descriptors . UnresolvedMosaicDescriptor (
customMosaicId ,
new models . Amount ( 1n ))
],
undefined ),
accountBKeyPair . publicKey );
// Build the bonded aggregate transaction
const embeddedTxs = [ embeddedTx1 , embeddedTx2 ];
const bondedTx = facade . createTransactionFromTypedDescriptor (
new descriptors . AggregateBondedTransactionV3Descriptor (
facade . static . hashEmbeddedTransactions ( embeddedTxs ),
embeddedTxs ,
undefined ),
accountAKeyPair . publicKey ,
feeMultiplier ,
2 * 60 * 60 ,
1 );
// Sign the bonded aggregate
const bondedSignature = facade . signTransaction (
accountAKeyPair , bondedTx );
const bondedPayload = facade . transactionFactory
. static . attachSignature ( bondedTx , bondedSignature );
const bondedHash = facade
. hashTransaction ( bondedTx ). toString ();
console . log ( '[Account A] Bonded aggregate hash: ' +
` ${ bondedHash . substring ( 0 , 16 ) } ...` );
// Create the hash lock transaction
const hashLock = facade . createTransactionFromTypedDescriptor (
new descriptors . HashLockTransactionV1Descriptor (
new descriptors . UnresolvedMosaicDescriptor (
generateMosaicAliasId ( 'symbol.xym' ),
new models . Amount ( 10 _000_000n )),
new models . BlockDuration ( 100n ),
new Hash256 ( bondedHash )),
accountAKeyPair . publicKey ,
feeMultiplier ,
2 * 60 * 60 );
const hashLockSignature = facade . signTransaction (
accountAKeyPair , hashLock );
const hashLockPayload = facade . transactionFactory
. static . attachSignature ( hashLock , hashLockSignature );
const hashLockHash = facade
. hashTransaction ( hashLock ). toString ();
// Confirm hash lock via WebSocket
const lockWebSocket = new WebSocket ( WS_URL );
const lockUid = await new Promise ( resolve => {
lockWebSocket . addEventListener ( 'message' , event => {
const message = JSON . parse ( event . data );
resolve ( message . uid );
}, { once : true });
});
const addressA = accountAAddress . toString ();
const lockChannels = [
`confirmedAdded/ ${ addressA } ` ,
`status/ ${ addressA } `
];
for ( const channel of lockChannels ) {
lockWebSocket . send ( JSON . stringify ({
uid : lockUid , subscribe : channel
}));
}
// Announce hash lock
await announceTransaction (
hashLockPayload , '/transactions' ,
'[Account A] Announced hash lock ' +
` ${ hashLockHash . substring ( 0 , 16 ) } ...` );
// Wait for hash lock confirmation
await new Promise (( resolve , reject ) => {
lockWebSocket . addEventListener ( 'message' , event => {
const message = JSON . parse ( event . data );
const name = message . topic . split ( '/' )[ 0 ];
if ( 'confirmedAdded' === name &&
message . data . meta . hash === hashLockHash ) {
console . log ( 'Hash lock confirmed' );
resolve ();
}
if ( 'status' === name &&
message . data . hash === hashLockHash ) {
reject ( new Error (
`Hash lock failed: ${ message . data . code } ` ));
}
});
});
for ( const channel of lockChannels ) {
lockWebSocket . send ( JSON . stringify ({
uid : lockUid , unsubscribe : channel
}));
}
lockWebSocket . close ();
// [Account A] Build embedded transactions for the swap
final EmbeddedTransaction embeddedTx1 =
facade . createEmbeddedTransactionFromTypedDescriptor (
new TransferTransactionV1Descriptor (
accountBAddress ,
List . of ( new UnresolvedMosaicDescriptor (
new UnresolvedMosaicId (
IdGenerator . generateMosaicAliasId (
"symbol.xym" )),
new Amount ( 10_000_000 ))),
null ),
accountAKeyPair . getPublicKey ());
final long customMosaicId = 0x6D1314BE751B62C2L ;
final EmbeddedTransaction embeddedTx2 =
facade . createEmbeddedTransactionFromTypedDescriptor (
new TransferTransactionV1Descriptor (
accountAAddress ,
List . of ( new UnresolvedMosaicDescriptor (
new UnresolvedMosaicId ( customMosaicId ),
new Amount ( 1 ))),
null ),
accountBKeyPair . getPublicKey ());
// Build the bonded aggregate transaction
final List < EmbeddedTransaction > embeddedTxs =
List . of ( embeddedTx1 , embeddedTx2 );
final Transaction bondedTx =
facade . createTransactionFromTypedDescriptor (
new AggregateBondedTransactionV3Descriptor (
SymbolFacade . hashEmbeddedTransactions ( embeddedTxs ),
embeddedTxs ,
null ),
accountAKeyPair . getPublicKey (),
feeMultiplier ,
2 * 60 * 60 ,
1 );
// Sign the bonded aggregate
final CryptoTypes . Signature bondedSignature =
facade . signTransaction ( accountAKeyPair , bondedTx );
final String bondedPayload = SymbolTransactionFactory
. attachSignature ( bondedTx , bondedSignature );
bondedHash = facade . hashTransaction ( bondedTx ). toString ();
System . out . println ( "[Account A] Bonded aggregate hash: "
+ bondedHash . substring ( 0 , 16 ) + "..." );
// Create the hash lock transaction
final Transaction hashLock =
facade . createTransactionFromTypedDescriptor (
new HashLockTransactionV1Descriptor (
new UnresolvedMosaicDescriptor (
new UnresolvedMosaicId (
IdGenerator . generateMosaicAliasId (
"symbol.xym" )),
new Amount ( 10_000_000 )),
new BlockDuration ( 100 ),
new CryptoTypes . Hash256 ( bondedHash )),
accountAKeyPair . getPublicKey (),
feeMultiplier ,
2 * 60 * 60 );
final CryptoTypes . Signature hashLockSignature =
facade . signTransaction ( accountAKeyPair , hashLock );
final String hashLockPayload = SymbolTransactionFactory
. attachSignature ( hashLock , hashLockSignature );
hashLockHash = facade . hashTransaction ( hashLock ). toString ();
// Confirm hash lock via WebSocket
uidFuture = new CompletableFuture <> ();
final WebSocketContainer container =
ContainerProvider . getWebSocketContainer ();
final Session lockSession = container . connectToServer (
this , URI . create ( wsUrl ));
final RemoteEndpoint . Basic lockRemote =
lockSession . getBasicRemote ();
final String lockUid = uidFuture . join ();
final String addressA = accountAAddress . toString ();
final List < String > lockChannels = List . of (
"confirmedAdded/" + addressA ,
"status/" + addressA );
for ( final String channel : lockChannels )
lockRemote . sendText ( JSON_MAPPER . createObjectNode ()
. put ( "uid" , lockUid ). put ( "subscribe" , channel )
. toString ());
// Announce hash lock
announceTransaction (
hashLockPayload , "/transactions" ,
"[Account A] Announced hash lock "
+ hashLockHash . substring ( 0 , 16 ) + "..." );
// Wait for hash lock confirmation
hashLockConfirmed . join ();
for ( final String channel : lockChannels )
lockRemote . sendText ( JSON_MAPPER . createObjectNode ()
. put ( "uid" , lockUid ). put ( "unsubscribe" , channel )
. toString ());
lockSession . close ();
アカウント A は、 ボンデッドアグリゲートトランザクション チュートリアルで説明されているのと同じパターンに従い、アカウント B の 1 つのカスタムモザイクと 10 XYM を交換するボンデッドアグリゲートを作成して署名し、必要なハッシュロックをアナウンスします。
唯一の違いは、ハッシュロックを確認するために /transactionStatus/{hash} GET をポーリングする代わりに、このチュートリアルでは トランザクションフローのリスニング チュートリアルで説明されているものと同じアプローチに従って WebSocket を使用することです。
アカウント B: 接続とチャネルのサブスクライブ
# [Account B] Connect to WebSocket for bonded flow
async with connect ( WS_URL ) as websocket :
response = json . loads ( await websocket . recv ())
uid = response [ 'uid' ]
print ( f '[Account B] Connected to { WS_URL } with uid { uid } ' )
# Subscribe to bonded transaction channels
channels = [
f 'partialAdded/ { account_b_address } ' ,
f 'partialRemoved/ { account_b_address } ' ,
f 'cosignature/ { account_b_address } ' ,
f 'unconfirmedAdded/ { account_b_address } ' ,
f 'unconfirmedRemoved/ { account_b_address } ' ,
f 'confirmedAdded/ { account_b_address } ' ,
f 'status/ { account_b_address } ' ,
]
for channel in channels :
await websocket . send ( json . dumps (
{ 'uid' : uid , 'subscribe' : channel }
))
name = channel . split ( '/' )[ 0 ]
print ( f '[Account B] Subscribed to { name } channel' )
// [Account B] Connect to WebSocket for bonded flow
const websocket = new WebSocket ( WS_URL );
const uid = await new Promise ( resolve => {
websocket . addEventListener ( 'message' , event => {
const message = JSON . parse ( event . data );
resolve ( message . uid );
}, { once : true });
});
console . log ( `[Account B] Connected to ${ WS_URL } with uid ${ uid } ` );
// Subscribe to bonded transaction channels
const addressB = accountBAddress . toString ();
const channels = [
`partialAdded/ ${ addressB } ` ,
`partialRemoved/ ${ addressB } ` ,
`cosignature/ ${ addressB } ` ,
`unconfirmedAdded/ ${ addressB } ` ,
`unconfirmedRemoved/ ${ addressB } ` ,
`confirmedAdded/ ${ addressB } ` ,
`status/ ${ addressB } `
];
for ( const channel of channels ) {
websocket . send ( JSON . stringify ({ uid , subscribe : channel }));
const name = channel . split ( '/' )[ 0 ];
console . log ( `[Account B] Subscribed to ${ name } channel` );
}
// [Account B] Connect to WebSocket for bonded flow
uidFuture = new CompletableFuture <> ();
final Session session = container . connectToServer (
this , URI . create ( wsUrl ));
final RemoteEndpoint . Basic remote = session . getBasicRemote ();
final String uid = uidFuture . join ();
System . out . printf ( "[Account B] Connected to %s with uid %s%n" ,
wsUrl , uid );
// Subscribe to bonded transaction channels
final String addressB = accountBAddress . toString ();
final List < String > channels = List . of (
"partialAdded/" + addressB ,
"partialRemoved/" + addressB ,
"cosignature/" + addressB ,
"unconfirmedAdded/" + addressB ,
"unconfirmedRemoved/" + addressB ,
"confirmedAdded/" + addressB ,
"status/" + addressB );
for ( final String channel : channels ) {
remote . sendText ( JSON_MAPPER . createObjectNode ()
. put ( "uid" , uid ). put ( "subscribe" , channel )
. toString ());
System . out . printf (
"[Account B] Subscribed to %s channel%n" ,
channel . split ( "/" ) [ 0 ] );
}
このスニペットでは、 NODE_URL 環境変数を使用して Symbol API ノード を設定します。値が指定されない場合は、デフォルト値が使用されます。
WebSocket URL は、HTTP プロトコルを WebSocket プロトコルに置き換え、 /ws を追加することで NODE_URL から派生します。
アカウント B は WebSocket 接続を開き、ボンデッドトランザクションのライフサイクルを監視するために、自身のアドレスをスコープとするチャネルをサブスクライブします。
アカウント B はアグリゲートの参加者であるため、ノードはトランザクションのすべてのライフサイクルイベントをアカウント B のアドレスに配信します。
通常のトランザクション で使用されるチャネルに加えて、ボンデッドアグリゲートは追加のチャネルを使用します。
アカウント A: ボンデッドアグリゲートのアナウンス
アカウント B のサブスクライブが完了すると、アカウント A は通常の /transactions PUT エンドポイントではなく、 /transactions/partial PUT にボンデッドアグリゲートをアナウンスします。
アカウント B: WebSocket メッセージの処理と連署
# [Account B] Listen for bonded transaction flow
async for raw_message in websocket :
message = json . loads ( raw_message )
topic = message [ 'topic' ]
name = topic . split ( '/' )[ 0 ]
if name == 'cosignature' :
signer = message [ 'data' ][ 'signerPublicKey' ]
print ( f 'cosignature: signer= { signer [: 16 ] } ...' )
elif name == 'status' :
status_hash = message [ 'data' ][ 'hash' ]
print ( f 'status: hash= { status_hash [: 16 ] } ...' )
if status_hash == str ( bonded_hash ):
raise RuntimeError (
'Transaction failed: ' + message [ 'data' ][ 'code' ])
elif name == 'partialAdded' :
message_hash = message [ 'data' ][ 'meta' ][ 'hash' ]
print ( f 'partialAdded: hash= { message_hash [: 16 ] } ...' )
if message_hash == str ( bonded_hash ):
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_transaction (
cosignature_payload ,
'/transactions/cosignature' ,
'[Account B] Submitted cosignature'
)
elif name == 'confirmedAdded' :
message_hash = message [ 'data' ][ 'meta' ][ 'hash' ]
print ( f 'confirmedAdded: hash= { message_hash [: 16 ] } ...' )
if message_hash == str ( bonded_hash ):
print ( 'Transaction '
f ' { str ( bonded_hash )[: 16 ] } ... confirmed' )
break
else :
message_hash = message [ 'data' ][ 'meta' ][ 'hash' ]
print ( f ' { name } : hash= { message_hash [: 16 ] } ...' )
// [Account B] Listen for bonded transaction flow
const confirmed = new Promise (( resolve , reject ) => {
websocket . addEventListener ( 'message' , event => {
const message = JSON . parse ( event . data );
const topic = message . topic ;
const name = topic . split ( '/' )[ 0 ];
if ( 'cosignature' === name ) {
const signer = message . data . signerPublicKey ;
console . log (
`cosignature: signer= ${ signer . substring ( 0 , 16 ) } ...` );
} else if ( 'status' === name ) {
const statusHash = message . data . hash ;
console . log (
`status: hash= ${ statusHash . substring ( 0 , 16 ) } ...` );
if ( statusHash === bondedHash ) {
reject ( new Error (
`Transaction failed: ${ message . data . code } ` ));
}
} else if ( 'partialAdded' === name ) {
const messageHash = message . data . meta . hash ;
console . log ( 'partialAdded: hash=' +
` ${ messageHash . substring ( 0 , 16 ) } ...` );
if ( messageHash === bondedHash ) {
const cosignature =
SymbolFacade . cosignTransactionHash (
accountBKeyPair ,
new Hash256 ( bondedHash ), true );
const cosignaturePayload = JSON . stringify ({
version : cosignature . version . toString (),
signerPublicKey :
cosignature . signerPublicKey . toString (),
signature : cosignature . signature . toString (),
parentHash : cosignature . parentHash . toString ()
});
announceTransaction (
cosignaturePayload ,
'/transactions/cosignature' ,
'[Account B] Submitted cosignature' )
. catch ( err => console . error (
'Cosignature failed:' , err ));
}
} else if ( 'confirmedAdded' === name ) {
const messageHash = message . data . meta . hash ;
console . log ( 'confirmedAdded: hash=' +
` ${ messageHash . substring ( 0 , 16 ) } ...` );
if ( messageHash === bondedHash ) {
console . log ( `Transaction ${
bondedHash . substring ( 0 , 16 ) } ... confirmed` );
resolve ();
}
} else {
const messageHash = message . data . meta . hash ;
console . log (
` ${ name } : hash= ${ messageHash . substring ( 0 , 16 ) } ...` );
}
});
});
// [Account B] Listen for bonded transaction flow
@OnMessage
public void onMessage ( final String payload ) throws Exception {
final JsonNode message = JSON_MAPPER . readTree ( payload );
// Special case for the initial handshake message
if ( message . has ( "uid" )) {
uidFuture . complete ( message . get ( "uid" ). asText ());
return ;
}
final String topic = message . get ( "topic" ). asText ();
final String name = topic . split ( "/" ) [ 0 ] ;
if ( "cosignature" . equals ( name )) {
final String signer = message . get ( "data" )
. get ( "signerPublicKey" ). asText ();
System . out . println ( "cosignature: signer="
+ signer . substring ( 0 , 16 ) + "..." );
} else if ( "status" . equals ( name )) {
handleStatus ( message );
} else if ( "partialAdded" . equals ( name )) {
handlePartialAdded ( message );
} else if ( "confirmedAdded" . equals ( name )) {
handleConfirmedAdded ( message );
} else {
final String messageHash = message . get ( "data" )
. get ( "meta" ). get ( "hash" ). asText ();
System . out . printf ( "%s: hash=%s...%n" ,
name , messageHash . substring ( 0 , 16 ));
}
}
private void handleStatus ( final JsonNode message ) {
final String statusHash = message . get ( "data" ). get ( "hash" ). asText ();
System . out . println ( "status: hash="
+ statusHash . substring ( 0 , 16 ) + "..." );
if ( statusHash . equals ( hashLockHash ))
hashLockConfirmed . completeExceptionally ( new IOException (
"Hash lock failed: "
+ message . get ( "data" ). get ( "code" ). asText ()));
if ( statusHash . equals ( bondedHash ))
confirmed . completeExceptionally ( new IOException (
"Transaction failed: "
+ message . get ( "data" ). get ( "code" ). asText ()));
}
private void handlePartialAdded ( final JsonNode message )
throws IOException , InterruptedException {
final String messageHash = message . get ( "data" )
. get ( "meta" ). get ( "hash" ). asText ();
System . out . println ( "partialAdded: hash="
+ messageHash . substring ( 0 , 16 ) + "..." );
if ( messageHash . equals ( bondedHash )) {
final DetachedCosignature cosignature =
SymbolFacade . cosignTransactionHashDetached (
accountBKeyPair ,
new CryptoTypes . Hash256 ( bondedHash ));
final String cosignaturePayload = JSON_MAPPER
. writeValueAsString ( cosignature . toJson ());
announceTransaction (
cosignaturePayload , "/transactions/cosignature" ,
"[Account B] Submitted cosignature" );
}
}
private void handleConfirmedAdded ( final JsonNode message ) {
final String messageHash = message . get ( "data" )
. get ( "meta" ). get ( "hash" ). asText ();
System . out . println ( "confirmedAdded: hash="
+ messageHash . substring ( 0 , 16 ) + "..." );
if ( messageHash . equals ( hashLockHash )) {
System . out . println ( "Hash lock confirmed" );
hashLockConfirmed . complete ( null );
}
if ( messageHash . equals ( bondedHash )) {
System . out . println ( "Transaction "
+ bondedHash . substring ( 0 , 16 ) + "... confirmed" );
confirmed . complete ( null );
}
}
アカウント B は受信メッセージをリスニングし、チャネルごとに振り分けます。
メッセージのスキーマは、 cosignature メッセージを除き、 通常のトランザクションフロー チュートリアルと同じです。 cosignature メッセージは CosignatureDTO スキーマに従い、他のチャネルで使用される meta.hash フィールドは含まれません。
重要なアクションは partialAdded で発生します。ハッシュが期待されるアグリゲートと一致した場合、アカウント B は detached パラメータを Truetruetrue に設定した SymbolFacade.cosign_transaction_hash SymbolFacade.cosignTransactionHash SymbolFacade.cosignTransactionHash を使用してトランザクションに連署し、 /transactions/cosignature PUT に連署をアナウンスします。
より深い検証を行うために、アカウント B は /transactions/partial/{transactionId} GET から完全なトランザクションを取得し、内容を検査してから連署するかどうかを決定することができます。
成功したボンデッドアグリゲートの期待されるメッセージシーケンスは、テキストブックの トランザクションのライフサイクル セクションで説明されています。
partialAdded: ボンデッドアグリゲートがパーシャル(部分的)キャッシュに入り、連署を待ちます。
cosignature: アカウント B からの連署が追加されます。
unconfirmedAdded: 完全に署名されたトランザクションが 未承認トランザクションプール に入ります。
partialRemoved: トランザクションが partial 状態を抜けます。
unconfirmedRemoved: トランザクションが未承認プールを抜けます。
confirmedAdded: トランザクションがブロック内で承認されます。
アカウント B: チャネルのサブスクライブ解除
承認後、アカウント B は接続を閉じる前に、すべてのチャネルのサブスクライブ解除メッセージを送信します。
出力
Using node https://reference.symboltest.net:3001
Account A: TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
Account B: TCWYXKVYBMO4NBCUF3AXKJMXCGVSYQOS7ZG2TLI
[Account A] Bonded aggregate hash: 1B48628680C99C5F...
[Account A] Announced hash lock A9CD8DE6E4CD1EDF...
Hash lock confirmed
[Account B] Connected to wss://reference.symboltest.net:3001/ws with uid bPpqa5v2vhja9J5wUTjP8OIc5Io=
[Account B] Subscribed to partialAdded channel
[Account B] Subscribed to partialRemoved channel
[Account B] Subscribed to cosignature channel
[Account B] Subscribed to unconfirmedAdded channel
[Account B] Subscribed to unconfirmedRemoved channel
[Account B] Subscribed to confirmedAdded channel
[Account B] Subscribed to status channel
[Account A] Announced bonded 1B48628680C99C5F...
partialAdded: hash=1B48628680C99C5F...
[Account B] Submitted cosignature
cosignature: signer=D04AB232742BB4AB...
unconfirmedAdded: hash=1B48628680C99C5F...
partialRemoved: hash=1B48628680C99C5F...
unconfirmedRemoved: hash=1B48628680C99C5F...
confirmedAdded: hash=1B48628680C99C5F...
Transaction 1B48628680C99C5F... confirmed
[Account B] Unsubscribed from all channels
出力のポイント:
アカウント (2-3行目): アカウント A(開始者)とアカウント B(連署者)のアドレス。
ハッシュロック (6行目): ボンデッドアグリゲートハッシュが計算され、ハッシュロックがアナウンスされ、その承認が WebSocket 経由で受信されます。
接続 (7行目): WebSocket 接続が確立され、サーバーは一意の uid を返します。
サブスクリプション (8-14行目): ( status を含む)7つすべてのボンデッドトランザクションチャネルがサブスクライブされます。
アナウンス (15行目): ボンデッドアグリゲートが /transactions/partial にアナウンスされます。
連署 (16-18行目): アグリゲートが partialAdded に入り、アカウント B が連署を送信し、 cosignature チャネルがそれを受信したことを確認します。
承認 (19-22行目): 完全に署名されたトランザクションが未承認プールに入り( unconfirmedAdded )、 partial 状態を抜け( partialRemoved )、 unconfirmedRemoved を経て、最後に confirmedAdded になります。
結論
このチュートリアルでは、以下の方法を説明しました。