Bài 7: Bắt tay xây dựng DApp đầu tiên trên Solana (Phần 2)

Bài viết này sẽ tổng hợp các kiến thức cần có và hướng dẫn để bạn bắt đầu xây dựng DApp trên Solana. Tham khảo ngay!

Bài 7: Bắt tay xây dựng DApp đầu tiên trên Solana (Phần 2)

Trong Workshop 1, chúng ta đã hoàn thành Solana program cho hệ thống bỏ phiếu có trọng số. Trong bài này, chúng ta sẽ cùng nhau phát triển giao diện đơn giản cũng như kết nối giao diện đó với Solana program để thực hiện việc giao tiếp cho hệ thống.

Building the React App

Trong thư mục gốc của dự án Anchor, tạo một React app mới để ghi đè lên thư mục app hiện có:

npx create-react-app app --template typescript
cd app

Tham khảo bài số 2 bài số 3 để cài đặt ứng dụng cũng như các dependencies.

Kết nối đến ví

Ở các bài trước, chúng ta đã tìm hiểu về Goki, cách kết nối ví và lưu thông tin ví vào Redux để có thể sử dụng, quản lý một cách hiệu quả. Tương tự như vậy, chúng ta ứng dụng lại vào chương trình và tạo kết nối đến ví người dùng.

// view/app/index.tsx
import { useCallback, useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { useWalletKit, useSolana, useConnectedWallet } from '@gokiprotocol/walletkit'

import { Button, Col, Layout, Row, Typography } from 'antd'
import ListCandidates from 'view/listCandidates'
import CandidateCreate from 'view/candidateCreate'

import { setWalletInfo, WalletState } from 'store/wallet.reducer'
import { AppDispatch } from 'store'

const { Header, Content } = Layout

function App() {
 const dispatch = useDispatch<AppDispatch>()
 const wallet = useConnectedWallet()
 const { connect } = useWalletKit()
 const { providerMut } = useSolana()

 const fetchWalletInfo = useCallback(async () => {
   if (!wallet || !providerMut) return
   // TODO: fetch SOL balance
   const lamports = await providerMut.connection.getBalance(wallet.publicKey)
   let walletInfo: WalletState = {
     walletAddress: wallet.publicKey.toBase58(),
     balance: lamports,
   }
   dispatch(setWalletInfo(walletInfo))
 }, [providerMut, wallet])

 useEffect(() => {
   fetchWalletInfo()
 }, [fetchWalletInfo])

 return (
   <Layout style={{ height: '100vh' }}>
     <Header>
       <Col span={24}>
         {wallet ? (
           <Col span={24} style={{ color: 'white' }}>
             {wallet.publicKey.toBase58()}
           </Col>
         ) : (
           <Button type="primary" style={{ borderRadius: 40 }} onClick={connect}>
             Connect Wallet
           </Button>
         )}
       </Col>
     </Header>
     <Content style={{ padding: 40 }}>
       <Row gutter={[24, 24]}>
         <Col span={24}>
           <Row gutter={[24, 24]}>
             <Col flex="auto">
               <Typography.Title>List Candidates</Typography.Title>
             </Col>
             <Col>
               <CandidateCreate />
             </Col>
           </Row>
         </Col>
         <Col span={24}>
           <ListCandidates />
         </Col>
       </Row>
     </Content>
   </Layout>
 )
}

export default App

Giao diện bỏ phiếu

Danh sách Candidate

Giao diện List candidates thể hiện cách thông tin: candidate, start_date, end_date, …

Hình 1. Danh sách các ứng viên vào thông tin bỏ phiếu.

Hàm khởi tạo ứng viên (New candidate)

Giao diện khởi tạo candidate cần các thông tin cơ bản như địa chỉ token để bỏ phiếu, ngày bắt đầu, ngày kết thúc bỏ phiếu.

Hình 2. Giao diện khởi tạo thông tin cho ứng viên.

Hàm bỏ phiếu (Vote)

Hình 3. Cử tri nhập số lượng tokens để bầu cho ứng viên.

Kết nối đến Solana Program (Smart Contract)

Sao chép tệp target/types/l6.ts vào thư mục app/config và đặt tên idl.ts. Tệp này giúp định nghĩa các giao diện (interface) của Solana program. Nó giúp các chương trình off-chain, có thể là frontend hoặc backend với các ngôn ngữ khác nhau, biết các để giao tiếp với chương trình chạy on-chain. Nếu các bạn đã quen thuộc với Ethereum thì tệp IDL này có ý nghĩa tương đương với ABI.

Tiếp đến chúng ta phải tạo file config để định nghĩa kết nối. Cụ thể là mạng devnet.

// app/src/config/index.ts
import * as anchor from '@project-serum/anchor'
import { clusterApiUrl } from '@solana/web3.js'

import { IDL } from './idl'

export const DEFAULT_COMMITMENT = 'confirmed'
export const DEFAULT_CLUSTER = 'devnet'
export const PROGRAM_ADDRESS = new anchor.web3.PublicKey(
 'HCoUastFpW7wB9Ue4o4YHy27VTuiJEo7h9hKmhnXDQhD',
)
export const NODE_URL = clusterApiUrl(DEFAULT_CLUSTER)

export type CandidateData = {
 address: string
 mint: string
 amount: number
 startTime: number
 endTime: number
}

// Function support
export const getProvider = (wallet: any) => {
 const connection = new anchor.web3.Connection(NODE_URL, DEFAULT_COMMITMENT)
 return new anchor.Provider(connection, wallet, {
   preflightCommitment: DEFAULT_COMMITMENT,
 })
}

export const getProgram = (wallet: any) => {
 const provider = getProvider(wallet)
 return new anchor.Program(IDL, PROGRAM_ADDRESS, provider)
}

Viết hàm gọi và truyền vào các tham số định nghĩa ở IDL để tạo ứng viên

await program.rpc.initializeCandidate(new anchor.BN(startTime), new anchor.BN(endTime), {
  accounts: {
    authority: wallet.publicKey,
    candidate: candidate.publicKey,
    treasurer,
    mint: new anchor.web3.PublicKey(mintAddress),
    candidateTokenAccount,
    tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID,
    associatedTokenProgram: anchor.utils.token.ASSOCIATED_PROGRAM_ID,
    systemProgram: anchor.web3.SystemProgram.programId,
    rent: anchor.web3.SYSVAR_RENT_PUBKEY,
  },
  signers: [candidate],
})

Tạo hàm bỏ phiếu cho ứng viên

await program.rpc.vote(new anchor.BN(amount), {
     accounts: {
     authority: wallet.publicKey,
     candidate: candidatePublicKey,
     treasurer,
     mint: candidateData.mint,
     candidateTokenAccount,
     ballot,
     voterTokenAccount: walletTokenAccount,
     tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID,
     associatedTokenProgram: anchor.utils.token.ASSOCIATED_PROGRAM_ID,
     systemProgram: anchor.web3.SystemProgram.programId,
     rent: anchor.web3.SYSVAR_RENT_PUBKEY,
  },
  signers: [],
})

Tạo hàm close vote

await program.rpc.close({
 accounts: {
   authority: wallet.publicKey,
   candidate: candidatePublicKey,
   treasurer,
   mint: candidateData.mint,
   candidateTokenAccount,
   ballot,
   voterTokenAccount: walletTokenAccount,
   tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID,
   associatedTokenProgram: anchor.utils.token.ASSOCIATED_PROGRAM_ID,
   systemProgram: anchor.web3.SystemProgram.programId,
   rent: anchor.web3.SYSVAR_RENT_PUBKEY,
 },
 signers: [],
})

Tham khảo

Về Sentre Protocol

Sentre Protocol là nền tảng mở All-in-One hoạt động trên mạng lưới Solana, cung cấp DApp Store và giao thức mở giúp tích lũy thanh khoản. Với Sentre:

  • Người dùng có thể cài đặt các DApp yêu thích của mình và trải nghiệm thế giới DeFi trên một nền tảng duy nhất;
  • Lập trình viên và đối tác có thể phát hành DApp thông qua Sen Store, tận dụng tài nguyên có sẵn và tự do đóng góp cho nền tảng.