Published on
Solana

ทำ Frontend เชื่อมต่อ Wallet ด้วย Nextjs + Solana Wallet Adapter

nextjs-connect-solana-wallet
Discord

วันนี้ลองทำ Frontend เพื่อใส่ปุ่ม Connect Wallet ให้กับเว็บไซต์ ซึ่งผมจะใช้ตัว Solana Wallet Adapter ข้อดีคือ มัน handle หลายๆ Wallet ให้เราเลย ไม่ว่าจะเป็น Phantom, Sollet, Ledger, Solflare, Math Wallet เป็นต้น

ซึ่งถ้าเราไม่ใช้ เราก็ต้องไป handle เอง เช่น ถ้าเราติดตั้ง Phantom แล้ว เราก็เข้าถึง global object ได้ เหมือนกับ Metamask ที่ inject ethereum เป็น global object ใน window เช่นกัน

window.phantom;

// {solana: n}

ตัวอย่าง Solana Wallet Adapter

  • จะเห็นว่า มี Popup และรายชื่อ Wallet ให้เราเลือก Connect ได้เลย
  • จัดการ Auto connect ให้เรา
  • มี UI รองรับทั้ง Material UI และ Ant Design
  • รองรับ Frontend หลักๆ ทั้ง React, Vue และ Angular รวมถึง Svelte

สำหรับบทความนี้ ผมเลือกใช้

  • Phantom - เป็นกระเป๋า Wallet บน Browser Extension.
  • React + Nextjs - สำหรับ Stack ของ frontend ครับ

สร้างโปรเจ็ค Next.js

ทำการสร้างโปรเจ็คขึ้นมาโดยใช้ create-next-app ครับ และผมเลือกเป็น TypeScript (หากไม่อยากเขียนด้วย TypeScript ก็เอา option ออกได้ครับ) หรืออ่านเพิ่มเติม Next.js - Getting Started

yarn create next-app --typescript

# หรือถ้าใช้ npx
npx create-next-app@latest --typescript

ผมตั้งชื่อว่า hello-sonala-wallet-adapter (แล้วแต่เลยว่าอยากได้ชื่ออะไร)

What is your project named? … hello-solana-wallet-adapter

เมื่อได้โปรเจ็คแล้ว ลองเปิดโปรเจ็ค แล้วลอง start development mode ดู

yarn dev

ต้องได้หน้าเว็บ starter ของ Next.js http://localhost:3000

Nextjs Starter

เพิ่มปุ่ม Connect Wallet

ต่อมาติดตั้ง packages ของ Solana Wallet Adapter ที่จะใช้กัน

yarn add @solana/web3.js @solana/wallet-adapter-base @solana/wallet-adapter-wallets @solana/wallet-adapter-react
  • @solana/web3.js - เป็นตัว Solana Web3 (ถ้าในบทความนี้หลักๆ ก็แค่ดึง url ซึ่งถ้า demo เรา hardcode ก็ได้)
  • @solana/wallet-adapter-base - ตัวนี้เป็น base ที่เป็น Adapter Inferface และ Utilities ต่างๆ
  • @solana/wallet-adapter-react - เป็นตัวจัดการ Context และ React Hooks
  • @solana/wallet-adapter-react-ui - เป็นตัว React UI เช่น Modal, MultiButton
  • @solana/wallet-adapter-wallets - เป็นตัวจัดการ Wallets ทุกๆตัวเลย (มี Tree shaking ตัวไหนไม่ได้ใช้ ก็ไม่ถูก include เข้ามาเวลา build)

ซึ่งถ้าใครไม่อยากใช้ adapter-wallets ที่รวมทุก Wallets ก็สามารถใช้แยกได้เช่น @solana/wallet-adapter-phantom หรือ @solana/wallet-adapter-sollet

จากนั้นที่ไฟล์​ pages/_app.tsx เพิ่ม Provider ลงไป

pages/_app.tsx
import { useMemo } from "react";
import type { AppProps } from "next/app";

import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
import {
  ConnectionProvider,
  WalletProvider,
} from "@solana/wallet-adapter-react";
import {
  PhantomWalletAdapter,
  MathWalletAdapter,
  SolflareWalletAdapter,
} from "@solana/wallet-adapter-wallets";
import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
import { clusterApiUrl } from "@solana/web3.js";

import "../styles/globals.css";

function MyApp({ Component, pageProps }: AppProps) {
  const network = WalletAdapterNetwork.Devnet;
  const endpoint = useMemo(() => clusterApiUrl(network), [network]);

  const wallets = useMemo(() => {
    return [
      new PhantomWalletAdapter(),
      new SolflareWalletAdapter({ network }),
      new MathWalletAdapter(),
    ];
  }, [network]);

  return (
    <ConnectionProvider endpoint={endpoint}>
      <WalletProvider wallets={wallets}>
        <WalletModalProvider>
          <Component {...pageProps} />
        </WalletModalProvider>
      </WalletProvider>
    </ConnectionProvider>
  );
}

export default MyApp;

อธิบายเพิ่มเติมนิดหน่อย คือ

const network = WalletAdapterNetwork.Devnet;
const endpoint = useMemo(() => clusterApiUrl(network), [network]);

เป็นการระบุ ให้ใช้ Network เป็น Devnet

const wallets = useMemo(() => {
  return [
    new PhantomWalletAdapter(),
    new SolflareWalletAdapter({ network }),
    new MathWalletAdapter()
  ];
}, [network]);

สร้างตัว Wallets ขึ้นมา เพื่อเป็นเมนูให้ User เลือกครับ ถ้าอยากมีตัวเลือกหลาย Wallet ก็ import เพิ่ม

return (
  <ConnectionProvider endpoint={endpoint}>
    <WalletProvider wallets={wallets}>
      <WalletModalProvider>
        <Component {...pageProps} />
      </WalletModalProvider>
    </WalletProvider>
  </ConnectionProvider>
);
  • ConnectionProvider - กำหนด endpoint จาก url ที่เรา set ค่าไว้
  • WalletProvder - กำหนด ตัวเลือก wallets ที่เราต้องการ
  • WalletModalProvider - เป็นตัว Modal UI ของ React

ทีนี้ก็เพิ่มปุ่ม Connect Wallet ที่หน้า pages/index.tsx ครับ โดยที่ผมไม่ได้เปลี่ยน content ที่ nextjs generate มาให้นะครับ แต่เอาปุ่มไปแทนที่ส่วนนี้

<p className={styles.description}>
  Get started by editing <code className={styles.code}>pages/index.tsx</code>
</p>

ที่เหลือเหมือนเดิม เปลี่ยนเป็น

pages/index.tsx
import {
  WalletDisconnectButton,
  WalletMultiButton,
} from "@solana/wallet-adapter-react-ui";

const Home: NextPage = () => {
  return (
    // โค๊ดอื่นๆ ก่อนหน้า
    <div>
      <WalletMultiButton />
      <WalletDisconnectButton />
    </div>
    // ...
    // โค๊ดอื่นๆ ไม่ได้แก้
  )
}
Without css

แต่ว่าหน้าเว็บ ทำไมปุ่มเป็นแบบนี้ และก็กดอะไรไม่มีอะไรเกิดขึ้นเลย? จริงๆ มันได้ครับ และการทำงานปกติ เพียงแต่ว่าไม่มี css เท่านั้น

แก้ไขไฟล์ pages/_app.tsx โดยเพิ่ม css ของ wallet-adapter-react-ui ลงไปครับ

pages/_app.tsx
import"@solana/wallet-adapter-react-ui/styles.css";

เพียงแค่นี้ก็ได้แล้ว

Wallet Adapter

แต่มันไม่ค่อยสวย ผมขอเพิ่ม css ให้มันนิดหน่อยนะครับ ให้มี spacing ของปุ่ม Connect และ Disconnect

Home.module.css
.buttonContainer {
  display: flex;
  margin-top: 1em;
  margin-bottom: 1em;
}

/* WalletMultiButton */
.buttonContainer > * {
  margin-left: 0.5rem;
  margin-right: 0.5rem;
}

และเพิ่มคลาสไป ในไฟล์ pages/index.tsx

<div className={styles.buttonContainer}>
  <WalletMultiButton />
  <WalletDisconnectButton />
</div>
Popup

เป็นอันเรียบร้อย ซึ่ง Flow การทำงานของมันคือ Select Wallet ก่อน จากนั้นก็ Connect ครับ (สังเกตจาก icon wallet ที่เราเลือก)

Wallet Final

เพื่อนๆ ลองนำไปใช้ ไปลองเล่นกันดูนะครับ ลองสลับ Network ลอง handle network ลองส่ง Transaction ดู หรือลองดูตัวอย่างอื่นๆ เพิ่มเติมได้ครับ ส่วนใหญ่จะเป็น Community ช่วยๆกันทำขึ้น เช่น Vue, Svelte

Happy Coding ❤️

Authors
Discord