Skip to content

Sending Messages with Transfer Transactions⚓︎

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.

Prerequisites⚓︎

Before you start, make sure to:

Additionally, check the Transfer transaction tutorial to understand how fee calculation, network time, and transaction confirmation work.

Full Code⚓︎

import json
import os
import time
import urllib.error
import urllib.request
from binascii import hexlify

from symbolchain.CryptoTypes import PrivateKey, PublicKey
from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.symbol.MessageEncoder import MessageEncoder
from symbolchain.symbol.Network import NetworkTimestamp
from symbolchain.sc import Amount

# Configuration
NODE_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 transaction
def retrieve_confirmed_transaction(hash_value, label):
    print(f"Polling for {label} confirmation...")
    confirmed = False
    attempts = 0
    max_attempts = 60

    while not confirmed and attempts < max_attempts:
        try:
            url = f"{NODE_URL}/transactions/confirmed/{hash_value}"
            with urllib.request.urlopen(url) as response:
                confirmed = True
                print(f"  {label} confirmed!")
                return json.loads(response.read().decode())
        except urllib.error.HTTPError:
            # Transaction not yet confirmed
            pass
        attempts += 1
        time.sleep(2)

    if not confirmed:
        raise Exception(
            f"{label} not confirmed after {max_attempts} attempts"
        )

# Set up sender and recipient accounts
facade = 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 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())
    timestamp = NetworkTimestamp(int(
        response_json['communicationTimestamps']['receiveTimestamp'])
    )
    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}\n")

# ===== PLAIN TEXT MESSAGE =====
print("==> Sending Plain Text Message")

# Create a plain text message
plain_message = "Hello, Symbol!".encode("utf-8")
print(f"Plain message: {plain_message.decode('utf-8')}")

# Build transfer transaction with plain message
plain_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 transaction
plain_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",
)
with urllib.request.urlopen(plain_announce_request) as response:
    print(f"Plain message transaction announced\n")

# ===== RECEIVING PLAIN TEXT MESSAGE =====
print("<== Receiving Plain Text Message")

# Wait for confirmation
plain_tx_data = retrieve_confirmed_transaction(
    plain_transaction_hash, "Plain message transaction"
)

# Decode plain message from confirmed transaction
received_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 pair
sender_message_encoder = MessageEncoder(sender_key_pair)

# Encrypt the message using recipient's public key
secret_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 message
encrypted_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 transaction
encrypted_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",
)
with urllib.request.urlopen(encrypted_announce_request) as response:
    print(f"Encrypted message transaction announced\n")

# ===== RECEIVING ENCRYPTED MESSAGE =====
print("<== Receiving Encrypted Message")

# Wait for confirmation
encrypted_tx_data = retrieve_confirmed_transaction(
    encrypted_transaction_hash, "Encrypted message transaction"
)

# Decode encrypted message using recipient's private key
recipient_message_encoder = MessageEncoder(recipient_key_pair)
received_encrypted_message = bytes.fromhex(
    encrypted_tx_data["transaction"]["message"]
)

# Get sender's public key from the transaction
sender_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
)

if is_decoded:
    message_text = decrypted_message.decode("utf-8")
    print(f"Recipient decrypted message: {message_text}")
else:
    print(f"Recipient failed to decrypt message")

Download source

import { PrivateKey, PublicKey } from 'symbol-sdk';
import {
    SymbolFacade,
    NetworkTimestamp,
    models,
    MessageEncoder
} from 'symbol-sdk/symbol';

// Configuration
const NODE_URL = process.env.NODE_URL ||
    'https://001-sai-dual.symboltest.net:3001';
console.log('Using node', NODE_URL);

// Helper function to poll for confirmed transaction
async function retrieveConfirmedTransaction(hash, label) {
    console.log(`Polling for ${label} confirmation...`);
    let confirmed = false;
    let attempts = 0;
    const maxAttempts = 60;

    while (!confirmed && attempts < maxAttempts) {
        try {
            const response = await fetch(
                `${NODE_URL}/transactions/confirmed/${hash}`);
            if (response.ok) {
                confirmed = true;
                console.log(`  ${label} confirmed!`);
                return await response.json();
            }
        } catch (error) {
            // Transaction not yet confirmed
        }
        attempts++;
        await new Promise(resolve => setTimeout(resolve, 2000));
    }

    if (!confirmed) {
        throw new Error(
            `${label} not confirmed after ${maxAttempts} attempts`);
    }
}

// Set up sender and recipient accounts
const facade = new SymbolFacade('testnet');

const senderPrivateKeyString = process.env.SENDER_PRIVATE_KEY ||
    '0000000000000000000000000000000000000000000000000000000000000000';
const senderKeyPair = new SymbolFacade.KeyPair(
    new PrivateKey(senderPrivateKeyString));
const senderAddress = facade.network.publicKeyToAddress(
    senderKeyPair.publicKey);

const recipientPrivateKeyString = process.env.RECIPIENT_PRIVATE_KEY ||
    '1111111111111111111111111111111111111111111111111111111111111111';
const recipientKeyPair = new SymbolFacade.KeyPair(
    new PrivateKey(recipientPrivateKeyString));
const recipientAddress = facade.network.publicKeyToAddress(
    recipientKeyPair.publicKey);

console.log('Sender address:', senderAddress.toString());
console.log('Recipient address:', recipientAddress.toString(), '\n');

// 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, '\n');

// ===== PLAIN TEXT MESSAGE =====
console.log('==> Sending Plain Text Message');

// Create a plain text message
const plainMessage = new TextEncoder().encode('Hello, Symbol!');
console.log('Plain message:',
    new TextDecoder().decode(plainMessage));

// Build transfer transaction with plain message
const plainTransaction = facade.transactionFactory.create({
    type: 'transfer_transaction_v1',
    signerPublicKey: senderKeyPair.publicKey.toString(),
    deadline: timestamp.addHours(2).timestamp,
    recipientAddress: recipientAddress.toString(),
    mosaics: [],
    message: plainMessage
});
plainTransaction.fee = new models.Amount(feeMult * plainTransaction.size);

// Sign and announce the transaction
const plainSignature = facade.signTransaction(
    senderKeyPair, plainTransaction);
const plainJsonPayload = facade.transactionFactory.static
    .attachSignature(plainTransaction, plainSignature);
const plainTransactionHash = facade.hashTransaction(
    plainTransaction).toString();
console.log('Transaction hash:', plainTransactionHash);

await fetch(`${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 confirmation
const plainTxData = await retrieveConfirmedTransaction(
    plainTransactionHash, 'Plain message transaction');

// Decode plain message from confirmed transaction
const receivedPlainMessage = Buffer.from(
    plainTxData.transaction.message, 'hex');
console.log('Received plain message:',
    new TextDecoder().decode(receivedPlainMessage), '\n');

// ===== ENCRYPTED MESSAGE =====
console.log('==> Sending Encrypted Message');

// Create a message encoder with sender's key pair
const senderMessageEncoder = new MessageEncoder(senderKeyPair);

// Encrypt the message using recipient's public key
const secretMessage = new TextEncoder().encode(
    'This is a secret message!');
const encryptedPayload = senderMessageEncoder.encode(
    recipientKeyPair.publicKey, secretMessage
);
console.log('Original message:',
    new TextDecoder().decode(secretMessage));
const hex = Buffer.from(encryptedPayload).toString('hex');
console.log('Encrypted payload:', hex);

// Build transfer transaction with encrypted message
const encryptedTransaction = facade.transactionFactory.create({
    type: 'transfer_transaction_v1',
    signerPublicKey: senderKeyPair.publicKey.toString(),
    deadline: timestamp.addHours(2).timestamp,
    recipientAddress: recipientAddress.toString(),
    mosaics: [],
    message: encryptedPayload
});
encryptedTransaction.fee = new models.Amount(
    feeMult * encryptedTransaction.size);

// Sign and announce the transaction
const encryptedSignature = facade.signTransaction(
    senderKeyPair, encryptedTransaction);
const encryptedJsonPayload = facade.transactionFactory.static
.attachSignature(encryptedTransaction, encryptedSignature);
const encryptedTransactionHash = facade.hashTransaction(
    encryptedTransaction).toString();
console.log('Transaction hash:', encryptedTransactionHash);

await fetch(`${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 confirmation
const encryptedTxData = await retrieveConfirmedTransaction(
    encryptedTransactionHash, 'Encrypted message transaction');

// Decode encrypted message using recipient's private key
const recipientMessageEncoder = new MessageEncoder(recipientKeyPair);
const receivedEncryptedMessage = Buffer.from(
    encryptedTxData.transaction.message, 'hex');

// Get sender's public key from the transaction
const senderPublicKeyFromTx = new PublicKey(
    encryptedTxData.transaction.signerPublicKey);

const result = recipientMessageEncoder.tryDecode(
    senderPublicKeyFromTx, receivedEncryptedMessage);

if (result.isDecoded) {
    console.log('Recipient decrypted message:',
        new TextDecoder().decode(result.message));
} else {
    console.log('Recipient failed to decrypt message');
}

Download source

Code Explanation⚓︎

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.

Setting Up Accounts⚓︎

# Set up sender and recipient accounts
facade = 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 accounts
const facade = new SymbolFacade('testnet');

const senderPrivateKeyString = process.env.SENDER_PRIVATE_KEY ||
    '0000000000000000000000000000000000000000000000000000000000000000';
const senderKeyPair = new SymbolFacade.KeyPair(
    new PrivateKey(senderPrivateKeyString));
const senderAddress = facade.network.publicKeyToAddress(
    senderKeyPair.publicKey);

const recipientPrivateKeyString = process.env.RECIPIENT_PRIVATE_KEY ||
    '1111111111111111111111111111111111111111111111111111111111111111';
const recipientKeyPair = new SymbolFacade.KeyPair(
    new PrivateKey(recipientPrivateKeyString));
const recipientAddress = 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.

Sending a Plain Text Message⚓︎

print("==> Sending Plain Text Message")

# Create a plain text message
plain_message = "Hello, Symbol!".encode("utf-8")
print(f"Plain message: {plain_message.decode('utf-8')}")

# Build transfer transaction with plain message
plain_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 message
const plainMessage = new TextEncoder().encode('Hello, Symbol!');
console.log('Plain message:',
    new TextDecoder().decode(plainMessage));

// Build transfer transaction with plain message
const plainTransaction = 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.

The transaction is then signed and announced following the same process as in Creating a Transfer Transaction.

Message constraints:

  • 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.

Receiving a Plain Text Message⚓︎

print("<== Receiving Plain Text Message")

# Wait for confirmation
plain_tx_data = retrieve_confirmed_transaction(
    plain_transaction_hash, "Plain message transaction"
)

# Decode plain message from confirmed transaction
received_plain_message = bytes.fromhex(
    plain_tx_data["transaction"]["message"]
)
print(
    f"Received plain message: {received_plain_message.decode('utf-8')}\n"
)
console.log('<== Receiving Plain Text Message');

// Wait for confirmation
const plainTxData = await retrieveConfirmedTransaction(
    plainTransactionHash, 'Plain message transaction');

// Decode plain message from confirmed transaction
const receivedPlainMessage = Buffer.from(
    plainTxData.transaction.message, 'hex');
console.log('Received plain message:',
    new TextDecoder().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.

Sending an Encrypted Message⚓︎

print("==> Sending Encrypted Message")

# Create a message encoder with sender's key pair
sender_message_encoder = MessageEncoder(sender_key_pair)

# Encrypt the message using recipient's public key
secret_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 message
encrypted_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 pair
const senderMessageEncoder = new MessageEncoder(senderKeyPair);

// Encrypt the message using recipient's public key
const secretMessage = new TextEncoder().encode(
    'This is a secret message!');
const encryptedPayload = senderMessageEncoder.encode(
    recipientKeyPair.publicKey, secretMessage
);
console.log('Original message:',
    new TextDecoder().decode(secretMessage));
const hex = Buffer.from(encryptedPayload).toString('hex');
console.log('Encrypted payload:', hex);

// Build transfer transaction with encrypted message
const encryptedTransaction = 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:

  1. A is created with the sender's key pair.
  2. The message is encoded using the recipient's public key and the message bytes with .
  3. The encrypted payload is attached to the transaction's message field.

The transaction is then signed and announced following the same process as in Creating a Transfer Transaction.

Message encryption is a convention

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.

For more details, see Optional Messages in the Textbook.

Receiving an Encrypted Message⚓︎

print("<== Receiving Encrypted Message")

# Wait for confirmation
encrypted_tx_data = retrieve_confirmed_transaction(
    encrypted_transaction_hash, "Encrypted message transaction"
)

# Decode encrypted message using recipient's private key
recipient_message_encoder = MessageEncoder(recipient_key_pair)
received_encrypted_message = bytes.fromhex(
    encrypted_tx_data["transaction"]["message"]
)

# Get sender's public key from the transaction
sender_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
)

if is_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 confirmation
const encryptedTxData = await retrieveConfirmedTransaction(
    encryptedTransactionHash, 'Encrypted message transaction');

// Decode encrypted message using recipient's private key
const recipientMessageEncoder = new MessageEncoder(recipientKeyPair);
const receivedEncryptedMessage = Buffer.from(
    encryptedTxData.transaction.message, 'hex');

// Get sender's public key from the transaction
const senderPublicKeyFromTx = new PublicKey(
    encryptedTxData.transaction.signerPublicKey);

const result = recipientMessageEncoder.tryDecode(
    senderPublicKeyFromTx, receivedEncryptedMessage);

if (result.isDecoded) {
    console.log('Recipient decrypted message:',
        new TextDecoder().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.

Output⚓︎

The output shown below corresponds to a typical run of the program.

Using node https://001-sai-dual.symboltest.net:3001
Sender address: TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
Recipient address: TCWYXKVYBMO4NBCUF3AXKJMXCGVSYQOS7ZG2TLI

Fetching current network time from /node/time
  Network time: 93047688268 ms since nemesis
Fetching recommended fees from /network/fees/transaction
  Fee multiplier: 100

==> Sending Plain Text Message
Plain message: Hello, Symbol!
Transaction hash: F30D4F2D20CDF46EC2A5D3D05E3A1059A18BD6D66C661A3DD66A7946612E857A
Plain message transaction announced

<== Receiving Plain Text Message
Polling for Plain message transaction confirmation...
  Plain message transaction confirmed!
Received plain message: Hello, Symbol!

==> Sending Encrypted Message
Original message: This is a secret message!
Encrypted payload: 01c1cc6085863447340555ba03d319e9b5e64c1619ff9fb6ed20a9e823f121056e0d96927df2a523df3f4e9663eb5981e2542528cb49
Transaction hash: 8E4582520E9BD1C4AC1E758BE49BFDF8719F9563676289D81A00E5532C8FA86D
Encrypted message transaction announced

<== Receiving Encrypted Message
Polling for Encrypted message transaction confirmation...
  Encrypted message transaction confirmed!
Recipient decrypted message: This is a secret message!

You can view the transactions on the Symbol Testnet Explorer by searching for the transaction hashes printed in the output.

The explorer cannot decrypt encrypted messages because it doesn't have access to the private keys.

Conclusion⚓︎

This tutorial showed how to:

Step Related documentation
Convert text into UTF-8 bytes TextEncoder (JS) and str.encode/bytes.decode (Python)
System methods, not part of the Symbol SDK
Encrypt and decrypt a message
Include a message in a Transfer Transaction