Messaging Guide
This guide covers sending and receiving encrypted and plaintext messages between addresses using the XChain SDK’s messaging module.
Overview
The MESSAGE action supports three encryption methods:
| Method | Name | Use Case |
|---|---|---|
| 1 | ECIES (default) | Address communication — encrypt to recipient’s public key, no key exchange needed, works across devices |
| 2 | ECDH | Session communication — two-party key exchange for forward secrecy, device-bound |
| 3 | AES | Shared secret communication — pre-shared key encryption, key exchanged out-of-band |
Prerequisites
- Running xchain-explorer, xchain-encoder, and a coin node
- A funded address with UTXOs for sending transactions
Setup
const XChainSDK = require('xchain-sdk');
const sdk = new XChainSDK({
network: 'bitcoin-regtest',
explorerUrl: 'localhost',
explorerPort: 8080,
encoderUrl: 'localhost',
encoderPort: 3001
});
Sending an Encrypted Message (ECIES)
ECIES is the default and recommended encryption method. It encrypts directly to the recipient’s public key — no prior key exchange is needed, and messages can be decrypted on any device that holds the recipient’s private key.
let result = await sdk.sendMessage({
wif: senderWIF,
destination: '1RecipientAddress...',
coin: 'BTC',
message: 'Hello, this is a private message!',
encoder: { pubkey: senderPubkeyHex }
});
console.log('Sent encrypted message, txid:', result.txid);
Under the hood, the SDK:
- Looks up the recipient’s public key via the explorer API
- Generates an ephemeral keypair
- Derives a shared secret via ECDH with the recipient’s public key
- Encrypts the message with AES-256-GCM
- Creates a MESSAGE action, encodes it into a PSBT, signs, and broadcasts
Sending a Plaintext Message
Plaintext messages are visible to everyone on-chain. Pass method: null to skip encryption.
let result = await sdk.sendMessage({
wif: senderWIF,
destination: '1RecipientAddress...',
coin: 'BTC',
message: 'This is a public message visible to everyone!',
method: null,
encoder: { pubkey: senderPubkeyHex }
});
Reading and Decrypting Messages
Fetch messages for an address. Pass your WIF to auto-decrypt ECIES messages addressed to you.
let messages = await sdk.getMessagesForAddress('1MyAddress...', {
wif: myWIF, // Needed to decrypt ECIES messages
type: 'received', // 'sent', 'received', or 'all'
limit: 10
});
for (let msg of messages) {
console.log(`From: ${msg.from}`);
console.log(` Text: ${msg.text}`);
console.log(` Encrypted: ${msg.encrypted}`);
console.log(` Method: ${msg.method}`);
console.log(` Block: ${msg.block}`);
}
Each message object contains:
from— sender addressto— recipient addresstext— decrypted message text (ornullif decryption failed or no WIF provided)encrypted—trueif the message was encryptedmethod— encryption method used (1, 2, 3, ornullfor plaintext)txid— transaction hashblock— block heighttimestamp— block timestamp
Low-Level ECIES Encrypt/Decrypt
Use the messaging module directly for encryption without creating an on-chain transaction.
// Look up a public key
let recipientPubkey = await sdk.getPublicKey('1RecipientAddress...');
// Encrypt
let encrypted = sdk.messaging.eciesEncrypt('Secret message', recipientPubkey);
console.log('Ciphertext:', encrypted.ciphertext);
// Decrypt (recipient side)
let decrypted = sdk.messaging.eciesDecrypt(encrypted.ciphertext, recipientWIF);
console.log('Decrypted:', decrypted.plaintext);
ECDH Session-Based Messaging (Method 2)
ECDH requires a key exchange handshake before sending messages. Both parties exchange public keys via format 0/1 MESSAGE actions, then derive a shared secret for the session.
Note: ECDH sessions are device-bound. The shared secret only exists on the devices that performed the key exchange. Use ECIES (method 1) if you need multi-device support.
// Step 1: Sender generates their session key
let senderSession = sdk.messaging.generateSessionKey(senderWIF);
// Step 2: Send format 0 key exchange request (on-chain)
let keyExchange = await sdk.message({
destination: '1RecipientAddress...',
coin: 'BTC',
encryptionMethod: 2,
encryptionKey: senderSession.publicKey
}, { pubkey: senderPubkeyHex });
// Sign and broadcast keyExchange.psbt...
// Step 3: Recipient responds with format 1 containing their pubkey
let recipientSession = sdk.messaging.generateSessionKey(recipientWIF);
let keyResponse = await sdk.message({
destination: senderAddress,
coin: 'BTC',
encryptionMethod: 2,
encryptionKey: recipientSession.publicKey
}, { pubkey: recipientPubkeyHex });
// Sign and broadcast keyResponse.psbt...
// Step 4: Both sides derive the same shared secret
let senderSecret = sdk.messaging.deriveSharedSecret(senderWIF, recipientSession.publicKey);
let recipientSecret = sdk.messaging.deriveSharedSecret(recipientWIF, senderSession.publicKey);
// senderSecret.sharedSecret === recipientSecret.sharedSecret
// Step 5: Encrypt/decrypt with the shared secret
let encrypted = sdk.messaging.sessionEncrypt('Session message', senderSecret.sharedSecret);
let decrypted = sdk.messaging.sessionDecrypt(encrypted.ciphertext, recipientSecret.sharedSecret);
AES Pre-Shared Key Messaging (Method 3)
AES uses a pre-shared key that both parties know. The key must be exchanged out-of-band (not on-chain).
let sharedKey = 'my-secret-passphrase-shared-between-parties';
// Send
let result = await sdk.sendMessage({
wif: senderWIF,
destination: '1RecipientAddress...',
coin: 'BTC',
message: 'Encrypted with a shared passphrase',
method: 3,
sharedKey: sharedKey,
encoder: { pubkey: senderPubkeyHex }
});
// Decrypt manually (getMessagesForAddress can't auto-decrypt AES — the app must supply the key)
let encrypted = sdk.messaging.aesEncrypt('Test message', sharedKey);
let decrypted = sdk.messaging.aesDecrypt(encrypted.ciphertext, sharedKey);
Public Key Lookup
Look up the public key for any address that has sent at least one XChain transaction.
let pubkey = await sdk.getPublicKey('1SomeAddress...');
if (pubkey) {
console.log('Public key:', pubkey);
} else {
console.log('No public key found (address may not have sent any XChain transactions)');
}
Choosing an Encryption Method
| Consideration | ECIES (1) | ECDH (2) | AES (3) |
|---|---|---|---|
| Key exchange needed? | No | Yes (format 0/1) | Out-of-band |
| Multi-device support | Yes | No (device-bound) | Yes (if key is shared) |
| Forward secrecy | Per-message (ephemeral keys) | Per-session | No |
| Best for | General messaging | Long-running sessions | Password-protected messages |