Set up the memo & send the tx
import tweetnacl from 'tweetnacl';
import { Buffer } from "buffer";
import { Client, Wallet, deriveAddress, Payment } from "xrpl";
import {
edwardsToMontgomeryPub,
edwardsToMontgomeryPriv,
} from "@noble/curves/ed25519";
const { box, randomBytes } = tweetnacl;
/**
* Encrypts a message using X25519 (Montgomery curve) for Diffie-Hellman key exchange
*
* @param message - Plain text message to encrypt
* @param recipientPublicKey - Recipient's Ed25519 public key (hex encoded)
* @param senderSecretKey - Sender's Ed25519 private key (hex encoded)
* @returns JSON string containing base64 encoded encrypted message and nonce
*
* Steps:
* 1. Convert hex keys to byte arrays
* 2. Generate random nonce for uniqueness
* 3. Convert message to bytes
* 4. Convert Ed25519 keys to X25519 (Montgomery) format for encryption
* 5. Encrypt using NaCl box with converted keys
* 6. Return encrypted message and nonce as base64 JSON
*/
export function encryptMessage(
message: string,
recipientPublicKey: string,
senderSecretKey: string,
): string {
const pubKeyBytes = Buffer.from(recipientPublicKey.slice(2), "hex");
const secretKeyBytes = Buffer.from(senderSecretKey.slice(2), "hex");
const nonce = randomBytes(box.nonceLength);
const messageUint8 = Buffer.from(message);
const pubKeyCurve = edwardsToMontgomeryPub(pubKeyBytes);
const privKeyCurve = edwardsToMontgomeryPriv(secretKeyBytes);
const encryptedMessage = box(messageUint8, nonce, pubKeyCurve, privKeyCurve);
return JSON.stringify({
encrypted: Buffer.from(encryptedMessage).toString("base64"),
nonce: Buffer.from(nonce).toString("base64"),
});
}
/**
* Sends an XRPL transaction containing an encrypted message in its memo field
*
* @param cypherMessage - The encrypted message to send
* @param myWallet - Sender's XRPL wallet for signing the transaction
* @param receiverPubKey - Recipient's public key to derive their XRPL address
* @returns Transaction result or undefined if error occurs
*
* Flow:
* 1. Connect to XRPL testnet
* 2. Create Payment transaction:
* - Minimal amount (1 drop)
* - Include encrypted message in memo field
* - Derive recipient's address from their public key
* 3. Prepare, sign and submit transaction
* 4. Wait for validation and return result
* 5. Always disconnect client when done
*
* Note: MemoType is hex encoded "http://example.com/memo/generic"
*/
async function sendTX(
cypherMessage: string,
myWallet: Wallet,
recipientPublicKey: string,
) {
const client = new Client("wss://clio.altnet.rippletest.net:51233/");
try {
await client.connect();
const receiverAddress = deriveAddress(recipientPublicKey);
const tx: Payment = {
TransactionType: "Payment",
Account: myWallet.classicAddress,
Destination: receiverAddress,
Amount: "10000000",
Memos: [
{
Memo: {
MemoData: Buffer.from(cypherMessage).toString("hex"),
},
},
],
};
const prepared = await client.autofill(tx);
const signed = myWallet.sign(prepared);
const result = await client.submitAndWait(signed.tx_blob);
return result;
} catch (error) {
console.log(error);
} finally {
await client.disconnect();
}
}
async function main(): Promise<void> {
const recipientPublicKey = "Recipient public key goes here";
const myWallet = Wallet.fromSeed("Your seed goes here");
const cypherMessage = encryptMessage(
"Hello World",
recipientPublicKey,
myWallet.privateKey,
);
console.log("==============CYPHERED MESSAGE==============");
console.log(cypherMessage);
const tx = await sendTX(cypherMessage, myWallet, recipientPublicKey);
console.log("==============TRANSACTION==============");
console.log(tx);
console.log("all done");
}
main();
Last updated

