惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Security Archives - TechRepublic
Simon Willison's Weblog
Simon Willison's Weblog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
O
OpenAI News
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
S
Secure Thoughts
MongoDB | Blog
MongoDB | Blog
V
V2EX - 技术
Last Week in AI
Last Week in AI
Attack and Defense Labs
Attack and Defense Labs
Application and Cybersecurity Blog
Application and Cybersecurity Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
PCI Perspectives
PCI Perspectives
I
InfoQ
WordPress大学
WordPress大学
L
LangChain Blog
腾讯CDC
T
Troy Hunt's Blog
Schneier on Security
Schneier on Security
博客园 - 三生石上(FineUI控件)
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
V
Visual Studio Blog
美团技术团队
N
News | PayPal Newsroom
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
M
MIT News - Artificial intelligence
宝玉的分享
宝玉的分享
博客园 - 聂微东
V
Vulnerabilities – Threatpost
Hugging Face - Blog
Hugging Face - Blog
Spread Privacy
Spread Privacy
博客园 - 司徒正美
Engineering at Meta
Engineering at Meta
小众软件
小众软件
N
News and Events Feed by Topic
有赞技术团队
有赞技术团队
E
Exploit-DB.com RSS Feed
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
W
WeLiveSecurity
L
LINUX DO - 最新话题
MyScale Blog
MyScale Blog
Microsoft Azure Blog
Microsoft Azure Blog
F
Fortinet All Blogs
Apple Machine Learning Research
Apple Machine Learning Research
Latest news
Latest news
S
Securelist
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
爱范儿
爱范儿
SecWiki News
SecWiki News
GbyAI
GbyAI

掘金

C# 类型系统 海量人群包存储优化:基于 RoaringBitmap 交换格式与 Redis 分片 Bitmap 的实践 �������� JavaScript �հ�����ԭ����ʵս 驯服 React 里的 DOM 事件:useEventListener、useEventEmitter、useKeyModifier、useTextSelect 鸿蒙项目首页启动链路与 ArkUI 架构学习总结 juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn xlt-token 1.1:给 NestJS 补上 Sa-Token 式鉴权能力 juejin.cn juejin.cn juejin.cn Transformer 原论文怎么训出来的:8 张 P100、12 小时、warmup 4000 步 Hermes 升级后,我的 Telegram 附件突然发不出来了 Transformer 中的前馈网络:那个看似平平无奇的两层 MLP,其实是「记忆」所在 juejin.cn 【Agentic RL / 强化学习 / OPD】OpenClaw-RL 源码阅读笔记 --- (1)---基础 juejin.cn juejin.cn juejin.cn 我让 AI 加了一个开关,结果代码走了原本不该走的分支 Manim物理模拟:别自己写欧拉了! 我也该升级了,陪伴了我7年的博客 juejin.cn juejin.cn MCP 高德地图实战:当 AI 学会使用工具,一个协议如何重塑大模型的行动边界 用魔法打败魔法:我让AI替我去面试前端岗,AI面试官给我打了92分,还发了offer juejin.cn juejin.cn juejin.cn juejin.cn Android Input Spy Window Claude Code CLI 命令大全:60 个原生命令一次讲清 juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn 关于一个新手小白靠claude帮助下的全栈留言板项目开发 juejin.cn juejin.cn juejin.cn 从本地开发到生产部署:用 Docker Compose 跑通 NestJS、MySQL 与 Milvus AI应用开发七:可以替代 RAG 的技术 juejin.cn 小书匠:一款本地优先、去中心化的全能笔记软件 juejin.cn juejin.cn juejin.cn Shadow实战接入与生产落地:从零搭建到稳定运行 Shadow Transform:编译期的魔法——字节码替换实战 juejin.cn juejin.cn Hermes Agent:一个真正“会成长”的开源 AI Agent,正在改变 AI 自动化玩法 juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn CryptoJS:数据安全的JavaScript加密利器 juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn ArkClaw AI 盯盘管家 —— 从手动口令到自动推送,4 套预置定时任务模版一键启用 juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn “杀!杀!杀!”、“我最讨厌事后道歉”——骂“杀哥”之前,谁还没当过情绪崩溃的人 juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn Crawlee StagehandCrawler:自然语言点 Load More 的工程化爬虫 juejin.cn
从“连接不上”到“交易成功”:我用 @solana/web3.js 在 React 中搞定 Solana 钱包交互的全过程
竹林818 · 2026-05-26 · via 掘金

背景:一个看似简单的 NFT 铸造需求

两个月前,我接手了一个 Solana 上的 NFT 铸造项目。产品需求很明确:用户在浏览器中连接 Phantom 钱包,输入数量,点击“铸造”按钮,就能 Mint 一个 NFT。听起来和以太坊上的流程差不多,我当时想:“不就是调个钱包 API 嘛,半天搞定。”

结果,我花了整整两天时间,才让第一笔交易成功上链。问题出在哪里?不是我不懂区块链,而是我对 Solana 的“前端开发范式”完全没概念。在以太坊上,我用 ethers.js 或 wagmi 习惯了,但 Solana 的账户模型、交易结构、钱包交互方式都和以太坊完全不同。更坑的是,@solana/web3.js 的版本迭代很快,网上很多教程用的是 v1.x,而现在已经是 v2.x 了,API 变化很大。

我当时的困境是:钱包连接上了,也能获取到地址,但一发送交易就报错,不是“invalid account”就是“failed to serialize”。这篇文章,就是我把整个排查和实现过程完整记录下来,希望帮到同样在 Solana 前端路上踩坑的你。

问题分析:为什么官方示例跑不通?

我的第一步,当然是去翻 @solana/web3.js 的官方文档。文档给了一个简单的“发送 SOL”示例,大概长这样:

import { Connection, Transaction, SystemProgram, LAMPORTS_PER_SOL } from '@solana/web3.js';

const connection = new Connection('https://api.mainnet-beta.solana.com');
const transaction = new Transaction().add(
  SystemProgram.transfer({
    fromPubkey: sender.publicKey,
    toPubkey: receiver.publicKey,
    lamports: LAMPORTS_PER_SOL,
  })
);
const signature = await sendAndConfirmTransaction(connection, transaction, [sender]);

我当时心想:“这不挺简单吗?直接抄过来改改就行。” 但是,当我把这段代码放到 React 项目里时,问题接踵而至。

第一个坑:sendAndConfirmTransaction 这个函数在 v2.x 里已经废弃了。 是的,我用的 @solana/web3.js 版本是 2.0.0,这个 API 被移除了。取而代之的是 sendTransaction 和独立的 confirmTransaction 方法。但文档的示例还是旧的,我一开始没注意版本号,直接复制粘贴,然后报错说找不到这个函数。

第二个坑:钱包的 signTransaction 方法返回的是 Uint8Array,不是 Transaction 对象。 我用 Phantom 钱包的 window.solana.signTransaction(transaction) 时,发现它返回的是一个序列化后的字节数组,而 sendTransaction 需要的是 Transaction 对象。这就尴尬了——我到底该用哪个?

第三个坑:确认交易时,状态码的含义。 交易发送后,我需要等待确认。但 confirmTransaction 返回的 SignatureResult 里有个 err 字段,如果交易失败,err 是一个对象,不是简单的字符串。我第一次没做错误判断,直接用了 result.value 的布尔值,结果交易失败了还显示“成功”。

这三个坑让我意识到,官方文档只是告诉你“能做什么”,但没告诉你“在真实项目中怎么做”。我需要一个完整的、能直接跑通的流程。

核心实现:从零搭建 Solana 前端交互

第一步:选对版本,装对依赖

我先确认了项目环境:React 18 + TypeScript + @solana/web3.js v2.0.0。安装命令很简单:

npm install @solana/web3.js@2.0.0

但这里有个细节:如果你需要钱包适配(比如 Phantom),还要装 @solana/wallet-adapter-wallets@solana/wallet-adapter-react。我当时只装了核心库,结果用 window.solana 直接操作时,发现它和 React 的状态管理不太兼容。后来我改用 @solana/wallet-adapter-base 来统一处理钱包连接,省心很多。

不过,为了更贴近“纯 @solana/web3.js”的使用场景,我决定在本文中只依赖核心库和 Phantom 的官方 API,不引入额外的钱包适配器。这样你就能更清楚地看到每一步在做什么。

注意: Solana 的 RPC 节点需要选择。我用的是 Helius 的公共节点(https://api.devnet.solana.com),因为开发网免费且稳定。主网的话,建议用 QuickNode 或自己搭建的节点,避免被限流。

第二步:连接钱包并获取账户信息

核心思路:使用 window.solana 对象(Phantom 注入的)来请求连接,然后获取用户的公钥。这里有个坑:window.solana 在页面加载时可能还没准备好,需要检查是否存在。

我写了一个自定义 Hook,专门处理钱包连接:

// hooks/useWallet.ts
import { useEffect, useState } from 'react';
import { PublicKey, Connection } from '@solana/web3.js';

interface WalletState {
  publicKey: PublicKey | null;
  connected: boolean;
  connect: () => Promise<void>;
  disconnect: () => void;
}

export function useWallet(): WalletState {
  const [publicKey, setPublicKey] = useState<PublicKey | null>(null);
  const [connected, setConnected] = useState(false);

  const connect = async () => {
    try {
      // 检查 Phantom 是否安装
      if (!window.solana || !window.solana.isPhantom) {
        alert('请安装 Phantom 钱包!');
        return;
      }
      // 请求连接
      const response = await window.solana.connect();
      // response.publicKey 是一个 PublicKey 对象
      setPublicKey(response.publicKey);
      setConnected(true);
    } catch (error) {
      console.error('连接失败:', error);
    }
  };

  const disconnect = () => {
    window.solana.disconnect();
    setPublicKey(null);
    setConnected(false);
  };

  // 监听账户变化
  useEffect(() => {
    if (window.solana?.on) {
      window.solana.on('accountChanged', (publicKey: PublicKey | null) => {
        if (publicKey) {
          setPublicKey(publicKey);
        } else {
          setPublicKey(null);
          setConnected(false);
        }
      });
    }
    return () => {
      window.solana?.removeAllListeners('accountChanged');
    };
  }, []);

  return { publicKey, connected, connect, disconnect };
}

这里有个坑: 我一开始以为 window.solana.connect() 返回的是 { publicKey: string },但实际上它返回的是 { publicKey: PublicKey },而且 PublicKey 是一个类,不是普通的字符串。如果你直接把它存成字符串,后面做交易时会报类型错误。

第三步:构建并发送交易

这是最核心的部分。我需要在用户点击“铸造”按钮时,构建一个包含 NFT 铸造指令的交易,然后让用户用钱包签名,最后发送到链上。

Solana 的交易结构是:一个 Transaction 对象包含一个或多个 Instruction,每个 Instruction 指定程序 ID、账户列表和数据。对于 NFT 铸造,我需要调用 Metaplex 的 Candy Machine 程序,但为了简化演示,这里我用一个简单的 SOL 转账为例,流程是一样的。

// utils/sendTransaction.ts
import { Connection, Transaction, SystemProgram, LAMPORTS_PER_SOL, sendAndConfirmTransaction } from '@solana/web3.js';

export async function sendSol(
  connection: Connection,
  fromPubkey: PublicKey,
  toPubkey: PublicKey,
  amount: number
): Promise<string> {
  // 构建交易
  const transaction = new Transaction().add(
    SystemProgram.transfer({
      fromPubkey: fromPubkey,
      toPubkey: toPubkey,
      lamports: amount * LAMPORTS_PER_SOL, // 注意单位转换
    })
  );

  // 设置交易参数:最新区块哈希和费用
  const { blockhash } = await connection.getLatestBlockhash();
  transaction.recentBlockhash = blockhash;
  transaction.feePayer = fromPubkey;

  // 用钱包签名
  const signedTransaction = await window.solana.signTransaction(transaction);

  // 发送交易
  const signature = await connection.sendRawTransaction(signedTransaction.serialize());

  // 等待确认
  const confirmation = await connection.confirmTransaction(signature, 'confirmed');

  if (confirmation.value.err) {
    throw new Error(`交易失败: ${JSON.stringify(confirmation.value.err)}`);
  }

  return signature;
}

注意这个细节: 我用了 sendRawTransaction 而不是 sendTransaction。这是因为 window.solana.signTransaction 返回的是签名后的 Transaction 对象,但 sendTransaction 方法在 v2.x 中接收的是序列化后的字节数组。所以我们需要调用 signedTransaction.serialize() 来获取 Uint8Array,然后用 sendRawTransaction 发送。

这里有个坑: 我一开始用 sendTransaction(signedTransaction),结果报错说“Transaction object is not serializable”。后来查文档才发现,sendTransaction 在 v2.x 中已经被重构了,它期望的参数是 Uint8ArrayBuffer,而不是 Transaction 对象。所以正确做法是 sendRawTransaction

第四步:处理确认状态和错误

确认交易时,confirmTransaction 返回的是一个 Promise<SignatureResult>,其中 SignatureResult 的结构是:

{
  context: { slot: number },
  value: { err: object | null }
}

如果 err 不为 null,说明交易失败。但 err 可能是一个对象(如 { InstructionError: [0, "Custom"] }),也可能是一个字符串。所以不能简单用 if (err) 来判断,需要解析。

我写了一个辅助函数:

function parseTransactionError(err: any): string {
  if (err === null) return '成功';
  if (typeof err === 'string') return err;
  if (err.InstructionError) {
    const [index, errorCode] = err.InstructionError;
    return `指令 ${index} 错误: ${errorCode}`;
  }
  return JSON.stringify(err);
}

这样在 UI 上就能显示具体的错误信息,而不是一个冰冷的“交易失败”。

完整代码:一个可运行的 React 组件

下面是一个完整的 React 组件,集成了钱包连接和 SOL 转账功能。你可以直接复制到一个新的 React 项目中运行,只需要确保安装了 @solana/web3.js@2.0.0

// App.tsx
import React, { useState } from 'react';
import { Connection, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js';
import { useWallet } from './hooks/useWallet';
import { sendSol } from './utils/sendTransaction';

// 使用 Devnet 节点
const connection = new Connection('https://api.devnet.solana.com', 'confirmed');

function App() {
  const { publicKey, connected, connect, disconnect } = useWallet();
  const [recipient, setRecipient] = useState('');
  const [amount, setAmount] = useState(0.01);
  const [status, setStatus] = useState('');

  const handleSend = async () => {
    if (!publicKey) return;
    setStatus('正在构建交易...');
    try {
      const toPubkey = new PublicKey(recipient);
      const signature = await sendSol(connection, publicKey, toPubkey, amount);
      setStatus(`交易成功!签名: ${signature}`);
    } catch (error) {
      setStatus(`交易失败: ${error.message}`);
    }
  };

  return (
    <div style={{ padding: '20px' }}>
      <h1>Solana 钱包交互示例</h1>
      {!connected ? (
        <button onClick={connect}>连接 Phantom 钱包</button>
      ) : (
        <div>
          <p>已连接: {publicKey.toBase58()}</p>
          <button onClick={disconnect}>断开连接</button>
          <div style={{ marginTop: '20px' }}>
            <input
              type="text"
              placeholder="接收地址 (Base58)"
              value={recipient}
              onChange={(e) => setRecipient(e.target.value)}
            />
            <input
              type="number"
              step="0.001"
              value={amount}
              onChange={(e) => setAmount(parseFloat(e.target.value))}
            />
            <button onClick={handleSend}>发送 SOL</button>
          </div>
          <p>{status}</p>
        </div>
      )}
    </div>
  );
}

export default App;

注意: 这个组件依赖上面写的 useWallet Hook 和 sendSol 函数。你需要创建对应的文件并导出。另外,window.solana 的类型定义需要安装 @solana/wallet-adapter-base 或手动声明:

// global.d.ts
interface Window {
  solana: any;
}

踩坑记录:我实际遇到的 4 个报错

  1. TypeError: Cannot read properties of undefined (reading 'signTransaction')

    • 原因:window.solana 未定义,因为 Phantom 没安装或未注入。
    • 解决:在 connect() 中加入 if (!window.solana) 的检查,并提示用户安装。
  2. Error: Transaction simulation failed: Invalid account data

    • 原因:我试图发送一个 NFT 铸造交易,但账户的 owner 不是预期的程序。
    • 解决:检查了 fromPubkey 是否正确,以及是否调用了正确的程序 ID。后来发现是账户数据格式问题,需要先获取账户的 accountInfo
  3. Error: 410 Gone: This RPC node is not available

    • 原因:使用了公共 RPC 节点,但请求频率过高被限流。
    • 解决:切换到开发网节点 https://api.devnet.solana.com,并在本地缓存区块哈希,减少不必要的 RPC 调用。
  4. Error: Transaction has already been processed

    • 原因:重复发送了同一笔交易(比如用户双击了提交按钮)。
    • 解决:在发送交易前禁用按钮,并使用 useRef 存储当前交易的 blockhash,避免重复。

小结

这次经历让我深刻体会到,Solana 和以太坊的前端开发思路完全不同。核心收获是:一定要理解 Solana 的交易模型——Transaction 是容器,Instruction 是操作,签名后序列化再发送。 同时,不要完全信任官方文档的示例,要结合版本号确认 API 是否已废弃。如果你想继续深挖,可以研究一下 @solana/web3.jsTransactionMessage API(v2.x 新引入的),它提供了一种更声明式的交易构建方式。