Solana/更多开发者工具/web3.js 快速上手

我们开发了一个简单的 dapp, 演示如何使用 solana/web3.js 为我们的 on-chain program 提供前端交互功能. 你可以参考并扩展到自己的项目中. 本文使用的例子是使用 react + vite 的前端, 连接 phantom 钱包, 并通过上节课介绍的基于 pinocchio 的数据存储程序来读写 pda 数据.

项目地址仍然位于 https://github.com/mohanson/pxsol-ss-pinocchio.

为了方便读者们学习, 我已经将该项目部署到 solana 主网上, 程序地址是 9RctzLPHP58wrnoGCbb5FpFKbmQb6f53i5PsebQZSaQL, 你可以直接使用该程序进行测试.

在部署程序时作者因为失误发生了数次错误, 总共花费了大约 1.5 sol 的手续费才完成了部署, 也就是大约 300 美元, 请大家未来在操作主网时务必小心谨慎, 避免不必要的损失.

作者的碎碎念

我本身并非前端工程师, 对前端的了解仅仅限于能做出一个简单的 demo, 但我觉得 web3.js 的学习曲线并不陡峭, 只要你有一定的前端基础, 并且了解 solana 的基本概念, 就能很快上手.

另外现在的 ai 非常强大, 我在写本文的前端代码时大量使用了 github copilot 和 chatgpt, 这些工具能极大地提高开发效率, 但也会带来一些问题, 例如生成的代码可能并不完全正确, 需要你有一定的判断能力来辨别和修正错误. 所以我建议大家在学习和使用这些工具时, 不要完全依赖它们, 而是要结合自己的理解和经验.

在此处我只介绍最核心的代码片段, 以及一些需要注意的点. 如果你想了解完整的代码, 请参考项目源码.

建立连接

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

export const PROGRAM_ID = new PublicKey('9RctzLPHP58wrnoGCbb5FpFKbmQb6f53i5PsebQZSaQL')
export const RPC_ENDPOINT = import.meta.env.VITE_SOLANA_RPC
  || 'https://api.mainnet-beta.solana.com'

export const connection = new Connection(RPC_ENDPOINT, 'confirmed')

连接浏览器里的 phantom 钱包

import type { Transaction } from '@solana/web3.js'

type PhantomProvider = {
  isPhantom?: boolean
  publicKey?: PublicKey
  connect(opts?: { onlyIfTrusted?: boolean }): Promise<{ publicKey: PublicKey }>
  disconnect(): Promise<void>
  signTransaction(tx: Transaction): Promise<Transaction>
}

declare global { interface Window { solana?: PhantomProvider } }

export async function connectPhantom(): Promise<PublicKey> {
  if (!window.solana?.isPhantom) throw new Error('Phantom not found')
  return (await window.solana.connect()).publicKey
}

派生程序扩展账户

前端需保持和链上程序相同的派生种子:

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

export async function deriveDataPda(user: PublicKey): Promise<[PublicKey, number]> {
  return PublicKey.findProgramAddress([user.toBuffer()], PROGRAM_ID)
}

读取账户数据

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

export async function fetchUserData(conn: Connection, user: PublicKey): Promise<Uint8Array | null> {
  const [pda] = await deriveDataPda(user)
  const info = await conn.getAccountInfo(pda, { commitment: 'confirmed' })
  return info ? info.data : null
}

export function decodeUtf8(data: Uint8Array | null): string {
  return data ? new TextDecoder().decode(data) : ''
}

构造写入指令

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

export async function buildWriteIx(user: PublicKey, payload: Uint8Array): Promise<TransactionInstruction> {
  const [pda] = await deriveDataPda(user)
  return new TransactionInstruction({
    programId: PROGRAM_ID,
    keys: [
      { pubkey: user, isSigner: true, isWritable: true },
      { pubkey: pda, isSigner: false, isWritable: true },
      { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
    ],
    data: payload,
  })
}

备注: 若你项目里需要 Buffer, 可使用 data: Buffer.from(payload), 并 import { Buffer } from 'buffer'.

发送交易并确认

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

type PhantomProvider = { signTransaction(tx: Transaction): Promise<Transaction> }

export async function sendAndConfirm(
  conn: Connection,
  user: PublicKey,
  ix: TransactionInstruction,
  wallet: PhantomProvider,
) {
  const tx = new Transaction().add(ix)
  tx.feePayer = user
  const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash('finalized')
  tx.recentBlockhash = blockhash

  const signed = await wallet.signTransaction(tx)
  const sig = await conn.sendRawTransaction(signed.serialize(), { preflightCommitment: 'finalized' })
  await conn.confirmTransaction({ signature: sig, blockhash, lastValidBlockHeight }, 'finalized')
  return sig
}

运行

$ npm run dev
# Open http://localhost:5173
# Connect Phantom wallet and save/load data.

首先点击 connect 连接钱包, 然后在输入框中输入任意字符串, 点击 save 保存到链上, 再刷新页面, 即可读取刚才保存的数据.

img