The status/{address}WS WebSocket channel sends real-time notifications when a transaction related to a
specific account is rejected by the network.
Instead of polling the /transactionStatus/{hash}GET endpoint, the status channel pushes error details as soon as
the network rejects a transaction.
This tutorial shows how to subscribe to the status channel and handle rejection notifications.
To test the listener, the code deliberately sends an invalid transaction that the network will reject.
importasyncioimportjsonimportosimporturllib.requestfromsymbolchain.CryptoTypesimportPrivateKeyfromsymbolchain.facade.SymbolFacadeimportSymbolFacadefromsymbolchain.symbol.NetworkimportNetworkTimestampfromsymbolchain.symbol.IdGeneratorimportgenerate_mosaic_alias_idfromsymbolchain.scimportAmountfromwebsocketsimportconnectNODE_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}')MONITOR_ADDRESS=os.getenv('MONITOR_ADDRESS','TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I')print(f'Monitoring address: {MONITOR_ADDRESS}')SIGNER_PRIVATE_KEY=os.getenv('SIGNER_PRIVATE_KEY','0000000000000000000000000000000000000000000000000000000000000000')facade=SymbolFacade('testnet')signer_key_pair=SymbolFacade.KeyPair(PrivateKey(SIGNER_PRIVATE_KEY))asyncdefmain():asyncwithconnect(WS_URL)aswebsocket:# Connect to WebSocketresponse=json.loads(awaitwebsocket.recv())uid=response['uid']print(f'Connected to {WS_URL} with uid {uid}')# Subscribe to status channelchannel=f'status/{MONITOR_ADDRESS}'awaitwebsocket.send(json.dumps({'uid':uid,'subscribe':channel}))print('Subscribed to status channel')# Build a transfer transaction with a non-existent mosaicwithurllib.request.urlopen(f'{NODE_URL}/node/time')asresp:time_json=json.loads(resp.read().decode())timestamp=NetworkTimestamp(int(time_json['communicationTimestamps']['receiveTimestamp']))withurllib.request.urlopen(f'{NODE_URL}/network/fees/transaction')asresp:fee_json=json.loads(resp.read().decode())fee_mult=max(fee_json['medianFeeMultiplier'],fee_json['minFeeMultiplier'])transaction=facade.transaction_factory.create({'type':'transfer_transaction_v1','signer_public_key':signer_key_pair.public_key,'deadline':timestamp.add_hours(2).timestamp,'recipient_address':MONITOR_ADDRESS,'mosaics':[{'mosaic_id':generate_mosaic_alias_id('symbol.unknown'),'amount':1}],})transaction.fee=Amount(fee_mult*transaction.size)signature=facade.sign_transaction(signer_key_pair,transaction)json_payload=facade.transaction_factory.attach_signature(transaction,signature)transaction_hash=str(facade.hash_transaction(transaction))announce_request=urllib.request.Request(f'{NODE_URL}/transactions',data=json_payload.encode(),headers={'Content-Type':'application/json'},method='PUT')withurllib.request.urlopen(announce_request)asresp:resp.read()print(f'Announced transaction {transaction_hash[:16]}...')# Wait for error via WebSocketasyncforraw_messageinwebsocket:msg=json.loads(raw_message)tx_hash=msg['data']['hash']code=msg['data']['code']print(f'Transaction {tx_hash[:16]}... 'f'rejected with code: {code}')iftx_hash==transaction_hash:break# Unsubscribe before closingawaitwebsocket.send(json.dumps({'uid':uid,'unsubscribe':channel}))print('Unsubscribed from status channel')try:asyncio.run(main())exceptExceptionaserror:print(error)
import{PrivateKey}from'symbol-sdk';import{SymbolFacade,NetworkTimestamp,models,generateMosaicAliasId}from'symbol-sdk/symbol';constNODE_URL=process.env.NODE_URL||'https://reference.symboltest.net:3001';constWS_URL=NODE_URL.replace('http','ws')+'/ws';console.log(`Using node ${NODE_URL}`);constMONITOR_ADDRESS=process.env.MONITOR_ADDRESS||'TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I';console.log(`Monitoring address: ${MONITOR_ADDRESS}`);constSIGNER_PRIVATE_KEY=process.env.SIGNER_PRIVATE_KEY||'0000000000000000000000000000000000000000000000000000000000000000';constfacade=newSymbolFacade('testnet');constsignerKeyPair=newSymbolFacade.KeyPair(newPrivateKey(SIGNER_PRIVATE_KEY));try{// Connect to WebSocketconstwebsocket=newWebSocket(WS_URL);constuid=awaitnewPromise((resolve)=>{websocket.addEventListener('message',(event)=>{constmessage=JSON.parse(event.data);resolve(message.uid);},{once:true});});console.log(`Connected to ${WS_URL} with uid ${uid}`);// Subscribe to status channelconstchannel=`status/${MONITOR_ADDRESS}`;websocket.send(JSON.stringify({uid,subscribe:channel}));console.log('Subscribed to status channel');// Build a transfer transaction with a non-existent mosaicconsttimeResponse=awaitfetch(`${NODE_URL}/node/time`);consttimeJSON=awaittimeResponse.json();consttimestamp=newNetworkTimestamp(timeJSON.communicationTimestamps.receiveTimestamp);constfeeResponse=awaitfetch(`${NODE_URL}/network/fees/transaction`);constfeeJSON=awaitfeeResponse.json();constfeeMult=Math.max(feeJSON.medianFeeMultiplier,feeJSON.minFeeMultiplier);consttransaction=facade.transactionFactory.create({type:'transfer_transaction_v1',signerPublicKey:signerKeyPair.publicKey.toString(),deadline:timestamp.addHours(2).timestamp,recipientAddress:MONITOR_ADDRESS,mosaics:[{mosaicId:generateMosaicAliasId('symbol.unknown'),amount:1n}],});transaction.fee=newmodels.Amount(feeMult*transaction.size);constsignature=facade.signTransaction(signerKeyPair,transaction);constjsonPayload=facade.transactionFactory.static.attachSignature(transaction,signature);consttransactionHash=facade.hashTransaction(transaction).toString();constrejected=newPromise((resolve)=>{websocket.addEventListener('message',(event)=>{constmsg=JSON.parse(event.data);consttxHash=msg.data.hash;constcode=msg.data.code;console.log(`Transaction ${txHash.substring(0,16)}... `+`rejected with code: ${code}`);if(txHash===transactionHash){resolve();}});});awaitfetch(`${NODE_URL}/transactions`,{method:'PUT',headers:{'Content-Type':'application/json'},body:jsonPayload});console.log('Announced transaction '+`${transactionHash.substring(0,16)}...`);// Wait for error via WebSocketawaitrejected;// Unsubscribe before closingwebsocket.send(JSON.stringify({uid,unsubscribe:channel}));console.log('Unsubscribed from status channel');websocket.close();}catch(error){console.error(error);}
The snippet uses the NODE_URL environment variable to set the Symbol API node.
If no value is provided, a default one is used.
The WebSocket URL is derived from NODE_URL by replacing the HTTP protocol with the WebSocket protocol and appending
/ws.
The status channel is scoped to a specific address.
The MONITOR_ADDRESS environment variable sets the address to watch.
The channel notifies whenever the address participates in a rejected transaction, whether as sender, recipient, or any
other role derived from the transaction's content (for example, signer of an embedded transaction in an
aggregate transaction).
To trigger a rejection, this tutorial sends a transfer transaction with a non-existent mosaic, signed with the
private key from SIGNER_PRIVATE_KEY.
If any of these environment variables is not provided, the tutorial provides default values that correspond to the same
account.
asyncwithconnect(WS_URL)aswebsocket:# Connect to WebSocketresponse=json.loads(awaitwebsocket.recv())uid=response['uid']print(f'Connected to {WS_URL} with uid {uid}')
// Connect to WebSocketconstwebsocket=newWebSocket(WS_URL);constuid=awaitnewPromise((resolve)=>{websocket.addEventListener('message',(event)=>{constmessage=JSON.parse(event.data);resolve(message.uid);},{once:true});});console.log(`Connected to ${WS_URL} with uid ${uid}`);
The code opens a WebSocket connection to the node's /ws endpoint.
Upon connecting, the server sends a message containing a unique identifier (uid) that must be included in all
subsequent subscription requests.
# Subscribe to status channelchannel=f'status/{MONITOR_ADDRESS}'awaitwebsocket.send(json.dumps({'uid':uid,'subscribe':channel}))print('Subscribed to status channel')
// Subscribe to status channelconstchannel=`status/${MONITOR_ADDRESS}`;websocket.send(JSON.stringify({uid,subscribe:channel}));console.log('Subscribed to status channel');
The code subscribes to the status/{address}WS channel scoped to the monitored address.
This channel notifies whenever a transaction involving the address is rejected by the network, providing the error code
and the transaction hash.
Building and Signing an Invalid Transfer Transaction⚓︎
# Build a transfer transaction with a non-existent mosaicwithurllib.request.urlopen(f'{NODE_URL}/node/time')asresp:time_json=json.loads(resp.read().decode())timestamp=NetworkTimestamp(int(time_json['communicationTimestamps']['receiveTimestamp']))withurllib.request.urlopen(f'{NODE_URL}/network/fees/transaction')asresp:fee_json=json.loads(resp.read().decode())fee_mult=max(fee_json['medianFeeMultiplier'],fee_json['minFeeMultiplier'])transaction=facade.transaction_factory.create({'type':'transfer_transaction_v1','signer_public_key':signer_key_pair.public_key,'deadline':timestamp.add_hours(2).timestamp,'recipient_address':MONITOR_ADDRESS,'mosaics':[{'mosaic_id':generate_mosaic_alias_id('symbol.unknown'),'amount':1}],})transaction.fee=Amount(fee_mult*transaction.size)signature=facade.sign_transaction(signer_key_pair,transaction)json_payload=facade.transaction_factory.attach_signature(transaction,signature)transaction_hash=str(facade.hash_transaction(transaction))
// Build a transfer transaction with a non-existent mosaicconsttimeResponse=awaitfetch(`${NODE_URL}/node/time`);consttimeJSON=awaittimeResponse.json();consttimestamp=newNetworkTimestamp(timeJSON.communicationTimestamps.receiveTimestamp);constfeeResponse=awaitfetch(`${NODE_URL}/network/fees/transaction`);constfeeJSON=awaitfeeResponse.json();constfeeMult=Math.max(feeJSON.medianFeeMultiplier,feeJSON.minFeeMultiplier);consttransaction=facade.transactionFactory.create({type:'transfer_transaction_v1',signerPublicKey:signerKeyPair.publicKey.toString(),deadline:timestamp.addHours(2).timestamp,recipientAddress:MONITOR_ADDRESS,mosaics:[{mosaicId:generateMosaicAliasId('symbol.unknown'),amount:1n}],});transaction.fee=newmodels.Amount(feeMult*transaction.size);constsignature=facade.signTransaction(signerKeyPair,transaction);constjsonPayload=facade.transactionFactory.static.attachSignature(transaction,signature);consttransactionHash=facade.hashTransaction(transaction).toString();
This tutorial builds a Transfer Transaction sent to the monitored address, including
a mosaic with the alias symbol.unknown.
Since this mosaic does not exist on the network, the transaction will be rejected.
The transaction is built as usual: fetching the network time and fee multiplier, creating the transaction descriptor,
and signing it.
The hash is computed locally so it can be matched against the incoming WebSocket error message.
announce_request=urllib.request.Request(f'{NODE_URL}/transactions',data=json_payload.encode(),headers={'Content-Type':'application/json'},method='PUT')withurllib.request.urlopen(announce_request)asresp:resp.read()print(f'Announced transaction {transaction_hash[:16]}...')# Wait for error via WebSocketasyncforraw_messageinwebsocket:msg=json.loads(raw_message)tx_hash=msg['data']['hash']code=msg['data']['code']print(f'Transaction {tx_hash[:16]}... 'f'rejected with code: {code}')iftx_hash==transaction_hash:break
constrejected=newPromise((resolve)=>{websocket.addEventListener('message',(event)=>{constmsg=JSON.parse(event.data);consttxHash=msg.data.hash;constcode=msg.data.code;console.log(`Transaction ${txHash.substring(0,16)}... `+`rejected with code: ${code}`);if(txHash===transactionHash){resolve();}});});awaitfetch(`${NODE_URL}/transactions`,{method:'PUT',headers:{'Content-Type':'application/json'},body:jsonPayload});console.log('Announced transaction '+`${transactionHash.substring(0,16)}...`);// Wait for error via WebSocketawaitrejected;
The code announces the transaction and then listens for incoming messages.
Each message follows the TransactionStatusDTO schema
and contains:
hash: The hash of the rejected transaction.
code: The error code explaining why the transaction was rejected.
See the TransactionStatusEnum schema for all possible
values.
When the received hash matches the announced transaction, the program prints the error code and exits.
// Unsubscribe before closingwebsocket.send(JSON.stringify({uid,unsubscribe:channel}));console.log('Unsubscribed from status channel');websocket.close();
After receiving the error, the code sends an unsubscribe message before closing the connection.
Using node https://reference.symboltest.net:3001
Monitoring address: TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
Connected to wss://reference.symboltest.net:3001/ws with uid jI0YhF0bJflDsIO915kmWUlZZew=
Subscribed to status channel
Announced transaction E14B012D3D254EF2...
Transaction E14B012D3D254EF2... rejected with code: Failure_Core_Insufficient_Balance
Unsubscribed from status channel
The output shows:
Address (line 2): The monitored address.
Connection (line 3): The WebSocket connection is established and the server returns a unique uid.
Subscription (line 4): The status channel is subscribed.
Announcement (line 5): The transaction is announced and its hash is printed.
Error (line 6): The network rejects the transaction with Failure_Core_Insufficient_Balance because the sender
does not hold the requested mosaic.
Unsubscribe (line 7): The code unsubscribes from the status channel.