Sui Stack Messaging SDK: EP 2 First Step Encrypted Chat CLI
Setup, Create Encrypted Channel, and Chat via CLI on Sui Testnet

Search for a command to run...
Setup, Create Encrypted Channel, and Chat via CLI on Sui Testnet

No comments yet. Be the first to comment.
A developer's overview of the Sui Payment Stack: components, roles, and the problems it solves

Understanding How DeepBook Predict Works Under the Hood

Understanding How Prediction Markets Work — DeepBook Predict vs Polymarket vs Kalshi

Build an AI Chatbot with Groq on Sui Encrypted Messaging

กลับมาตามสัญญาครับ จาก EP ที่แล้วเราได้เรียนรู้ภาพรวมของ Sui Stack Messaging SDK ไปแล้วว่ามันคืออะไร ทำงานยังไง มี 3 layers อะไรบ้าง ใครยังไม่ได้อ่านเบรคไว้ตรงนี้ก่อนเลยไปอ่านกันนะ ไม่งั้นจะงงเออ
ถึงเวลาละ เราจะลงมือสร้าง CLI Chatroom ซึ่งจะให้ AI ค่อยโต้ตอบกับเราเเทนตามวิถีคนเป็น introvert (ทำเป็น CLI เพราะตัวฉันไม่ถนัดงาน frontend เลยจ้า) ที่ข้อความทุกอย่างถูก encrypted ด้วย Seal แล้วเก็บบน Sui blockchain โดยจะทำเป็น CLI app ง่ายๆ ที่มีคำสั่งแบบนี้
create-room — สร้างห้องแชท encrypted
list-rooms — ดูห้องที่เราอยู่
chat — เข้าห้องแชท อ่าน + ส่งข้อความ real-time
invite — เชิญคนเข้าห้อง
members — ดูสมาชิก
ตาม step ครับเริ่มจากสร้าง project กันก่อน
mkdir sui-chat-cli && cd sui-chat-cli
npm init -y
npm i @mysten/messaging @mysten/seal @mysten/sui \
chalk commander dotenv groq-sdk
npm i -D typescript tsx @types/node
อย่าลืมเพิ่ม "type": "module" ใน package.json ด้วยนะ แล้วก็เพิ่ม scripts ไว้ใช้สะดวกๆ
{
"type": "module",
"scripts": {
"dev": "tsx src/index.ts",
"bot": "tsx src/bot/bot.ts"
}
}
จากนั้นสร้าง .env โดย private key สามารถไป export จาก Slush wallet ได้นะ และเราแนะนำให้แยกกระเป๋าที่ใช้ทดสอบกับกระเป๋าส่วนตัว จะได้ไม่พลาด ซึ่งครั้งใช้บน Testnet เท่านั้น เพราะยังไม่มี Mainnet ให้ใช้งาน
SUI_PRIVATE_KEY=suiprivkeyxxxxx
BOT_PRIVATE_KEY=suiprivkeyxxxxx
GROQ_API_KEY=gsk_xxxxx
GROQ_MODEL=llama-3.3-70b-versatile
NETWORK=testnet
BOT_POLL_INTERVAL_MS=5000
import "dotenv/config";
import {
TESTNET_MESSAGING_PACKAGE_CONFIG,
MAINNET_MESSAGING_PACKAGE_CONFIG,
} from "@mysten/messaging";
const network = (process.env.NETWORK ?? "testnet") as "testnet" | "mainnet";
export const config = {
suiPrivateKey: process.env.SUI_PRIVATE_KEY!,
botPrivateKey: process.env.BOT_PRIVATE_KEY || process.env.SUI_PRIVATE_KEY!,
network,
groqApiKey: process.env.GROQ_API_KEY!,
groqModel: process.env.GROQ_MODEL || "llama-3.3-70b-versatile",
botPollIntervalMs: parseInt(process.env.BOT_POLL_INTERVAL_MS || "5000"),
messagingPackageId:
network === "mainnet"
? MAINNET_MESSAGING_PACKAGE_CONFIG.packageId
: TESTNET_MESSAGING_PACKAGE_CONFIG.packageId,
get fullnodeUrl(): string {
return this.network === "mainnet"
? "https://fullnode.mainnet.sui.io:443"
: "https://fullnode.testnet.sui.io:443";
},
suiscanTxUrl(digest: string): string {
return `https://suiscan.xyz/\({this.network}/tx/\){digest}`;
},
sealServers: [
{
objectId: "0x73d05d62c18d9374e3ea529e8e0ed6161da1a141a94d3f76ae3fe4e99356db75",
weight: 1,
},
{
objectId: "0xf5d14a81a982144ae441cd7d64b09027f116a468bd36e7eca494f750591623c8",
weight: 1,
},
],
walrus: {
aggregator: "https://aggregator.walrus-testnet.walrus.space",
publisher: "https://publisher.walrus-testnet.walrus.space",
epochs: 1,
},
};
ตรง messagingPackageId นี่สำคัญ เพราะถ้า SDK upgrade version ค่านี้อาจเปลี่ยน การดึงจาก SDK โดยตรงทำให้ไม่ต้องมานั่ง update เอง ปัจจุบันมันมีแค่ Testnet นะ ซึ่งมันก็คือ packageId สำหรับส่วนของ messaging นั้นเเหละ ส่วนอื่นๆก็น่าจะเคยเห็นกันมาบ้างแล้วจากบทความก่อนๆ
export function createMessagingClient(keypair: Ed25519Keypair) {
const client = new SuiClient({
url: config.fullnodeUrl,
network: config.network,
mvr: {
overrides: {
packages: {
"@local-pkg/sui-stack-messaging": config.messagingPackageId,
},
},
},
})
.$extend(SealClient.asClientExtension({
serverConfigs: config.sealServers,
}))
.$extend(messaging({
walrusStorageConfig: { /* Walrus endpoints */ },
sessionKeyConfig: {
address: keypair.toSuiAddress(),
ttlMin: 30,
signer: keypair,
},
}));
return { client, messagingClient: client.messaging, keypair };
}
export function createUserClient() {
const keypair = createKeypair(config.suiPrivateKey);
return createMessagingClient(keypair);
}
มีจุดสำคัญที่เราอาจจะพึ่งเคยเห็นกันก็คือ เราจะใส่ mvr.overrides.packages ไม่งั้นจะเจอ error MVR Api URL is not set ตรงนี้เสียเวลาไป debug อยู่พอสมควร กับอีกส่วนนึงคือ walrus config ที่ต้องใส่ด้วยเพราะตัว SDK มันบังคับถึงจะยังไม่ได้ใช้ก็ตาม
MVR คือ Move Registry นะ ไว้ค่อยลงลึกกันอีกทีนึงแต่ฝั่ง npm อาจจะคุ้นๆกันอยู่ละ
ก่อนจะส่งหรืออ่านข้อความได้ เราต้องมี encryption key ของ channel นั้น ซึ่ง SDK มี method ให้ดึงออกมาจาก channel object บน chain
export async function getEncryptionKey(
messagingClient: any,
channelId: string,
userAddress: string,
) {
const channels = await messagingClient.getChannelObjectsByChannelIds({
channelIds: [channelId],
userAddress,
});
const ch = channels[0];
return {
$kind: "Encrypted" as const,
encryptedBytes: new Uint8Array(ch.encryption_key_history.latest),
version: ch.encryption_key_history.latest_version,
};
}
สิ่งที่ควรรู้คือ encryption key จะ rotate ทุกครั้งที่เชิญสมาชิกใหม่เข้า channel ดังนั้นถ้าเราเชิญใครเข้ามาใหม่ key เก่าจะใช้ไม่ได้ ต้อง fetch ใหม่ ส่วนนี้เราสร้างแยกออกมาจะได้เรียกใช้งานได้สะดวก
คำสั่งแรกที่เราต้องทำ คือสร้าง encrypted channel
export async function createRoom(memberAddresses: string[]) {
const { messagingClient, keypair } = createUserClient();
const { channelId, creatorCapId } =
await messagingClient.executeCreateChannelTransaction({
signer: keypair,
initialMembers: memberAddresses,
});
const memberCap = await messagingClient.getUserMemberCap(
keypair.toSuiAddress(), channelId
);
console.log("✅ Channel ID:", channelId);
}
executeCreateChannelTransaction ทำ 2 อย่างในครั้งเดียว — สร้าง channel + attach encryption key หลังสร้างเสร็จเราจะได้ channelId ที่ต้องเก็บไว้แชร์กับคนอื่น
ส่วน getUserMemberCap คือไปดึง MemberCap ของเรา ซึ่งเป็น object บน chain ที่ prove ว่าเราเป็นสมาชิกห้องนี้ ต้องมี MemberCap ถึงจะส่งข้อความได้
ดู channels ที่เราเข้าร่วมอยู่ ก็แค่เรียก getChannelMemberships
const result = await messagingClient.getChannelMemberships({
address: myAddress,
});
ส่วนดูสมาชิกของ channel ก็ใช้ getChannelMembers
const result = await messagingClient.getChannelMembers(channelId);
อันนี้จะทำหรือไม่ทำก็ได้ เพราะง่ายมาก SDK จัดการทุกอย่างให้แล้ว
const { digest, addedMembers } =
await messagingClient.executeAddMembersTransaction({
signer: keypair,
channelId,
memberCapId: memberCap.id.id,
newMemberAddresses,
});
⚠️ มีแค่ creator (คนที่สร้างห้อง) เท่านั้นที่เชิญคนเข้าได้นะ ถ้าคนอื่นลอง invite จะ error และอย่าลืมว่าเมื่อเชิญสมาชิกใหม่ encryption key จะ rotate — ต้อง fetch key ใหม่ก่อนส่งข้อความ
มาถึงส่วนสำคัญละ ส่วนนี้จะเป็น interface เอาไว้เราเข้าห้อง chat ผ่าน CLI ซึ่งจะทำให้เราสามารถ ส่ง และอ่านข้อความได้ โดยจะแบ่งเป็น 3 ส่วนหลักๆคือ
ส่วนที่ 1 — ดึง MemberCap, encryption key, และแสดงข้อความเก่า 10 ข้อความย้อนหลัง
const { messagingClient, keypair } = createUserClient();
const myAddress = keypair.toSuiAddress();
const memberCap = await messagingClient.getUserMemberCap(myAddress, channelId);
const encryptedKey = await getEncryptionKey(messagingClient, channelId, myAddress);
const { messages } = await messagingClient.getChannelMessages({
channelId,
userAddress: myAddress,
limit: 10,
direction: "backward",
});
ส่วนที่ 2: Polling — เช็คข้อความใหม่ทุก 3 วินาที
SDK ไม่มี WebSocket ดังนั้นเราต้อง poll เอง หลักการคือเราจะจำ lastMessageCount ไว้ แล้วถาม SDK ว่ามีข้อความใหม่กว่านี้ไหม ตอนนี้เลยทำเป็น interval ง่ายๆ
const [channel] = await client.getChannelObjectsByChannelIds({
channelIds: [channelId], userAddress: myAddress,
});
const pollingState = {
lastMessageCount: BigInt(channel.messages_count),
lastCursor: null as bigint | null,
channelId,
};
setInterval(async () => {
const { messages } = await client.getLatestMessages({
channelId, userAddress: myAddress, pollingState,
});
for (const msg of messages) {
if (msg.sender !== myAddress) printMessage(msg);
}
pollingState.lastMessageCount += BigInt(messages.length);
}, 3000);
ส่วนที่ 3: Input Loop — ใช้ในการส่งข้อความ
const rl = createInterface({ input: process.stdin, output: process.stdout });
rl.on("line", async (line) => {
if (text === "/quit") { process.exit(0); }
const { digest } = await messagingClient.executeSendMessageTransaction({
signer: keypair,
channelId,
memberCapId: memberCap.id.id,
message: text,
encryptedKey,
});
});
โดยส่วนนี้เราจะอ่านข้อความจาก cli ซึ่งทุกข้อความที่ส่ง จะถูก encrypt ด้วย Seal → store เป็น object บน Sui → ได้ transaction digest กลับมา ลองไปดูที่ https://suiscan.xyz/ ได้เลย (อย่าลืมเปลี่ยนเป็น Testnet)
สุดท้ายละทำการรวมทุก command ด้วย Commander
program.command("create-room")
.argument("[members...]", "Sui addresses to invite")
.action(async (members) => { await createRoom(members || []); });
program.command("list-rooms").action(listRooms);
program.command("chat")
.argument("<channelId>", "Channel ID")
.action(interactiveChat);
program.command("invite")
.argument("<channelId>", "Channel ID")
.argument("<addresses...>", "Sui addresses to invite")
.action(inviteMember);
program.command("members")
.argument("<channelId>", "Channel ID")
.action(listMembers);
เมื่อประกอบร่างทุกอย่างแล้ว ลองรันทดสอบกันดูนะ
# Help command
npm run dev -- --help
# Create room
npm run dev -- create-room
# Create room and invite
npm run dev -- create-room 0xALICE_ADDRESS
# List all rooms
npm run dev -- list-rooms
# Join chat room
npm run dev -- chat <channelId>
# List all members
npm run dev -- members <channelId>
# Invite bot to chat room
npm run dev -- invite <channelId> <BOT_ADDRESS>
เรียบร้อยเราได้ตัว client แล้วเเต่.... ยังไม่จบนะ มาได้เกินครึ่งทางละ ไว้ต่อ EP หน้า ซึ่งเป็น EP สุดท้ายละ ตัว EP หน้าเราจะมาต่อกับ AI โดยให้ AI ตอบโต้กับคนในห้อง ซึ่งเราเลือกใช้ Groq AI มันมี Free API key อยู่ด้วย หรือใครจะใช้ตัวอื่นๆก็เตรียมๆไว้ได้เลย เพราะส่วนนี้เเค่เป็นการเขียน API เชื่อมต่อกับ AI เท่านั้น
เจอกันจ้า