transfer transactions can include an optional message field, which allows attaching up to 1,024 bytes of data to the
transaction.
Messages can be sent as plain text or encrypted using the recipient's public key, ensuring only the intended recipient
can read them.
This tutorial shows how to send both plain and encrypted messages and how to decode received messages.
importjsonimportosimporttimeimporturllib.errorimporturllib.requestfrombinasciiimporthexlifyfromsymbolchain.CryptoTypesimportPrivateKey,PublicKeyfromsymbolchain.facade.SymbolFacadeimportSymbolFacadefromsymbolchain.symbol.MessageEncoderimportMessageEncoderfromsymbolchain.symbol.NetworkimportNetworkTimestampfromsymbolchain.scimportAmount# ConfigurationNODE_URL=os.environ.get("NODE_URL","https://001-sai-dual.symboltest.net:3001")print(f"Using node {NODE_URL}")# Helper function to poll for confirmed transactiondefretrieve_confirmed_transaction(hash_value,label):print(f"Polling for {label} confirmation...")confirmed=Falseattempts=0max_attempts=60whilenotconfirmedandattempts<max_attempts:try:url=f"{NODE_URL}/transactions/confirmed/{hash_value}"withurllib.request.urlopen(url)asresponse:confirmed=Trueprint(f" {label} confirmed!")returnjson.loads(response.read().decode())excepturllib.error.HTTPError:# Transaction not yet confirmedpassattempts+=1time.sleep(2)ifnotconfirmed:raiseException(f"{label} not confirmed after {max_attempts} attempts")# Set up sender and recipient accountsfacade=SymbolFacade("testnet")sender_private_key_string=os.environ.get("SENDER_PRIVATE_KEY","0000000000000000000000000000000000000000000000000000000000000000",)sender_key_pair=facade.KeyPair(PrivateKey(sender_private_key_string))sender_address=facade.network.public_key_to_address(sender_key_pair.public_key)recipient_private_key_string=os.environ.get("RECIPIENT_PRIVATE_KEY","1111111111111111111111111111111111111111111111111111111111111111",)recipient_key_pair=facade.KeyPair(PrivateKey(recipient_private_key_string))recipient_address=facade.network.public_key_to_address(recipient_key_pair.public_key)print(f"Sender address: {sender_address}")print(f"Recipient address: {recipient_address}\n")# 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())timestamp=NetworkTimestamp(int(response_json['communicationTimestamps']['receiveTimestamp']))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}\n")# ===== PLAIN TEXT MESSAGE =====print("==> Sending Plain Text Message")# Create a plain text messageplain_message="Hello, Symbol!".encode("utf-8")print(f"Plain message: {plain_message.decode('utf-8')}")# Build transfer transaction with plain messageplain_transaction=facade.transaction_factory.create({"type":"transfer_transaction_v1","signer_public_key":sender_key_pair.public_key,"deadline":timestamp.add_hours(2).timestamp,"recipient_address":recipient_address,"mosaics":[],"message":plain_message,})plain_transaction.fee=Amount(fee_mult*plain_transaction.size)# Sign and announce the transactionplain_signature=facade.sign_transaction(sender_key_pair,plain_transaction)plain_json_payload=facade.transaction_factory.attach_signature(plain_transaction,plain_signature)plain_transaction_hash=facade.hash_transaction(plain_transaction)print(f"Transaction hash: {plain_transaction_hash}")plain_announce_request=urllib.request.Request(f"{NODE_URL}/transactions",data=plain_json_payload.encode("utf-8"),headers={"Content-Type":"application/json"},method="PUT",)withurllib.request.urlopen(plain_announce_request)asresponse:print(f"Plain message transaction announced\n")# ===== RECEIVING PLAIN TEXT MESSAGE =====print("<== Receiving Plain Text Message")# Wait for confirmationplain_tx_data=retrieve_confirmed_transaction(plain_transaction_hash,"Plain message transaction")# Decode plain message from confirmed transactionreceived_plain_message=bytes.fromhex(plain_tx_data["transaction"]["message"])print(f"Received plain message: {received_plain_message.decode('utf-8')}\n")# ===== ENCRYPTED MESSAGE =====print("==> Sending Encrypted Message")# Create a message encoder with sender's key pairsender_message_encoder=MessageEncoder(sender_key_pair)# Encrypt the message using recipient's public keysecret_message="This is a secret message!".encode("utf-8")encrypted_payload=sender_message_encoder.encode(recipient_key_pair.public_key,secret_message)print(f"Original message: {secret_message.decode('utf-8')}")print("Encrypted payload: "+hexlify(encrypted_payload).decode("utf-8"))# Build transfer transaction with encrypted messageencrypted_transaction=facade.transaction_factory.create({"type":"transfer_transaction_v1","signer_public_key":sender_key_pair.public_key,"deadline":timestamp.add_hours(2).timestamp,"recipient_address":recipient_address,"mosaics":[],"message":encrypted_payload,})encrypted_transaction.fee=Amount(fee_mult*encrypted_transaction.size)# Sign and announce the transactionencrypted_signature=facade.sign_transaction(sender_key_pair,encrypted_transaction)encrypted_json_payload=facade.transaction_factory.attach_signature(encrypted_transaction,encrypted_signature)encrypted_transaction_hash=facade.hash_transaction(encrypted_transaction)print(f"Transaction hash: {encrypted_transaction_hash}")encrypted_announce_request=urllib.request.Request(f"{NODE_URL}/transactions",data=encrypted_json_payload.encode("utf-8"),headers={"Content-Type":"application/json"},method="PUT",)withurllib.request.urlopen(encrypted_announce_request)asresponse:print(f"Encrypted message transaction announced\n")# ===== RECEIVING ENCRYPTED MESSAGE =====print("<== Receiving Encrypted Message")# Wait for confirmationencrypted_tx_data=retrieve_confirmed_transaction(encrypted_transaction_hash,"Encrypted message transaction")# Decode encrypted message using recipient's private keyrecipient_message_encoder=MessageEncoder(recipient_key_pair)received_encrypted_message=bytes.fromhex(encrypted_tx_data["transaction"]["message"])# Get sender's public key from the transactionsender_public_key_from_tx=PublicKey(encrypted_tx_data["transaction"]["signerPublicKey"])(is_decoded,decrypted_message)=recipient_message_encoder.try_decode(sender_public_key_from_tx,received_encrypted_message)ifis_decoded:message_text=decrypted_message.decode("utf-8")print(f"Recipient decrypted message: {message_text}")else:print(f"Recipient failed to decrypt message")
import{PrivateKey,PublicKey}from'symbol-sdk';import{SymbolFacade,NetworkTimestamp,models,MessageEncoder}from'symbol-sdk/symbol';// ConfigurationconstNODE_URL=process.env.NODE_URL||'https://001-sai-dual.symboltest.net:3001';console.log('Using node',NODE_URL);// Helper function to poll for confirmed transactionasyncfunctionretrieveConfirmedTransaction(hash,label){console.log(`Polling for ${label} confirmation...`);letconfirmed=false;letattempts=0;constmaxAttempts=60;while(!confirmed&&attempts<maxAttempts){try{constresponse=awaitfetch(`${NODE_URL}/transactions/confirmed/${hash}`);if(response.ok){confirmed=true;console.log(` ${label} confirmed!`);returnawaitresponse.json();}}catch(error){// Transaction not yet confirmed}attempts++;awaitnewPromise(resolve=>setTimeout(resolve,2000));}if(!confirmed){thrownewError(`${label} not confirmed after ${maxAttempts} attempts`);}}// Set up sender and recipient accountsconstfacade=newSymbolFacade('testnet');constsenderPrivateKeyString=process.env.SENDER_PRIVATE_KEY||'0000000000000000000000000000000000000000000000000000000000000000';constsenderKeyPair=newSymbolFacade.KeyPair(newPrivateKey(senderPrivateKeyString));constsenderAddress=facade.network.publicKeyToAddress(senderKeyPair.publicKey);constrecipientPrivateKeyString=process.env.RECIPIENT_PRIVATE_KEY||'1111111111111111111111111111111111111111111111111111111111111111';constrecipientKeyPair=newSymbolFacade.KeyPair(newPrivateKey(recipientPrivateKeyString));constrecipientAddress=facade.network.publicKeyToAddress(recipientKeyPair.publicKey);console.log('Sender address:',senderAddress.toString());console.log('Recipient address:',recipientAddress.toString(),'\n');// 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,'\n');// ===== PLAIN TEXT MESSAGE =====console.log('==> Sending Plain Text Message');// Create a plain text messageconstplainMessage=newTextEncoder().encode('Hello, Symbol!');console.log('Plain message:',newTextDecoder().decode(plainMessage));// Build transfer transaction with plain messageconstplainTransaction=facade.transactionFactory.create({type:'transfer_transaction_v1',signerPublicKey:senderKeyPair.publicKey.toString(),deadline:timestamp.addHours(2).timestamp,recipientAddress:recipientAddress.toString(),mosaics:[],message:plainMessage});plainTransaction.fee=newmodels.Amount(feeMult*plainTransaction.size);// Sign and announce the transactionconstplainSignature=facade.signTransaction(senderKeyPair,plainTransaction);constplainJsonPayload=facade.transactionFactory.static.attachSignature(plainTransaction,plainSignature);constplainTransactionHash=facade.hashTransaction(plainTransaction).toString();console.log('Transaction hash:',plainTransactionHash);awaitfetch(`${NODE_URL}/transactions`,{method:'PUT',headers:{'Content-Type':'application/json'},body:plainJsonPayload});console.log('Plain message transaction announced\n');// ===== RECEIVING PLAIN TEXT MESSAGE =====console.log('<== Receiving Plain Text Message');// Wait for confirmationconstplainTxData=awaitretrieveConfirmedTransaction(plainTransactionHash,'Plain message transaction');// Decode plain message from confirmed transactionconstreceivedPlainMessage=Buffer.from(plainTxData.transaction.message,'hex');console.log('Received plain message:',newTextDecoder().decode(receivedPlainMessage),'\n');// ===== ENCRYPTED MESSAGE =====console.log('==> Sending Encrypted Message');// Create a message encoder with sender's key pairconstsenderMessageEncoder=newMessageEncoder(senderKeyPair);// Encrypt the message using recipient's public keyconstsecretMessage=newTextEncoder().encode('This is a secret message!');constencryptedPayload=senderMessageEncoder.encode(recipientKeyPair.publicKey,secretMessage);console.log('Original message:',newTextDecoder().decode(secretMessage));consthex=Buffer.from(encryptedPayload).toString('hex');console.log('Encrypted payload:',hex);// Build transfer transaction with encrypted messageconstencryptedTransaction=facade.transactionFactory.create({type:'transfer_transaction_v1',signerPublicKey:senderKeyPair.publicKey.toString(),deadline:timestamp.addHours(2).timestamp,recipientAddress:recipientAddress.toString(),mosaics:[],message:encryptedPayload});encryptedTransaction.fee=newmodels.Amount(feeMult*encryptedTransaction.size);// Sign and announce the transactionconstencryptedSignature=facade.signTransaction(senderKeyPair,encryptedTransaction);constencryptedJsonPayload=facade.transactionFactory.static.attachSignature(encryptedTransaction,encryptedSignature);constencryptedTransactionHash=facade.hashTransaction(encryptedTransaction).toString();console.log('Transaction hash:',encryptedTransactionHash);awaitfetch(`${NODE_URL}/transactions`,{method:'PUT',headers:{'Content-Type':'application/json'},body:encryptedJsonPayload});console.log('Encrypted message transaction announced\n');// ===== RECEIVING ENCRYPTED MESSAGE =====console.log('<== Receiving Encrypted Message');// Wait for confirmationconstencryptedTxData=awaitretrieveConfirmedTransaction(encryptedTransactionHash,'Encrypted message transaction');// Decode encrypted message using recipient's private keyconstrecipientMessageEncoder=newMessageEncoder(recipientKeyPair);constreceivedEncryptedMessage=Buffer.from(encryptedTxData.transaction.message,'hex');// Get sender's public key from the transactionconstsenderPublicKeyFromTx=newPublicKey(encryptedTxData.transaction.signerPublicKey);constresult=recipientMessageEncoder.tryDecode(senderPublicKeyFromTx,receivedEncryptedMessage);if(result.isDecoded){console.log('Recipient decrypted message:',newTextDecoder().decode(result.message));}else{console.log('Recipient failed to decrypt message');}
This tutorial focuses on the message-specific aspects of transfer transactions.
The parts about fetching network time, calculating fees, and announcing transactions have been explained in the
Transfer Transaction tutorial and are skipped here for brevity.
# Set up sender and recipient accountsfacade=SymbolFacade("testnet")sender_private_key_string=os.environ.get("SENDER_PRIVATE_KEY","0000000000000000000000000000000000000000000000000000000000000000",)sender_key_pair=facade.KeyPair(PrivateKey(sender_private_key_string))sender_address=facade.network.public_key_to_address(sender_key_pair.public_key)recipient_private_key_string=os.environ.get("RECIPIENT_PRIVATE_KEY","1111111111111111111111111111111111111111111111111111111111111111",)recipient_key_pair=facade.KeyPair(PrivateKey(recipient_private_key_string))recipient_address=facade.network.public_key_to_address(recipient_key_pair.public_key)print(f"Sender address: {sender_address}")print(f"Recipient address: {recipient_address}\n")
// Set up sender and recipient accountsconstfacade=newSymbolFacade('testnet');constsenderPrivateKeyString=process.env.SENDER_PRIVATE_KEY||'0000000000000000000000000000000000000000000000000000000000000000';constsenderKeyPair=newSymbolFacade.KeyPair(newPrivateKey(senderPrivateKeyString));constsenderAddress=facade.network.publicKeyToAddress(senderKeyPair.publicKey);constrecipientPrivateKeyString=process.env.RECIPIENT_PRIVATE_KEY||'1111111111111111111111111111111111111111111111111111111111111111';constrecipientKeyPair=newSymbolFacade.KeyPair(newPrivateKey(recipientPrivateKeyString));constrecipientAddress=facade.network.publicKeyToAddress(recipientKeyPair.publicKey);console.log('Sender address:',senderAddress.toString());console.log('Recipient address:',recipientAddress.toString(),'\n');
To send a message, you need the sender's private key and the recipient's address.
To encrypt a message, you additionally need the recipient's public key.
This tutorial uses two accounts (sender and recipient) to demonstrate both sending and receiving plain and encrypted
messages.
The snippet reads their private keys from the SENDER_PRIVATE_KEY and RECIPIENT_PRIVATE_KEY environment variables,
which default to test keys if not set.
The recipient's public key and address are derived from their private key.
Retrieving public keys
When only the address is known, you can retrieve the public key from the network using the
/accounts/{accountId}GET endpoint.
An account's public key becomes available only after it has broadcast at least one transaction.
print("==> Sending Plain Text Message")# Create a plain text messageplain_message="Hello, Symbol!".encode("utf-8")print(f"Plain message: {plain_message.decode('utf-8')}")# Build transfer transaction with plain messageplain_transaction=facade.transaction_factory.create({"type":"transfer_transaction_v1","signer_public_key":sender_key_pair.public_key,"deadline":timestamp.add_hours(2).timestamp,"recipient_address":recipient_address,"mosaics":[],"message":plain_message,})
console.log('==> Sending Plain Text Message');// Create a plain text messageconstplainMessage=newTextEncoder().encode('Hello, Symbol!');console.log('Plain message:',newTextDecoder().decode(plainMessage));// Build transfer transaction with plain messageconstplainTransaction=facade.transactionFactory.create({type:'transfer_transaction_v1',signerPublicKey:senderKeyPair.publicKey.toString(),deadline:timestamp.addHours(2).timestamp,recipientAddress:recipientAddress.toString(),mosaics:[],message:plainMessage});
You can combine mosaic transfers with messages by including both the mosaics and message fields in the transaction
descriptor.
Maximum size: 1,024 bytes (the network rejects larger messages).
Encoding: UTF-8 by convention, though the protocol doesn't enforce a standard.
Privacy: All messages are publicly visible on the blockchain unless encrypted.
Handling larger data
For applications requiring more than 1,024 bytes of data, common approaches include:
On-chain storage: Split the data across multiple transactions within an aggregate transaction, allowing
you to keep everything on the blockchain.
Off-chain storage: Store the data off-chain and include a hash and a reference in the message field.
The hash verifies data integrity while the reference enables retrieval.
console.log('<== Receiving Plain Text Message');// Wait for confirmationconstplainTxData=awaitretrieveConfirmedTransaction(plainTransactionHash,'Plain message transaction');// Decode plain message from confirmed transactionconstreceivedPlainMessage=Buffer.from(plainTxData.transaction.message,'hex');console.log('Received plain message:',newTextDecoder().decode(receivedPlainMessage),'\n');
After announcing the transaction, the retrieve_confirmed_transaction helper function polls the
/transactions/confirmed/{transactionId}GET endpoint until the transaction is confirmed.
The confirmed transaction contains the message as a hex string.
To retrieve the original message, it converts the hex string to bytes and decodes it as UTF-8.
print("==> Sending Encrypted Message")# Create a message encoder with sender's key pairsender_message_encoder=MessageEncoder(sender_key_pair)# Encrypt the message using recipient's public keysecret_message="This is a secret message!".encode("utf-8")encrypted_payload=sender_message_encoder.encode(recipient_key_pair.public_key,secret_message)print(f"Original message: {secret_message.decode('utf-8')}")print("Encrypted payload: "+hexlify(encrypted_payload).decode("utf-8"))# Build transfer transaction with encrypted messageencrypted_transaction=facade.transaction_factory.create({"type":"transfer_transaction_v1","signer_public_key":sender_key_pair.public_key,"deadline":timestamp.add_hours(2).timestamp,"recipient_address":recipient_address,"mosaics":[],"message":encrypted_payload,})
console.log('==> Sending Encrypted Message');// Create a message encoder with sender's key pairconstsenderMessageEncoder=newMessageEncoder(senderKeyPair);// Encrypt the message using recipient's public keyconstsecretMessage=newTextEncoder().encode('This is a secret message!');constencryptedPayload=senderMessageEncoder.encode(recipientKeyPair.publicKey,secretMessage);console.log('Original message:',newTextDecoder().decode(secretMessage));consthex=Buffer.from(encryptedPayload).toString('hex');console.log('Encrypted payload:',hex);// Build transfer transaction with encrypted messageconstencryptedTransaction=facade.transactionFactory.create({type:'transfer_transaction_v1',signerPublicKey:senderKeyPair.publicKey.toString(),deadline:timestamp.addHours(2).timestamp,recipientAddress:recipientAddress.toString(),mosaics:[],message:encryptedPayload});
Encrypted messages provide confidentiality by protecting the message content using a shared secret derived from the
sender's private key and the recipient's public key.
Both the sender and recipient can decrypt the message using their own private key and the other party's public key.
The class handles message encryption:
A is created with the sender's key pair.
The message is encoded using the recipient's public key and the message bytes with .
The encrypted payload is attached to the transaction's message field.
The Symbol protocol does not define a standard for message encryption.
Sender and recipient must agree in advance on whether messages are encrypted and the cipher used.
The class implements a widely adopted convention used by most wallets and applications.
print("<== Receiving Encrypted Message")# Wait for confirmationencrypted_tx_data=retrieve_confirmed_transaction(encrypted_transaction_hash,"Encrypted message transaction")# Decode encrypted message using recipient's private keyrecipient_message_encoder=MessageEncoder(recipient_key_pair)received_encrypted_message=bytes.fromhex(encrypted_tx_data["transaction"]["message"])# Get sender's public key from the transactionsender_public_key_from_tx=PublicKey(encrypted_tx_data["transaction"]["signerPublicKey"])(is_decoded,decrypted_message)=recipient_message_encoder.try_decode(sender_public_key_from_tx,received_encrypted_message)ifis_decoded:message_text=decrypted_message.decode("utf-8")print(f"Recipient decrypted message: {message_text}")else:print(f"Recipient failed to decrypt message")
console.log('<== Receiving Encrypted Message');// Wait for confirmationconstencryptedTxData=awaitretrieveConfirmedTransaction(encryptedTransactionHash,'Encrypted message transaction');// Decode encrypted message using recipient's private keyconstrecipientMessageEncoder=newMessageEncoder(recipientKeyPair);constreceivedEncryptedMessage=Buffer.from(encryptedTxData.transaction.message,'hex');// Get sender's public key from the transactionconstsenderPublicKeyFromTx=newPublicKey(encryptedTxData.transaction.signerPublicKey);constresult=recipientMessageEncoder.tryDecode(senderPublicKeyFromTx,receivedEncryptedMessage);if(result.isDecoded){console.log('Recipient decrypted message:',newTextDecoder().decode(result.message));}else{console.log('Recipient failed to decrypt message');}
After announcing the encrypted message transaction, the retrieve_confirmed_transaction helper function polls for
confirmation.
To decrypt the message from the confirmed transaction, a is created with the recipient's key pair,
then is called with the sender's public key (obtained from the transaction's
signerPublicKey field) and the encrypted payload.
The method returns a tuple (is_decoded, message) indicating whether decryption was successful, and, if so, contains
the original plaintext bytes, which still need to be decoded.
Decryption works both ways
Because the encryption uses a shared secret derived from both key pairs, the sender can also decrypt the message
using their own private key and the recipient's public key.
This allows both parties to verify the message content after it has been published on the blockchain.
If decryption fails, possible causes include:
The message was encrypted for a different recipient.
The message is corrupted or tampered with.
The message is plain text, not encrypted.
An incorrect public key was used for the other party.