สวัสดีครับ วันนี้มาพบกับบทความเกี่ยวกับการเขียน Smart Contract บน Solana กันนะครับ จริงๆ บทความนี้เกิดขึ้นมาเพราะผมอยากทบทวนและเรียบเรียงสิ่งที่เรียนรู้มาครับ และอยากลองดูว่าเข้าใจขั้นตอนการทำงานของมันมั้ย โดยการดูจาก Example HelloWorld ครับ
และเมื่อเสาร์ อาทิตย์ที่ผ่านมา (จริงๆคือ 4ทุ่ม - ตี1 เวลาไทย) ไปลองเรียน Solana Bootcamp - Chainlink วันละ 3ชั่วโมง 2วัน แล้วรู้สึกว่าได้รู้อะไรเยอะขึ้นมาก (แม้ว่าโค๊ดที่เขียนๆ พิมพ์ๆ จะเข้าใจไม่ถึง 50% ก็เถอะ) หลังจากไปนั่งอ่านเพิ่ม นั่งฝึกเขียนเพิ่ม ก่อนหน้านี้เคยลอง Anchor แต่ก็ยังไม่ค่อยเข้าใจมาก ส่วนนึงเพราะ syntax Rust ที่ไม่ชิน และไม่คล่อง จนพอเริ่มเข้าใจ Rust มากขึ้น มาอ่าน Solana หรือ Anchor อีกรอบ ก็เข้าใจมากขึ้นไปด้วย เลยลองเขียนเป็นบทความดูว่าจะออกมาเป็นยังไง
เตรียมความพร้อม
- เข้าใจ Rust เบื้องต้น (หรืออ่านโค๊ดแล้วพอเข้าใจ ก็โอเคครับ) - สำหรับคนมีพื้นฐานโปรแกรมมิ่ง ลองอ่านแบบเร็วๆ สั้นๆ Rust Playground และ Learn Rust in X minutes
- ใช้งาน Command Line พื้นฐานเป็น
- ติดตั้ง Node.js เรียบร้อยแล้ว เข้าใจ JavaScript หรือ TypeScript
Step 0 - Solana คร่าวๆ
- หน่วยคือ SOL และ Lamports โดย 1 Lamport มีค่า 0.000000001 SOL
- มี clusters หลักๆคือ Local (Localnet/Test Validator), Devnet, Testnet และ Mainnet beta
- Program หรือเรียกอีกอย่างว่า Smart Contract (ใน chain อื่นๆ)
- Program หลักๆ มี Native Program และ Solana Program Library (SPL)
- จากรูปด้านบน จะเห็นว่า เราสามารถเขียน Program ได้หลายภาษาไม่ว่าจะเป็น Rust, C หรือ C++
- สามารถส่ง transaction หรือ query ด้วย Client ต่างๆ ผ่าน JSON-RPC API
- ฝั่ง Client ทำได้ทั้ง CLI, JavaScript SDK, Rust SDK หรืออื่นๆ
- Account ใน Solana ใช้สำหรับเก็บ state แบ่งหลักได้ 3 แบบ Data Account, Program Account และ Native Account.
- Data account มี System owned account และ Program derived address (PDA) account.
- Program account จะไม่เก็บ state
- สมมติเราสร้าง Program (Smart Contract) ขึ้นมา 1 ตัว เป็น counter ง่ายๆ นับเลข ต้องมี 2 account คือ 1. ไว้เก็บโค๊ด และ 2. ไว้เก็บข้อมูล state.
Reference : Solana Cookbook
Step 1 - ติดตั้งโปรแกรม
ติดตั้ง Rust
อย่างแรก ติดตั้ง Rust ก่อนครับ โดยเข้าไปที่เว็บ rustup จะมีขั้นตอนการติดตั้ง (ถ้าเป็น Windows ก็จะเป็นตัว urstup-init.exe ดาวน์โหลดไป install ได้เลย)
ตัว rustup จะติดตั้งพร้อมกับ rustc ที่เป็น compiler และ cargo เป็นตัว Package Manager คล้ายๆกับ Yarn / npm ของฝั่ง Node.js
เช็คว่า ติดตั้งเรียบร้อยมั้ย ด้วยคำสั่ง:
และ
ติดตั้ง Solana CLI
ขั้นตอนนี้ เราจะติดตั้งตัว Solana CLI กันนะครับ โดยเราสามารถเลือกติดตั้ง แต่ละเวอร์ชั่นได้ เช่น ด้านล่าง ติดตั้ง v1.10.6 ซึ่งเป็นเวอร์ชั่น beta
หากใครอยากได้ที่เป็น stable version ก็สามารถติดตั้งได้ ด้วยการเปลี่ยน v1.10.6
เป็น stable แบบนี้
เช็คว่า solana ติดตั้งเรียบร้อยแล้ว :
ติดตั้ง Node.js
สำหรับใครยังไม่มี Node.js สามารถติดตั้งได้ โดยเลือกดาวน์โหลดแบบ 16.14.2 LTS (ณ ช่วงเวลาที่เขียน)
หรือผ่าน Homebrew ก็แค่
สำหรับ Windows สามารถเลือกไฟล์ .msi เพื่อทำการติดตั้งได้เลยครับ
เช็คเวอร์ชั่น
สรุป เวอร์ชันในเครื่องของผม ดังนี้
- Rustup - 1.24.3
- Rustc - 1.59.0
- Cargo - 1.59.0
- Solana - 1.9.14
- Node.js - 16.14.2
Step 2 - Solana CLI
ต่อมา เราจะ setup และตั้งค่า CLI รวมถึง local cluster กันก่อนครับ ก่อนที่จะไปเริ่มสร้างโปรเจ็คกัน
ขั้นแรก ตั้งค่า config ให้เป็น localhost
ต่อมาสร้าง Keypair ขึ้นมา
ระบบจะให้เราใส่รหัส BIP39 Passphrase เพื่อเพิ่มความปลอดภัยขึ้น (สำหรับ dev ไม่ต้องใส่ก็ได้ครับ) แนะนำว่ากระเป๋า dev ไม่ควรเอาไปใช้กับเงินจริงๆนะครับ
คำ 12 คำที่เราต้องจดไว้ หากเราต้องการสร้าง KeyPair เพื่อใช้งานจริงๆ อย่าให้ใครรู้ Seedphrase รวมถึง ข้อมูล Keypair ที่เป็น id.json
นะครับ เพราะข้างในคือ public key + private key สามารถเอาไปใช้งานได้เลย (แต่ถ้า dev ก็ไม่เป็นไร เราสร้างแล้วท้ิง ไม่ได้ใช้อยู่แล้ว)
เช็คว่าถูกต้องมั้ย ด้วยคำสั่ง
จะได้ผลลัพธ์ประมาณนี้
ต่อมา ต้องรัน local cluster (ควรเปิด Terminal อีกหน้าไว้) ด้วยคำสั่ง
สามารถดู logs ได้ ด้วยคำสั่ง (เปิด Terminal อีกแท็ป)
Step 3 - สร้างโปรเจ็ค
เริ่มสร้างโปรเจ็ค โดยอ้างอิงจาก ตัว Source Code จาก โปรเจ็ค Example Hello World ของ Solana นะครับ ซึ่งวิธีการเรียนรู้ที่ดีที่สุด คือ อ่านโค๊ดและลงมือทำ แม้จะเป็น Hello World ก็ตาม
ต่อมาสร้างโปรเจ็คขึ้นมาด้วย cargo ผมตั้งชื่อว่าโปรเจ็คว่า solana-helloworld (ชื่อแล้วแต่เพื่อนๆเลย)
ตัว Cargo จะสร้างไฟล์ให้เราดังนี้
เราสร้างโดยใช้ --lib
จะได้ไฟล์ lib.rs
แต่ถ้าปกติจะเป็น main.rs
นะครับ
ไฟล์ Cargo.toml
จะได้เป็นแบบนี้
Dependencies ที่ใช้ แบ่งเป็น
- borsh และ borse-derive - สำหรับทำ Deserialize และ Serialize
- solana-program - สำหรับการเขียน Program บน Solana (EVM เรียก Smart Contract แต่ Solana เรียก Program)
ต่อมาที่ไฟล์ src/lib.rs
พิมพ์โค๊ดนี้ลงไป
ทดลอง build โปรแกรมดู ตัว Cargo จะทำการ download dependencies และ compile ครับ
จะได้ผลลัพธ์ว่า build เรียบร้อย แสดงว่าไม่ติดปัญหา
ทีนี้ตัว Solana เราต้อง build เป็น BPF - Berkeley Packet Filter ก็เลยเปลี่ยน script เป็นแบบนี้
โดยกำหนด output ไฟล์คือโฟลเดอร์ dist/program
หลังจาก build เสร็จ สังเกตไฟล์ dist/program
จะมี 2 ไฟล์คือ
สิ่งที่เราต้องทำคือ deploy ตัว solana_helloworld.so
ลง on-chain ที่ local-cluster ของเรา
Deploy ด้วยคำสั่ง
จะได้ผลลัพธ์เป็นเลข Program Id ของเรา
เท่านี้ ก็เรียบร้อย ในการ deploy ลง local cluster (สามารถดู logs ได้)
4. เชื่อม Client SDK
ต่อมาเราต้องใช้ JavaScript/ TypeScript เป็น Client เพื่อเรียก Program ที่เรา deploy ลง local cluster ผ่าน JSON-RPC ครับ
สร้าง package.json
เปล่าๆ ขึ้นมาก่อน ด้วยคำสั่ง
จากนั้นติดตั้ง solana/web3.js, borsh และ typescript ดังนี้
ไฟล์ package.json
จะได้แบบนี้
สร้างไฟล์ tsconfig.json
ขึ้นมา
สร้างโฟลเดอร์ client เอาไว้เก็บไฟล์ ดังนี้
main.ts
- เป็นไฟล์หลักเอาไว้รัน program
hello_world.ts
- ไฟล์ business logic ต่างๆ
utils.ts
- ไฟล์ utils
หากเราดู client/main.ts
เราจะเห็นว่า การทำงานคร่าวๆ แม้จะยังไม่เห็น implementation คือ เริ่มจาก
- เช็ค connection local cluster ว่าเชื่อมต่อมั้ย
- เช็ค program ที่เรา deploy ไว้ถูกต้องมั้ย
sayHello
เป็นการส่ง transaction ไปที่ program
reportGreetings
เป็นการดึงข้อมูล state (query) ของ program มาแสดง
ไฟล์ utils ก็ไม่มีอะไรมาก เป็นแค่ helper ที่ช่วย อ่านไฟล์ต่างๆ เช่น config หรือ keypair
ต่อมาไฟล์หลัก hello_world.ts
ถ้าหากว่าไม่ได้ใช้ชื่อโปรเจ็คว่า solana_helloworld อย่าลืมเปลี่ยน path ให้ตรงด้วยนะครับ
ลองรัน client ดูผลลัพธ์ครับ
จะได้ผลลัพธ์แบบนี้
และถ้าเรารันอีกรอบ ค่า counter ก็จะเพิ่มเรื่อยๆ
5. Deserialize/Serialize
เราจะเห็นว่า ตอนเราใช้ JavaScript SDK เรียกผ่าน JSON RPC เราต้องทำการแปลง Deserialize/Serialize รวมถึงต้องกำหนด Schema ให้ตรงกันด้วย ดูแล้วยุ่งยากนิดๆ ใช่มั้ย?
ถ้าใช้ Anchor จะสะดวก และลดขั้นตอนนี้ลงได้เยอะเลย เพราะ Anchor จะ auto De/Serialize ให้เราเลย
สังเกต ไฟล์ client/hello_world.ts
เราต้องกำหนด schema ด้วยแบบนี้
- โดย
GreetingAccount
เป็น class ที่เราต้องกำหนด ให้มันตรงกับ hello world program ของเรา
GreetingSchema
ก็ต้องกำหนด struct ให้ตรง (IDL Spec) (คล้ายๆ ABI ของ Solidity)
จะเห็นว่าตอน initial ค่า counter เป็น 0 เพราะเราไม่ได้ส่งอะไรไปให้ program เลย และ program ก็ไม่ได้รับค่าใดๆ
ทีนี้ลองมาปรับแก้ client/hello_world.ts
ซักนิด ให้ส่งค่า counter ไปแบบกำหนดเอง
จะเห็นว่าสิ่งที่ต้องเพิ่มคือ ทำ serialize โดยใช้ Schema กับ instance ที่สร้างไว้ พร้อม initial state สุดท้าย ส่งค่า instructionData
ที่เป็น Buffer ไป (ก่อนหน้านี้เป็น Buffer.alloc(0)
)
เราปรับแก้ src/lib.rs
ให้รับค่าได้ โดยแก้เป็น
คือ เช็คว่า ถ้า client ส่ง counter มามากกว่า 0 ก็ใช้ counter จาก client แต่ถ้าส่งมา counter = 0 ก็ให้มัน += 1 แบบตอนแรก
ทีนี้ function เราก็จะได้ 2 แบบคือ นับ counter ปกติ เพิ่มทีละหนึ่ง ต่อการ sayHello()
1 ครั้ง กับ แบบที่ set counter จาก client ได้เลย
msg!()
เราสามารถดู log ได้ จาก solana logs
นะครับ
เมื่อเราแก้ Program ก็ต้อง build, compile และ deploy ใหม่
ทีนี้ก็ลองรัน
ลองปรับแก้ counter ใน client/hello_world.ts
และลองสั่งรัน client ดู ว่าค่าเป็นค่าที่เราส่งไปมั้ย?
ได้ผลลัพธ์ เป็นอันเรียบร้อย
ก่อนจบ พวก script ต่างๆ ต้องมานั่งพิมพ์ตลอด ก็สร้างเป็น Makefile หรือใส่ใน script package.json
ก็ได้ แบบนี้
สร้าง script มาไว้เพื่อให้ง่าย Makefile
เวลาใช้งานก็แค่รันคำสั่งสั้นๆ (แต่ต้องมี make ในเครื่องนะ)
หรือ
หรือ package.json
ก็แค่เพิ่ม script ลงไป
และก็ใช้คำสั่งได้เหมือนกัน
อื่นๆ เพิ่มเติม
หากเราไม่ต้องการ local cluster ต้องการ deploy ไป devnet หรือ testnet เราสามารถเปลี่ยน config และ deploy ไม่ต้องใส่ก็ได้ครับ
Airdrop ให้กับตัวเอง ก่อน เพราะ devnet ต้องใช้ SOL และไม่มี SOL ให้เหมือน local (ขอได้มากสุด 2 SOL ต่อครั้ง 24 SOL ต่อวัน)
ขอแบบระบุ address
เช็ค address จาก keypair ได้ด้วยคำสั่ง
ถ้าต่อ devnet อย่าลืมปิด solana logs
ด้วยนะ หรืออยากดู log ก็ไม่ว่ากันครับ 🤣
สรุป
ถึงแม้ว่าบทความนี้ส่วนใหญ่จะเป็น Example Hello World แต่ก็มีหลายๆ อย่างให้เราเรียนรู้ครับ ให้เราเข้าใจขั้นตอนการสร้าง, build, deploy และการเรียก JSON RPC ผ่าน JavaScript SDK Client
แม้ว่าจะไม่เข้าใจโค๊ดบางส่วน หรือเข้าใจไม่หมด ก็ไม่เป็นไร อย่างน้อย เราก็เห็นภาพ และรู้ว่าเราไม่เข้าใจส่วนไหน ก็แค่ไปทำความเข้าใจส่วนนั้นเพิ่มเติม ลองเล่น ลองปรับแก้ ลองรันโปรแกรม สิ่งสำคัญคือ การลงมือทำต่างหาก หวังว่าบทความนี้จะเป็นประโยชน์ไม่มากก็น้อย แล้วเจอกันบทความถัดไปครับ
อ่านเพิ่มเติม
สำหรับอ่านเพิ่มเติม แนะนำ Solana Developer Resources ในนั้นรวมเว็บต่างๆ ที่น่าสนใจครับ เช่น Solana Cookbook และ soldev. ส่วนถ้าอยากอ่าน Rust แนะนำเว็บ Official ของ Rust ครับ มีหนังสือและ Rustling รวมถึง Rust by Example เช่นกัน