สัปดาห์ที่ผ่านมาผมใช้เวลาพยายามทำความเข้าใจว่าโดเมน .night ทำงานอย่างไรจริงๆ ไม่ใช่จากการอ่านสเปก แต่จากการสร้างบางอย่างที่เชื่อมต่อกับเชนโดยตรง ผลลัพธ์ที่ได้คือโปรแกรมดูโปรไฟล์ขนาดเล็ก คุณแค่พิมพ์ชื่ออย่าง tomin.night แล้วมันจะดึงข้อมูลโปรไฟล์มาจากบล็อกเชนโดยตรง ไม่ต้องใช้ registrar API ไม่ต้องมีกำแพงการยืนยันตัวตน (auth wall) มีเพียงสมาร์ทคอนแทรคและ JavaScript เท่านั้น

สิ่งที่จะกล่าวต่อไปนี้คือวิธีการทำงาน สิ่งที่ผมได้เรียนรู้ และกับดักเฉพาะเจาะจงสองอย่างที่ทำให้ผมเสียเวลาไปทั้งบ่าย

ทำไมชื่อบน On-Chain ถึงสำคัญ

บน Midnight โดเมน .night ถูกจัดการโดย Midnames แทนที่จะเป็นการสอบถามไปยังเซิร์ฟเวอร์กลางของบริษัท คุณสามารถอ่านข้อมูลได้โดยตรงจากสมาร์ทคอนแทรคที่อยู่บนเชน ตัว Registry เองเป็นผู้ถือครองโดเมน เจ้าของ และฟิลด์โปรไฟล์ต่างๆ ที่เจ้าของแนบมา เนื่องจากข้อมูลนั้นอยู่บนเชน ตัวตน (identity) จึงสามารถพกพาไปได้ (portable) คุณไม่ได้เช่ามันจากแพลตฟอร์มที่สามารถเปลี่ยนแปลงเงื่อนไขหรือปิดบริการเมื่อไหร่ก็ได้ หากคุณควบคุมคีย์ได้ คุณก็ควบคุมชื่อได้

การเปลี่ยนแปลงนี้สำคัญสำหรับนักพัฒนาด้วยเช่นกัน เมื่อคุณสร้างระบบบน DNS แบบดั้งเดิม คุณต้องรับมือกับข้อจำกัดด้านอัตราการเรียกใช้งาน (rate limits), API keys และคำสัญญาเรื่อง uptime แต่ที่นี่ สถานะของคอนแทรค (contract state) คือแหล่งข้อมูลที่ถูกต้องที่สุด (source of truth) แอปพลิเคชันของคุณจะอ่านข้อมูลในแบบเดียวกับที่แอปพลิเคชันอื่นๆ อ่าน ไม่มีลำดับชั้นของ API ที่ได้รับสิทธิพิเศษ

โค้ดนั้นแทบจะง่ายเกินไป

ตัว @midnames/sdk จัดการงานหนักทั้งหมดให้ การแปลงโดเมนเป็นที่อยู่และโปรไฟล์ใช้โค้ดเพียงสองบรรทัดเท่านั้น:

const provider = createDefaultProvider({ networkId: "mainnet" });
const result = await resolveDomain(provider, "tomin.night");

แค่นั้นเลย Provider จะระบุเป้าหมายไปยังเครือข่าย และ Resolver จะสื่อสารกับคอนแทรค SDK จะส่งออบเจกต์ผลลัพธ์ที่รวมถึง flag บอกความสำเร็จ (success flag) หากไม่มีโดเมนนั้นอยู่ คุณไม่จำเป็นต้องเขียนการจัดการข้อผิดพลาดหลายชั้นหรือใช้ try-catch ครอบการทำงานของ RPC ที่ล้มเหลว SDK จะบอกคุณอย่างชัดเจนว่าไม่มีข้อมูลอยู่ตรงนั้น สิ่งนี้ทำให้การสร้าง UI เป็นเรื่องที่น่ารื่นรมย์อย่างไม่น่าเชื่อ คุณสามารถแยกเงื่อนไขตาม success flag และแสดงสถานะ “not found” ได้โดยไม่ต้องเดาว่าความล้มเหลวนั้นเกิดจากชื่อที่หายไปหรือโหนดตาย

สิ่งที่คุณจะได้รับกลับมา

เมื่อการค้นหาสำเร็จ ข้อมูล (payload) จะประกอบด้วยสองส่วนที่สำคัญ

Target คือที่อยู่กระเป๋าเงิน (wallet address) ที่โดเมนชี้ไป นี่คือประโยชน์หลัก มันเปลี่ยนที่อยู่แบบ hex ยาวๆ ให้กลายเป็นสิ่งที่มนุษย์สามารถอ่าน พิมพ์ และจดจำได้

Fields เก็บรายละเอียดโปรไฟล์ ไม่ว่าเจ้าของจะแนบอะไรไว้กับโดเมน—ลิงก์โซเชียล, อวตาร, หรือข้อความบันทึก—ก็จะอยู่ในโครงสร้างนี้ ฟิลด์เหล่านี้ไม่ได้ถูกเก็บไว้ใน MongoDB cluster ของบริษัทใดบริษัทหนึ่ง แต่มันคือฟิลด์ในสถานะของคอนแทรค (contract state) ซึ่งหมายความว่าแอปใดก็ตามที่รู้วิธีอ่าน Registry ก็สามารถแสดงโปรไฟล์แบบเดียวกันได้ โดยไม่ต้องมีการซิงค์ฐานข้อมูล

สองกับดักที่ทำให้ผมเสียเวลา

ไม่ใช่ทุกอย่างในการสร้างครั้งนี้ที่จะจบลงด้วยโค้ดสองบรรทัดและ success flag ผมเจออุปสรรคเฉพาะเจาะจงสองอย่างที่ควรระบุไว้เพื่อที่คุณจะได้ไม่ต้องทำพลาดซ้ำ

ความไม่สอดคล้องกันของเครือข่าย (Network Mismatch)

SDK รองรับทั้งสภาพแวดล้อม mainnet และ preprod ผมเสียเวลาไปนานพอสมควรในการดีบั๊กโดเมนที่ “ไม่มีอยู่จริง” ทั้งที่ชื่อก็ถูกต้อง โค้ดก็ดูถูกแล้ว Provider ก็ทำงานอยู่ ปัญหาก็คือสคริปต์ของผมกำลังสอบถามไปยัง preprod ในขณะที่ตัวโดเมนเองถูกลงทะเบียนไว้บน mainnet ข้อผิดพลาดที่ปรากฏดูเหมือนโดเมนหายไป แต่จริงๆ แล้วมันคือบริบทของเครือข่าย (network context) ที่หายไปต่างหาก

หากคุณกำลังค้นหาชื่อแล้วได้รับผลลัพธ์ว่าล้มเหลว ให้ตรวจสอบการตั้งค่า provider ก่อนที่จะดีบั๊กอย่างอื่น ตรวจสอบให้แน่ใจว่า networkId ของคุณตรงกับเครือข่ายที่โดเมนนั้นถูกสร้าง (minted) ขึ้นมาจริงๆ นี่คือความผิดพลาดประเภทที่ดูเหมือนจะชัดเจนเมื่อมองย้อนกลับไป แต่สังเกตได้ยากมากเมื่อคุณทึกทักไปเองว่าปัญหาอยู่ที่ตรรกะของคอนแทรค

ความยุ่งยากในการทำ Serialization

ข้อมูลที่ส่งกลับมาจาก SDK ไม่ใช่ JavaScript มาตรฐาน มันประกอบด้วยค่า BigInt และออบเจกต์ Map หากคุณพยายามส่งข้อมูลนั้นตรงๆ เข้าสู่ JSON.stringify เพื่อส่งไปยังเบราว์เซอร์ มันจะเกิด error หรือข้อมูลจะหายไปเงียบๆ เนื่องจาก BigInt ไม่มีรูปแบบการแสดงผล JSON ในตัว และ Map ก็ไม่ได้ถูก serialize ในแบบเดียวกับออบเจกต์ทั่วไป

สุดท้ายผมจึงต้องเขียน serializer ขึ้นมาเอง โดยมันจะไล่ตรวจสอบออบเจกต์ผลลัพธ์ แปลงค่า BigInt เป็นสตริง และเปลี่ยนอินสแตนซ์ของ Map ให้เป็นออบเจกต์ปกติก่อนที่การตอบกลับจะออกจากเซิร์ฟเวอร์ หากคุณกำลังสร้าง API ที่ให้บริการข้อมูล Midnight แก่ frontend ควรวางแผนขั้นตอนนี้ไว้ตั้งแต่เนิ่นๆ อย่าทึกทักเอาเองว่าผลลัพธ์จาก SDK จะพร้อมใช้งานกับ frontend ทันทีเพียงเพราะมันเป็น JavaScript

สถาปัตยกรรม (Architecture)

ผมตั้งใจเลือก stack ให้เรียบง่ายที่สุด ส่วน backend เป็น Node server ที่รันด้วย Express มันจะ import @midnames/sdk มาเพื่อรัน logic การทำ name resolution, จัดการความยุ่งยากในการทำ serialization และส่งข้อมูลออกไปเป็น JSON ที่สะอาดตา ส่วน frontend เป็นเพียง HTML และ vanilla JavaScript ธรรมดา ไม่ต้องมี build step ไม่ต้องใช้ framework และไม่ต้องมี wallet adapter

ผมเลือกที่จะรัน SDK บน backend แทนที่จะเป็นบน browser ด้วยเหตุผลทางปฏิบัติบางประการ มันช่วยแยกการตั้งค่า provider ออกจากฝั่ง client, ทำให้ผมมีที่เดียวในการจัดการความวุ่นวายเรื่อง serialization และหมายความว่า frontend มีหน้าที่เพียงแค่ดึงข้อมูลมาแสดงผลเท่านั้น

ส่วนที่ทำให้ผมประหลาดใจที่สุดคือ: การหาชื่อ (resolving a name) เป็นการอ่านข้อมูลแบบสาธารณะ (public read operation) คุณไม่จำเป็นต้องเชื่อมต่อ wallet ไม่ต้องมีการเซ็นชื่อ (signature) และไม่จำเป็นต้องให้ผู้ใช้ล็อกอินด้วยอะไรเลย หากโดเมนนั้นมีอยู่จริง สถานะของ contract ก็จะเปิดเผยให้ทุกคนที่เรียกดูสามารถมองเห็นได้ นี่คือความแตกต่างที่สำคัญจาก flow ของ web3 ทั่วไปที่ทุกการโต้ตอบมักจะเริ่มต้นด้วยการ "connect wallet" การอ่านข้อมูลอัตลักษณ์ (identity) บน Midnight นั้นเป็นแบบ permissionless ในลักษณะเดียวกับการอ่านเว็บไซต์สาธารณะที่เป็นแบบ permissionless

บทเรียนสำคัญที่ได้รับ

การสร้าง viewer ตัวนี้ทำให้ผมระลึกได้ว่า ส่วนที่ยากที่สุดของการพัฒนา blockchain มักไม่ใช่ตัว blockchain เอง Midnight ได้แก้ปัญหาที่ยากลำบากไปแล้ว นั่นคือการปล่อยให้ผู้คนเป็นเจ้าของชื่อและโปรไฟล์ของตนเองได้โดยไม่ต้องมีฐานข้อมูลกลาง ส่วนที่ยากสำหรับนักพัฒนาคือการจำให้ได้ว่ากำลังชี้ไปที่ network ไหน และการเขียน helper function เพื่อจัดการประเภทข้อมูล (data types) ให้สะอาด

โปรโตคอลนี้มอบอัตลักษณ์ที่พกพาไปได้ทุกที่ (portable identity) ให้กับคุณ หน้าที่ของคุณในฐานะนักพัฒนาคือการอ่านข้อมูลนั้นให้ถูกต้องและไม่ไปขัดขวางการใช้งานของผู้ใช้ จงรักษาโครงสร้าง (architecture) ให้เรียบง่าย แยก logic ที่ติดต่อกับ chain ออกจาก UI และปฏิบัติกับการอ่านข้อมูลสาธารณะเหมือนที่มันเป็น: นั่นคือการ query ฐานข้อมูลธรรมดาๆ ที่บังเอิญไปอยู่บน distributed ledger เท่านั้น

หากคุณต้องการดูโค้ดหรือลองรันด้วยตัวเอง สามารถดูซอร์สโค้ดฉบับเต็มได้ที่ https://github.com/tomiin/midnames-profile-viewer