EP 5: การรับส่งข้อมูลเข้ารหัสด้วย Walrus + Seal
รวมร่าง Walrus และ Seal สร้าง Privacy dApp ที่ใช้งานได้จริง

สวัสดีครับ เดินทางมาถึง EP สุดท้ายแล้ว สำหรับบทความเกี่ยวกับการใช้ Walrus และ Seal ในการสร้างเนื้อหาที่มีความ Privacy ก่อนอื่นเราย้อนไปดูกันสักนิดดีกว่า เราทำอะไรมาแล้วบ้าง
https://onthemove.contributiondao.com/ep-1-walrus-and-seal เริ่มต้นด้วยตัวอย่างโค๊ด ในการส่งข้อมูลไปเก็บที่ Walrus
https://onthemove.contributiondao.com/ep-2-walrus-programmable-storage ทำความรู้จักการเขียน Programmable Storage
https://onthemove.contributiondao.com/ep-3-walrus-seal-send-secret-message รู้จัก Seal และตัวอย่างโค๊ด ในการเข้ารหัสข้อมูล
https://onthemove.contributiondao.com/ep-4-seal-on-chain-access-control ตัวอย่าง Move Smart Contract ในการจัดการ Access Control
ลองไปอ่านดูย้อนหลังกันก่อนน๊ะ เพราะใน EP เราจะนำทุกอย่างมาประกอบร่างกันแล้วครับ
จากตัวอย่างโค๊ดล่าสุดนั้น เราเขียนละติดปัญหาเกี่ยวกับฟังก์ชั่นของ Walrus พอสมควร เเละพบว่าจริงๆเเล้ว เราสามารถเรียกใช้งาน API ผ่าน Provider endpoint เพื่อ ทำการรับเเละส่งข้อมูลไปยัง Walrus network ได้ เลยขอเปลี่ยนวิธีการรับส่งข้อมูลกันหน่อยนึงนะ
มาเริ่มกันเลย นี้คือตัวอย่างโค๊ดทั้งหมดที่นำมาประกอบร่างและ มีการทำให้โค๊ดอ่านง่ายขึ้นละ
ทำการเเก้ไข
.envกันก่อนSENDER_PRIVATE_KEY=suiprivatekey1xxx // Sender private key RECEIVER_PRIVATE_KEY=suiprivatekey1xxx //Receiver private key NETWORK=testnet PACKAGE_ID=0x0xxxx // Package Id ที่เราได้จาก EP.4สร้างไฟล์
config.tsimport "dotenv/config"; // --- Env Checks --- if (!process.env.SENDER_PRIVATE_KEY || !process.env.RECEIVER_PRIVATE_KEY) { throw new Error("Bro, you forgot the private keys in .env file."); } export const CONFIG = { // Keys SENDER_KEY: process.env.SENDER_PRIVATE_KEY, RECEIVER_KEY: process.env.RECEIVER_PRIVATE_KEY, // Network & Package SUI_NETWORK: process.env.SUI_NETWORK || "testnet", PACKAGE_ID: process.env.PACKAGE_ID // Seal Key Servers (Testnet) KEY_SERVERS: [ "0x73d05d62c18d9374e3ea529e8e0ed6161da1a141a94d3f76ae3fe4e99356db75", "0xf5d14a81a982144ae441cd7d64b09027f116a468bd36e7eca494f750591623c8", ], // Dummy 32-byte hex for initial encryption setup PLACEHOLDER_POLICY_ID: "0x" + "0".repeat(64), }; // --- Walrus Endpoints --- export const WALRUS_CONFIG = { EPOCHS: Number(process.env.WALRUS_EPOCHS ?? 1), PUBLISHERS: [ "https://publisher.walrus-testnet.walrus.space", "https://wal-publisher-testnet.staketab.org", "https://walrus-testnet-publisher.redundex.com", "https://walrus-testnet-publisher.nodes.guru", ].filter(Boolean) as string[], AGGREGATORS: [ "https://aggregator.walrus-testnet.walrus.space", "https://wal-aggregator-testnet.staketab.org", "https://walrus-testnet-aggregator.redundex.com", "https://walrus-testnet-aggregator.nodes.guru", ].filter(Boolean) as string[], };มีส่วนที่น่าสนใจเพิ่มขึ้นมาคือ
WALRUS_CONFIGซึ่งส่วนนี้จะมีทั้งWALRUS_EPOCH เป็นการระบุอายุการเก็บรักษาข้อมูลนั้นเอง
PUBLISHERS คือ endpoint สำหรับใช้ในการอัพโหลดข้อมูลไปยัง Walrus network
AGGREGATORS คือ endpoint สำหรับการอ่านข้อมูลจาก Walrus netwokr
สร้างไฟล์
walrus.tsimport { WALRUS_CONFIG } from "./config"; /** * Uploads raw bytes to Walrus. * It tries multiple publisher nodes until one works. */ export async function uploadBlob(data: Uint8Array): Promise<string> { let lastError: unknown = null; for (const baseUrl of WALRUS_CONFIG.PUBLISHERS) { const url = `\({baseUrl}/v1/blobs?epochs=\){WALRUS_CONFIG.EPOCHS}`; console.log(`☁️ Trying upload to: ${baseUrl}...`); try { const res = await fetch(url, { method: "PUT", body: data, }); if (!res.ok) { throw new Error(`HTTP \({res.status}: \){res.statusText}`); } const info = (await res.json()) as any; const blobId = info.newlyCreated?.blobObject?.blobId || info.alreadyCertified?.blobId; if (!blobId) throw new Error("Invalid response format from Walrus"); return blobId; // Success! } catch (e) { console.error(`-> Failed at ${baseUrl}, trying next...`); lastError = e; } } throw new Error(`All Walrus publishers failed. RIP. Error: ${lastError}`); } /** * Downloads raw bytes from Walrus. * It tries multiple aggregator nodes until one works. */ export async function downloadBlob(blobId: string): Promise<Uint8Array> { let lastError: unknown = null; for (const baseUrl of WALRUS_CONFIG.AGGREGATORS) { const url = `\({baseUrl}/v1/blobs/\){blobId}`; console.log(`⬇️ Trying download from: ${baseUrl}...`); try { const res = await fetch(url); if (!res.ok) throw new Error(`HTTP ${res.status}`); const buffer = await res.arrayBuffer(); return new Uint8Array(buffer); // Success! } catch (e) { console.error(`-> Node ${baseUrl} failed, trying next...`); lastError = e; } } throw new Error(`All Walrus aggregators failed. Blob might be gone.`); }ตรงไปตรงมาเลย ก่อนหน้านี้เราต้องเรียกผ่าน Walrus library ในการอ่านเเละเขียนข้อมูลใช่ไหม ซึ่งเท่าที่ลองดูเจอปัญหางอแงบ้าง เลยตัดจบด้วยการใช้วิธีเรียกผ่าน HTTP Protocol เลยทีเดียวจบ
สร้างไฟล์
main.tsimport { getFullnodeUrl, SuiClient } from "@mysten/sui/client"; import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519"; import { decodeSuiPrivateKey } from "@mysten/sui/cryptography"; import { SealClient, SessionKey, EncryptedObject } from "@mysten/seal"; import { Transaction } from "@mysten/sui/transactions"; import { fromHex, toHex } from "@mysten/sui/utils"; import { randomBytes } from "node:crypto"; import { CONFIG } from "./config"; import { uploadBlob, downloadBlob } from "./walrus"; let suiClient: SuiClient; let sealClient: SealClient; async function main() { console.log("🚀 Starting the Walrus x Seal Privacy Demo..."); // 1. Setup Network await initClients(); // 2. Setup Users const sender = getKeypair(CONFIG.SENDER_KEY); const receiver = getKeypair(CONFIG.RECEIVER_KEY); console.log(`👤 Sender: ${sender.getPublicKey().toSuiAddress()}`); console.log(`👤 Receiver: ${receiver.getPublicKey().toSuiAddress()}`); const secretMessage = "Yo fam, this is exclusive content! 📸 (Secured by Seal)"; // 3. Sender Flow const policyObjectId = await runSenderFlow(secretMessage, receiver, sender); // 4. Simulate Network Delay console.log("\n⏳ Chilling for 5s to let Walrus propagate..."); await sleep(5000); // 5. Receiver Flow await runReceiverFlow(policyObjectId, receiver); console.log("\n✨ Boom! Mission accomplished."); } // ============================================================================ // WORKFLOWS // ============================================================================ async function runSenderFlow( secret: string, recipient: Ed25519Keypair, sender: Ed25519Keypair ): Promise<string> { console.log(`\n--- [Step 1] Sender Turn ---`); console.log(`🔒 Encrypting payload...`); // Prepare a randomized ID for encryption // We add a nonce to ensure the ID is unique even if the policy ID is static const policyBytes = fromHex(CONFIG.PLACEHOLDER_POLICY_ID); const nonce = new Uint8Array(randomBytes(5)); const idBytes = new Uint8Array(policyBytes.length + nonce.length); idBytes.set(policyBytes, 0); idBytes.set(nonce, policyBytes.length); // Encrypt locally const plainBytes = new TextEncoder().encode(secret); const { encryptedObject } = await sealClient.encrypt({ threshold: 2, packageId: CONFIG.PACKAGE_ID, id: toHex(idBytes), data: plainBytes, }); console.log(`-> Encrypted size: ${encryptedObject.byteLength} bytes`); // Upload to Walrus (Using our helper) const blobId = await uploadBlob(encryptedObject); console.log(`✅ Walrus Blob ID: ${blobId}`); // Mint the Policy Object on Sui console.log("⛓️ Minting SecretMessage object on-chain..."); const tx = new Transaction(); tx.moveCall({ target: `${CONFIG.PACKAGE_ID}::message::create_secret_message`, arguments: [ tx.pure.address(recipient.getPublicKey().toSuiAddress()), tx.pure.string(blobId) ], }); const result = await suiClient.signAndExecuteTransaction({ signer: sender, transaction: tx, options: { showObjectChanges: true }, }); // Find the new object ID const created = result.objectChanges?.find( (c: any) => c.type === "created" && "objectId" in c && c.objectType.includes("::message::SecretMessage") ); if (!created || !("objectId" in created)) { throw new Error("Tx failed. Could not find created object."); } const objectId = created.objectId as string; console.log(`📡 Policy Object created: ${objectId}`); return objectId; } async function runReceiverFlow(policyObjectId: string, recipient: Ed25519Keypair) { console.log(`\n--- [Step 2] Receiver Turn ---`); console.log(`📦 Fetching policy object: ${policyObjectId}`); // 1. Get Blob ID from Chain const obj = await suiClient.getObject({ id: policyObjectId, options: { showContent: true }, }); const fields = (obj.data?.content as any)?.fields; if (!fields) throw new Error("Policy object is empty/invalid."); const blobId = fields.walrus_blob_id; console.log(`📦 Found Walrus ID: ${blobId}`); // 2. Download from Walrus (Using our helper) const encryptedBytes = await downloadBlob(blobId); console.log(` -> Got ${encryptedBytes.byteLength} bytes`); // 3. Validation const info = EncryptedObject.parse(encryptedBytes); if (!info.id || !info.threshold) throw new Error("Data looks corrupted."); // 4. Session Key (Ephemeral security) console.log(`🔑 Generating temp session key...`); const recipientAddr = recipient.getPublicKey().toSuiAddress(); const sessionKey = await SessionKey.create({ address: recipientAddr, packageId: CONFIG.PACKAGE_ID, ttlMin: 10, signer: recipient, suiClient, }); // 5. Build Proof (seal_approve) console.log("🛡️ Building auth proof..."); const tx = new Transaction(); tx.setSender(recipientAddr); tx.moveCall({ target: `${CONFIG.PACKAGE_ID}::message::seal_approve`, arguments: [ tx.pure.vector("u8", fromHex(info.id)), tx.object(policyObjectId), ], }); const txBytes = await tx.build({ client: suiClient, onlyTransactionKind: true }); // 6. Decrypt console.log("🔓 Asking Key Servers to decrypt..."); const decryptedBytes = await sealClient.decrypt({ data: encryptedBytes, sessionKey, txBytes, }); const msg = new TextDecoder().decode(decryptedBytes); console.log(`\n🎉 SUCCESS! Message: "${msg}"`); } async function initClients() { suiClient = new SuiClient({ url: getFullnodeUrl(CONFIG.SUI_NETWORK) }); sealClient = new SealClient({ suiClient, serverConfigs: CONFIG.KEY_SERVERS.map((id) => ({ objectId: id, weight: 1, })), verifyKeyServers: false, // skipping strict verification for testnet demo }); } function getKeypair(secretKey: string) { return Ed25519Keypair.fromSecretKey(decodeSuiPrivateKey(secretKey).secretKey); } function sleep(ms: number) { return new Promise((r) => setTimeout(r, ms)); } main().catch((e) => { console.error("FATAL ERROR:", e); process.exit(1); });มาถึงไฟล์สุดท้าย พระเอกของเรานั้นเอง มาไล่ทีละขั้นตอนกันนะ
เริ่มจากเป็นการ
initClients()ก่อนส่วนนี้จะเห็นว่าไม่มีการเรียกใช้งาน Walrus แล้ว เพราะเราจะส่งข้อมูลผ่าน HTTP กันส่วนของฟังก์ชั่น
runSenderFlowนั้นจะเป็นนำข้อความของเราไปเข้ารหัสด้วย Seal หลังจากนั้นก็ทำการอัพโหลดข้อมูลไปยัง Walrus network นั้นเอง เเละเมื่อเราได้blobIdมาแล้วก็ทำการเรียกใช้งานฟังก์ชั่น Sui smart contract ที่เราได้เขียนไว้เเล้วเพื่อกำหนดผู้รับส่วนของฟังก์ชั่น
runReceiverFlowทำตรงกันข้ามนั้นเองโดยนำblobIdไปดาวโหลดข้อมูลจาก Walrus network ลงมาก่อนแล้วทำการถอดรหัสข้อมูลด้วย Seal หลังจากผู้รับต้องทำการเรียก Sui smart contract เพื่อยืนยันความเป็นเจ้าของนั้นเอง
เรียบร้อยแล้วง่ายไหม ง่ายมั๊ง ลองเอาไปรันดูนะครับ เเล้วเอาผลลัพธ์มาโชว์ด้วยนะ นี้คือตัวอย่างเล็กๆน้อยนะ ซึ่งสิ่งที่เราได้สร้างขึ้นมานั้นจะมี คุณสมบัติ
Client-Side Encryption - ข้อมูลเข้ารหัสบนอุปกรณ์ผู้ใช้
Decentralized Storage - เก็บบน Walrus แบบกระจายศูนย์
On-Chain Access Control - Smart Contract ควบคุมสิทธิ์การเข้าถึง
Threshold Cryptography - ไม่มี Single Point of Failure
Programmable Ownership - สิทธิ์เป็น Object ที่โอนย้ายได้
ซึ่งจากคุณสมบัติเหล่านี้เราสามารถนำไปต่อยอดเป็น use case อื่นได้อีกมายเลยนะ เช่น
Creator Platforms ทำพวก Subscription-based content ได้สบายๆ
Gaming Content ทำพวก Unlockable content ตามเงื่อนไข On-Chain หรือจะทำ Secret quests และ time-limited events ก็ได้นะ
AI & Data Markets สาย AI ขาย AI models และ datasets แบบ encrypted
NFT Utilities ทำ NFT ที่มีคอนเทนต์จริงซ่อนอยู่ :P
เอาละ ลองไปต่อยอดกันดูนะ ไว้เจอกันใหม่
ปิดท้ายด้วยเเหล่งข้อมูลที่สามารถศึกษาได้เพิ่ม





