Solana Dapp 前端开发攻略

1,559 阅读3分钟

连接钱包

创建Context组件

import React, { FC, ReactNode, useMemo } from 'react'
import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react'
import { WalletModalProvider } from '@solana/wallet-adapter-react-ui'
import * as walletAdapterWallets from '@solana/wallet-adapter-wallets'
import { clusterApiUrl } from '@solana/web3.js'
require('@solana/wallet-adapter-react-ui/styles.css')

const SolanaContextProvider: FC<{ children: ReactNode }> = ({ children }) => {
  const endpoint = clusterApiUrl(SOLANA_CLUSTER)
  const wallets = useMemo(() => {
    return [new walletAdapterWallets.PhantomWalletAdapter()]
  }, [])

  return (
    <ConnectionProvider endpoint={endpoint}>
      <WalletProvider wallets={wallets} autoConnect={true}>
        <WalletModalProvider>{children}</WalletModalProvider>
      </WalletProvider>
    </ConnectionProvider>
  )
}

export default SolanaContextProvider

使用上述Context包裹组件树。

显示钱包弹窗

官方的wallet-adapter-react-ui库中已经封装了连接钱包的逻辑,因此实现钱包连接功能时,我们只需要显示钱包弹窗即可。

import { useWalletModal } from '@solana/wallet-adapter-react-ui'

const ShowWalletModalButton = () => {
  const { setVisible } = useWalletModal()
  
  const showModal = () => {
    setVisible(true)
  }
  
  return <button onClick={showModal}>Connect</button>
}

Token相关

查询SOL余额

import { Connection, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js'

const getSolBalance = async (connection, address: string) => {
  const publicKey = new PublicKey(address)
  
  const response = await connection.getBalance(publicKey)

  return response / LAMPORTS_PER_SOL
}

查询非原生代币余额

单次查询

getTokenAccountBalance会返回一个 SPL 代币帐户的余额。

import { Connection } from '@solana/web3.js'

async function getSolanaTokenBalance(connection: Connection, tokenAccount: PublicKey) {
  const info = await connection.getTokenAccountBalance(tokenAccount)

  return info?.value
}

批量查询

export const getMultipleTokenAccountsBalance = async (addresses: string[], mint: string) => {
  const createGetTokenAccountBalanceParams = (address: string) => {
    return {
      jsonrpc: '2.0',
      id: 1,
      method: 'getTokenAccountBalance',
      params: [
        address,
        {
          commitment: 'confirmed',
        },
      ],
    }
  }

  const atas = await Promise.allSettled(
    addresses.map((addr) => getAssociatedTokenAddress(new PublicKey(mint), new PublicKey(addr))),
  ).then((results) =>
    results.map((result) => {
      if (result.status === 'fulfilled') {
        return result.value
      } else {
        return null
      }
    }),
  )

  const params = atas.map((ata) => createGetTokenAccountBalanceParams(ata.toString()))

  const response = await fetch(clusterApiUrl(SOLANA_CLUSTER), {
    method: 'post',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(params),
  }).then((res) => res.json())

  return response
}

对信息进行签名

import { useWallet } from '@solana/wallet-adapter-react'
import { bs58 } from '@coral-xyz/anchor/dist/cjs/utils/bytes'

export const SignMessage = () => {
  const wallet = useWallet()


  const signMsg =  async (message: string) => {
    if (!wallet) {
      return Promise.reject(new Error('No Solana wallet connected'))
    }

    const encodedMessage = new TextEncoder().encode(message)
    const signature = await wallet?.signMessage(encodedMessage)
    if (!signature) {
      return Promise.reject(new Error('Failed to sign message'))
    }

    const decodedSignature = bs58.encode(signature)

    return decodedSignature
  }

  return <button onClick={() => {signMsg('12345')}}>Sign Message</button>
}

调用合约

获取Anchor provider

import { AnchorProvider, Provider, getProvider, setProvider } from '@coral-xyz/anchor'
import { useAnchorWallet, useConnection } from '@solana/wallet-adapter-react'
import { useMemo } from 'react'

const useAnchorProvider = (withWallet = true) => {
  const { connection } = useConnection()
  const wallet = useAnchorWallet()

  return useMemo(() => {
    let provider: Provider

    try {
      if (!withWallet) {
        provider = getProvider()
      } else {
        provider = new AnchorProvider(connection, wallet, {})
        setProvider(provider)
      }
    } catch {
      provider = new AnchorProvider(connection, wallet, {})
      setProvider(provider)
    }

    return provider
  }, [connection, wallet, withWallet])
}

export default useAnchorProvider

初始时,getProvider()获取到的provider比new AnchorProvider()获取到的provider少了字段,所以默认使用后者获取provider。

配置IDL

IDL(Interface Definition Language)定义了一个program的公共接口。

使用形如以下代码的IDL才能在调用program时获得类型提示。

export type XxxIdl = {}

export const XXX_IDL: XxxIdl = {}

获取Program实例

import { Program } from '@coral-xyz/anchor'
import useAnchorProvider from './useAnchorProvider'

const useXxxProgram = () => {
  const provider = useAnchorProvider()

  const program = new Program(XXX_IDL, XxxProgramId, provider)

  return program
}

export default useXxxProgram

查找account

Solana的链上数据存储在各种account中,调用Solana合约时常常需要传入相关的account。

查找PDA

PDA: Program Derived Addresse

import { PublicKey } from '@solana/web3.js'

const [programAddress] = PublicKey.findProgramAddressSync([...seeds], ProgramId)

查找ATA

ATA: Associated Token Accounts,存储某个帐户与某个代币的关联数据的帐户。

import { getAssociatedTokenAddress } from '@solana/spl-token'

const associatedTokenAddress = await getAssociatedTokenAddress(mint, account)

获取合约中的数据

假设有一个计数器合约,这个合约有一个字段存储了每个调用者的计数的数据,获取计数数据的逻辑如下。

const counter = await program.account.counter.fetch(address);

其中counter表示要获取的数据的字段,传入fetch()的地址是存储这组数据的PDA,需要通过findProgramAddressSync()计算得到。

调用合约方法

const tx = await program.methods.xxx(args).accounts(accounts).rpc()