Skip to content

Sending Messages with Transfer Transactions⚓︎

INTERMEDIATE

Transfer transactions can include an optional message field, which allows attaching up to 1024 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.NemFacade import NemFacade
from symbolchain.nc import Amount, Message, MessageType
from symbolchain.nem.FeeCalculator import calculate_transaction_fee
from symbolchain.nem.MessageEncoder import MessageEncoder
from symbolchain.nem.Network import NetworkTimestamp

# Configuration
NODE_URL = os.getenv('NODE_URL', 'http://libertalia.nemtest.net:7890')
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...')
    attempts = 0
    max_attempts = 120

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

    raise TimeoutError(
        f'{label} not confirmed after {max_attempts} attempts'
    )


# Set up sender and recipient accounts
facade = NemFacade('testnet')

sender_private_key_string = os.getenv(
    'SENDER_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000000',
)
sender_key_pair = NemFacade.KeyPair(
    PrivateKey(sender_private_key_string)
)
sender_address = facade.network.public_key_to_address(
    sender_key_pair.public_key
)

recipient_private_key_string = os.getenv(
    'RECIPIENT_PRIVATE_KEY',
    '1111111111111111111111111111111111111111111111111111111111111111',
)
recipient_key_pair = NemFacade.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 = '/time-sync/network-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())
    network_time = response_json['receiveTimeStamp'] // 1000
    timestamp = NetworkTimestamp(network_time)
    deadline = timestamp.add_hours(2)
    print(f'  Network time: {network_time} s since the nemesis block\n')

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

# Create a plain text message
plain_message = 'Hello, NEM!'.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_v2',
        'signer_public_key': sender_key_pair.public_key,
        'timestamp': timestamp.timestamp,
        'deadline': deadline.timestamp,
        'recipient_address': recipient_address,
        'amount': 0,
        'message': {
            'message_type': 'plain',
            'message': plain_message,
        },
    }
)
plain_transaction.fee = Amount(
    calculate_transaction_fee(plain_transaction))

# 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}/transaction/announce',
    data=plain_json_payload.encode('utf-8'),
    headers={'Content-Type': 'application/json'},
    method='POST',
)
with urllib.request.urlopen(plain_announce_request) as response:
    print('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']['payload']
)
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_message = sender_message_encoder.encode(
    recipient_key_pair.public_key, secret_message
)
print(f'Original message: {secret_message.decode("utf-8")}')
encrypted_payload = hexlify(encrypted_message.message).decode('utf-8')
print(f'Encrypted payload: {encrypted_payload}')

# Build transfer transaction with encrypted message
encrypted_transaction = facade.transaction_factory.create(
    {
        'type': 'transfer_transaction_v2',
        'signer_public_key': sender_key_pair.public_key,
        'timestamp': timestamp.timestamp,
        'deadline': deadline.timestamp,
        'recipient_address': recipient_address,
        'amount': 0,
        'message': {
            'message_type': 'encrypted',
            'message': encrypted_message.message,
        },
    }
)
encrypted_transaction.fee = Amount(
    calculate_transaction_fee(encrypted_transaction))

# 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}/transaction/announce',
    data=encrypted_json_payload.encode('utf-8'),
    headers={'Content-Type': 'application/json'},
    method='POST',
)
with urllib.request.urlopen(encrypted_announce_request) as response:
    print('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 = Message()
received_encrypted_message.message_type = MessageType.ENCRYPTED
received_encrypted_message.message = bytes.fromhex(
    encrypted_tx_data['transaction']['message']['payload']
)

# Get sender's public key from the transaction
sender_public_key_from_tx = PublicKey(
    encrypted_tx_data['transaction']['signer']
)

(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('Recipient failed to decrypt message')

Download source

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

// Configuration
const NODE_URL = process.env.NODE_URL ||
    'http://libertalia.nemtest.net:7890';
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 attempts = 0;
    const maxAttempts = 120;

    while (attempts < maxAttempts) {
        const response = await fetch(
            `${NODE_URL}/transaction/get?hash=${hash}`);
        if (response.ok) {
            console.log(`  ${label} confirmed!`);
            return response.json();
        }
        attempts++;
        await new Promise(resolve => { setTimeout(resolve, 2000); });
    }

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

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

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

const recipientPrivateKeyString = process.env.RECIPIENT_PRIVATE_KEY ||
    '1111111111111111111111111111111111111111111111111111111111111111';
const recipientKeyPair = new NemFacade.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 = '/time-sync/network-time';
console.log('Fetching current network time from', timePath);
const timeResponse = await fetch(`${NODE_URL}${timePath}`);
const timeJSON = await timeResponse.json();
const networkTime = Math.floor(timeJSON.receiveTimeStamp / 1000);
const timestamp = new NetworkTimestamp(networkTime);
const deadline = timestamp.addHours(2);
console.log('  Network time:', networkTime,
    's since the nemesis block', '\n');

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

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

// Build transfer transaction with plain message
const plainTransaction = facade.transactionFactory.create({
    type: 'transfer_transaction_v2',
    signerPublicKey: senderKeyPair.publicKey.toString(),
    timestamp: timestamp.timestamp,
    deadline: deadline.timestamp,
    recipientAddress: recipientAddress.toString(),
    amount: 0n,
    message: {
        messageType: 'plain',
        message: plainMessage
    }
});
plainTransaction.fee = new models.Amount(
    calculateTransactionFee(plainTransaction));

// 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}/transaction/announce`, {
    method: 'POST',
    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.payload, '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 encryptedMessage = senderMessageEncoder.encode(
    recipientKeyPair.publicKey, secretMessage
);
console.log('Original message:', new TextDecoder().decode(secretMessage));
console.log('Encrypted payload:',
    Buffer.from(encryptedMessage.message).toString('hex'));

// Build transfer transaction with encrypted message
const encryptedTransaction = facade.transactionFactory.create({
    type: 'transfer_transaction_v2',
    signerPublicKey: senderKeyPair.publicKey.toString(),
    timestamp: timestamp.timestamp,
    deadline: deadline.timestamp,
    recipientAddress: recipientAddress.toString(),
    amount: 0n,
    message: {
        messageType: 'encrypted',
        message: encryptedMessage.message
    }
});
encryptedTransaction.fee = new models.Amount(
    calculateTransactionFee(encryptedTransaction));

// 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}/transaction/announce`, {
    method: 'POST',
    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 = new models.Message();
receivedEncryptedMessage.messageType = models.MessageType.ENCRYPTED;
receivedEncryptedMessage.message = Buffer.from(
    encryptedTxData.transaction.message.payload, 'hex');

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

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 = NemFacade('testnet')

sender_private_key_string = os.getenv(
    'SENDER_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000000',
)
sender_key_pair = NemFacade.KeyPair(
    PrivateKey(sender_private_key_string)
)
sender_address = facade.network.public_key_to_address(
    sender_key_pair.public_key
)

recipient_private_key_string = os.getenv(
    'RECIPIENT_PRIVATE_KEY',
    '1111111111111111111111111111111111111111111111111111111111111111',
)
recipient_key_pair = NemFacade.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 NemFacade('testnet');

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

const recipientPrivateKeyString = process.env.RECIPIENT_PRIVATE_KEY ||
    '1111111111111111111111111111111111111111111111111111111111111111';
const recipientKeyPair = new NemFacade.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 /account/get 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, NEM!'.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_v2',
        'signer_public_key': sender_key_pair.public_key,
        'timestamp': timestamp.timestamp,
        'deadline': deadline.timestamp,
        'recipient_address': recipient_address,
        'amount': 0,
        'message': {
            'message_type': 'plain',
            'message': plain_message,
        },
    }
)
console.log('==> Sending Plain Text Message');

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

// Build transfer transaction with plain message
const plainTransaction = facade.transactionFactory.create({
    type: 'transfer_transaction_v2',
    signerPublicKey: senderKeyPair.publicKey.toString(),
    timestamp: timestamp.timestamp,
    deadline: deadline.timestamp,
    recipientAddress: recipientAddress.toString(),
    amount: 0n,
    message: {
        messageType: 'plain',
        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 Sending XEM with a Transfer Transaction.

Message constraints:

  • Maximum size: 1024 bytes (the network rejects larger messages).
  • Encoding: UTF-8 by convention, though the protocol does not enforce a standard.
  • Privacy: All messages are publicly visible on the blockchain unless encrypted.

Handling larger data

For applications requiring more than 1024 bytes of data, common approaches include:

  • On-chain storage: Split the data across multiple transfer transactions, 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']['payload']
)
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.payload, 'hex');
console.log('Received plain message:',
    new TextDecoder().decode(receivedPlainMessage), '\n');

After announcing the transaction, the helper function polls the /transaction/get 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_message = sender_message_encoder.encode(
    recipient_key_pair.public_key, secret_message
)
print(f'Original message: {secret_message.decode("utf-8")}')
encrypted_payload = hexlify(encrypted_message.message).decode('utf-8')
print(f'Encrypted payload: {encrypted_payload}')

# Build transfer transaction with encrypted message
encrypted_transaction = facade.transaction_factory.create(
    {
        'type': 'transfer_transaction_v2',
        'signer_public_key': sender_key_pair.public_key,
        'timestamp': timestamp.timestamp,
        'deadline': deadline.timestamp,
        'recipient_address': recipient_address,
        'amount': 0,
        'message': {
            'message_type': 'encrypted',
            'message': encrypted_message.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 encryptedMessage = senderMessageEncoder.encode(
    recipientKeyPair.publicKey, secretMessage
);
console.log('Original message:', new TextDecoder().decode(secretMessage));
console.log('Encrypted payload:',
    Buffer.from(encryptedMessage.message).toString('hex'));

// Build transfer transaction with encrypted message
const encryptedTransaction = facade.transactionFactory.create({
    type: 'transfer_transaction_v2',
    signerPublicKey: senderKeyPair.publicKey.toString(),
    timestamp: timestamp.timestamp,
    deadline: deadline.timestamp,
    recipientAddress: recipientAddress.toString(),
    amount: 0n,
    message: {
        messageType: 'encrypted',
        message: encryptedMessage.message
    }
});

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 Sending XEM with a Transfer Transaction.

Message encryption is a convention

The NEM 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.

implements AES-GCM, the convention used by most wallets and applications. The class also provides for compatibility with legacy AES-CBC messages, the convention used by the earliest NEM wallets and applications. automatically detects and decodes both schemes.

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 = Message()
received_encrypted_message.message_type = MessageType.ENCRYPTED
received_encrypted_message.message = bytes.fromhex(
    encrypted_tx_data['transaction']['message']['payload']
)

# Get sender's public key from the transaction
sender_public_key_from_tx = PublicKey(
    encrypted_tx_data['transaction']['signer']
)

(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('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 = new models.Message();
receivedEncryptedMessage.messageType = models.MessageType.ENCRYPTED;
receivedEncryptedMessage.message = Buffer.from(
    encryptedTxData.transaction.message.payload, 'hex');

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

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 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 signer field) and the encrypted payload.

The method returns a tuple 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.
  • The same convention was not used when encrypting and decrypting.

Output⚓︎

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

Using node http://libertalia.nemtest.net:7890
Sender address: TBONKWCOWBZYZB2I5JD3LSDBQVBYHB757VN3SKPP
Recipient address: TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4

Fetching current network time from /time-sync/network-time
  Network time: 355248540 s since the nemesis block

==> Sending Plain Text Message
Plain message: Hello, NEM!
Transaction hash: 2F8F20CAF8F6FA42ADE05ED85FED0B4D1DA88051972405C4AE2D181B01FB69C3
Plain message transaction announced

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

==> Sending Encrypted Message
Original message: This is a secret message!
Encrypted payload: 3fefcf6e4f1e5e2165f941a5c15ea66778f82eded50cfbde5fc27eba24f4580ff72fb9d0b45061747be0324a120dc357dd11351c09
Transaction hash: 76604471D5A345E6F5CE20C65D618BC6F9A600F1DF515A696B2152E0E2D0B427
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!

Some highlights from the output:

  • Plain message (line 9): The message attached to the first transaction. Because it is not encrypted, anyone inspecting the blockchain can read it.

  • Received plain message (line 16): The same message, recovered from the confirmed transaction by converting the hexadecimal payload back to UTF-8.

  • Original message (line 19): The secret message before encryption.

  • Encrypted payload (line 20): The result of encrypting the previous message with , shown as a hexadecimal string. This is what gets stored on the blockchain.

  • Recipient decrypted message (line 27): The original message, retrieved from the confirmed transaction and decrypted by using the recipient's private key and the sender's public key.

You can view the transactions on the NEM testnet explorer by searching for the transaction hashes printed in the output.

The explorer cannot decrypt encrypted messages because it does not 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 SDK
Encrypt a message
Decrypt a message