DApp 全栈开发

# DApp 全栈开发

DApp 的前端负责交互,钱包负责签名,合约负责关键规则,后端负责验证、索引和业务服务。采用区块链,不意味着数据库、鉴权和运维不再需要。

# 各组件如何配合

组件 职责 不应该做什么
wagmi 在 React 中管理钱包、链与请求状态 把钱包已连接当成服务端已认证
viem 编码 ABI、读合约、模拟、提交交易和解码日志 把模拟成功当成正式交易保证
RPC 向节点查询状态、广播交易 成为未经校验的业务成功来源
API 与数据库 会话、订单、索引投影和查询 只凭前端通知认定充值成功
索引 Worker 补扫区块、处理事件、恢复断点 把 WebSocket 推送当成永不丢失的队列

# 常用技术栈与选型

先分清三层:链交互库负责读链和调用合约,React 集成库负责连接状态和 Hooks,钱包 UI 库负责按钮和弹窗。它们有的是替代关系,有的是组合关系,不需要全部安装。

这三层主要解释前端链交互;完整开发还会涉及身份与嵌入式钱包服务、合约开发框架和本地测试链。Solana 与 EVM 的账户和交易模型不同,也需要分别选择客户端库。

层次 常见工具 具体负责什么 怎么搭配
链交互 ethers.js (opens new window)、viem (opens new window) 查余额、读写合约、请求签名、查询交易与事件 通常选一个作为主要调用库,也能在 Node.js 后端或脚本中使用
React 集成 wagmi (opens new window)、web3-react (opens new window) 接入钱包,把账户、网络和连接状态同步给 React 组件 wagmi 基于 viem,并结合 TanStack Query 管理请求;web3-react 主要负责连接器与连接状态,不是完整的合约请求缓存方案
钱包 UI RainbowKit (opens new window)、ConnectKit (opens new window) 连接按钮、钱包选择弹窗、账户展示与网络切换 都可基于 wagmi 使用,通常选一个 UI 方案,不替代底层链交互库
身份与钱包服务 Privy (opens new window) 登录、外部钱包连接、嵌入式钱包 可搭配链交互库;产品能力不等于项目已经全部启用
Solana 链交互 Solana Kit(@solana/kit) (opens new window) Solana RPC、账户读取、交易构造与签名 面向 Solana,不是 wagmi 或 EVM ABI 调用的直接替换件
合约开发 Truffle (opens new window) 合约编译、测试和部署脚本 已归档;维护历史项目时常与 Ganache 配合,新项目评估 Foundry 或 Hardhat
本地测试链 Ganache (opens new window) 提供本地 EVM、测试账户和 RPC 已归档;职责接近 Anvil,可供客户端库和开发框架连接

# ethers.js:通过 Provider、Signer 和 Contract 操作链

ethers.js (opens new window) 是 JavaScript/TypeScript 的以太坊交互库,不依赖 React。它把常见操作组织成三个对象:

  • Provider:查询链上数据的入口,例如查余额、区块和回执。JsonRpcProvider 连接 RPC URL,BrowserProvider 对接浏览器钱包提供的接口。
  • Signer:代表能请求签名和发送交易的账户。浏览器中通常通过钱包确认操作,网站不需要取得用户私钥。
  • Contract:把合约地址和 ABI 组合成可调用对象。连接 Provider 时可以读合约,连接 Signer 时可以请求执行需要交易的操作。

例如,用 ethers v6 在 Vite 浏览器项目中读取当前钱包的 ETH 余额。先安装 ethers@6,把下面内容保存为 src/main.ts,由页面的 <script type="module" src="/src/main.ts"></script> 加载。该例只请求账户访问权限并读取余额,不签名、不转账。

import { BrowserProvider, formatEther, type Eip1193Provider } from 'ethers';

// 简化为单个注入钱包;多钱包选择由连接器或钱包 UI 层负责。
declare global {
  interface Window { ethereum?: Eip1193Provider }
}

const button = document.createElement('button');
const output = document.createElement('pre');
button.textContent = '连接钱包并查询 ETH 余额';
document.body.append(button, output);

button.addEventListener('click', async () => {
  button.disabled = true;
  try {
    if (!window.ethereum) throw new Error('请先安装兼容的浏览器钱包');
    const provider = new BrowserProvider(window.ethereum);
    // 只在用户点击后请求账户权限,钱包可能弹窗让用户确认。
    const signer = await provider.getSigner();
    const address = await signer.getAddress();
    const network = await provider.getNetwork();
    // getBalance 返回以 wei 为单位的 bigint;展示时再转换成 ETH 字符串。
    const balance = await provider.getBalance(address);
    output.textContent = `链 ID:${network.chainId}\n地址:${address}\nETH 余额:${formatEther(balance)}`;
  } catch (error) {
    // 用户拒绝连接或 RPC 查询失败时给出提示,不把外部内容作为 HTML 执行。
    output.textContent = error instanceof Error ? error.message : '查询失败';
  } finally {
    button.disabled = false;
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

本例面向 Ethereum 主网或以 ETH 为原生币的测试网络,且只查询点击时的账户;完整产品需要处理用户中途切换账户、切换链和旧请求返回的问题,见下文的交易跟踪部分。调用合约时,把可信配置中的合约地址和 ABI 交给 Contract,读取函数连接 Provider,写入函数连接 Signer,并在提交后查询回执。

与 viem 的区别:ethers 围绕 Provider、Signer、Contract 对象组织调用;viem 围绕 Public Client、Wallet Client 和 action 函数组织调用,能结合常量 ABI 推导函数名、参数与返回值类型。ethers 也支持 TypeScript,不能说它没有类型安全。新项目已经使用 wagmi 时,直接沿用 viem 通常更一致;已有 ethers 合约封装和测试时,也不必只为换技术名而重写。

优点:可以独立用于前端、后端和脚本,对象职责清楚,便于把合约当作对象调用。代价:React 的连接状态、请求缓存和钱包选择 UI 仍需其他层处理;v5 的 Web3Provider、BigNumber 等教程不能直接套到 v6,v6 常见写法是 BrowserProvider 和原生 bigint。

# web3-react:把钱包连接状态交给 React

web3-react (opens new window) 通过连接器(Connector)对接不同钱包,再通过 React Hooks 提供当前账户、链 ID 和连接状态。可以把连接器理解为 “不同钱包的接头” :组件使用统一的状态接口,不必自己为每种钱包维护一套事件监听。

以官方 v8 架构为例,基本接入顺序是:

  1. 安装 @web3-react/core 和需要的钱包连接器,例如 @web3-react/metamask,并满足该版本的配套依赖。
  2. 通过 initializeConnector 创建连接器及其 Hooks,在应用组件外保持实例稳定。
  3. 用 Web3ReactProvider 把连接器提供给组件;用户点击连接按钮后,调用连接器的 activate() 请求连接。
  4. 用 useWeb3React 或连接器自己的 Hooks 读取账户、链和连接状态,再按对应版本的 Provider 接口对接合约调用库。

它解决的是钱包连接,不会自动替你提供完整的钱包选择弹窗、业务登录、合约数据缓存或付款确认。账户连接成功后,后端登录仍按 钱包、签名与身份 中的签名验证流程处理。

优点:连接器模块化,适合需要自行设计连接交互、维护多种钱包适配的项目。代价:UI 和业务请求管理需要自行组合,v6 与 v8 的接入方式也不同,不能混用教程。

维护状态:官方仓库已于 2026 年 7 月 7 日归档为只读。它仍值得用于理解和维护历史项目,但新项目不应只凭过去的知名度直接选用,需要评估钱包兼容、依赖安全和后续维护成本。历史项目在 2025 年使用它,与今天的新项目选型是两个不同的问题。

# RainbowKit:直接提供钱包连接界面

RainbowKit (opens new window) 是 React 钱包连接 UI 库。用户点击它提供的 ConnectButton 后,可以选择钱包、连接账户、查看账户信息和切换网络;底层连接状态由 wagmi 管理,链交互能力由 viem 提供。

以 RainbowKit 2、wagmi 2、viem 2 和 TanStack Query 5 为例,在已有的 Vite React TypeScript 项目中接入。安装依赖:

# 这几项分别提供钱包界面、React 链交互、底层客户端和请求缓存。
npm install @rainbow-me/rainbowkit@2 wagmi@2 viem@2 @tanstack/react-query@5
1
2

先在官方安装文档链接的 WalletConnect 项目管理平台创建项目,取得自己的 projectId,并在 Vite 的 .env.local 中配置 VITE_WALLETCONNECT_PROJECT_ID。它是连接服务的项目标识,不是钱包私钥;生产环境还应按平台要求限制允许使用的域名。

用下面内容作为 src/App.tsx,由 Vite 模板已有的 src/main.tsx 挂载:

import '@rainbow-me/rainbowkit/styles.css';
import { ConnectButton, getDefaultConfig, RainbowKitProvider } from '@rainbow-me/rainbowkit';
import { WagmiProvider } from 'wagmi';
import { sepolia } from 'wagmi/chains';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

// 必须替换为自己申请的项目 ID;未配置时主动报错,避免连接时才发现问题。
const projectId = import.meta.env.VITE_WALLETCONNECT_PROJECT_ID;
if (!projectId) throw new Error('请配置 VITE_WALLETCONNECT_PROJECT_ID');

// 本例只启用 Sepolia 测试网,不引导用户进行主网资产操作。
const config = getDefaultConfig({
  appName: 'DApp 钱包连接示例',
  projectId,
  chains: [sepolia],
});
// Vite 客户端示例在组件外创建,避免每次渲染都重建缓存。
const queryClient = new QueryClient();

export default function App() {
  return (
    <WagmiProvider config={config}>
      {/* 为 wagmi 的数据查询提供缓存;不是后端业务登录会话。 */}
      <QueryClientProvider client={queryClient}>
        <RainbowKitProvider>
          {/* 内置按钮会根据连接状态展示钱包选择或账户界面。 */}
          <ConnectButton />
        </RainbowKitProvider>
      </QueryClientProvider>
    </WagmiProvider>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

这只是连接界面的入口,购买、授权和存款仍需使用 wagmi Hooks 或 viem 编写。默认公共 RPC 适合入门,生产需要配置可靠的 RPC;Next.js 等 SSR 项目还要按官方文档处理 ssr 配置和客户端状态恢复,不能把这个纯客户端入口原样当作 SSR 方案。

优点:省去从零实现钱包选择、连接状态和网络切换界面的工作。代价:需要配合 wagmi、viem 和 TanStack Query,并遵守相互兼容的版本组合;高度定制的交互仍需扩展,组件也不会自动完成后端登录或资金安全校验。

# ConnectKit:另一种钱包连接 UI 方案

ConnectKit (opens new window) 与 RainbowKit 处于同一层:提供钱包连接按钮、选择弹窗和账户交互,底层依赖 wagmi、viem 和 TanStack Query。它不是另一条链,也不是合约调用库。

怎么接入:

  1. 安装 connectkit 及兼容版本的 wagmi、viem、@tanstack/react-query,准备 WalletConnect projectId。
  2. 用 ConnectKit 的 getDefaultConfig 配置应用名、支持的链、RPC 和 walletConnectProjectId,再交给 wagmi 的 createConfig。这里与 RainbowKit 同名函数的参数及返回值不同,不能只换 import 就认为接入完成。
  3. 在应用入口依次包裹 WagmiProvider、QueryClientProvider、ConnectKitProvider,页面放置 ConnectKitButton。
  4. 连接后用 wagmi 的 useAccount 等 Hooks 读取账户与链,业务中的合约操作仍交给 wagmi 或 viem。Next.js App Router 的 Provider 入口需要声明为客户端组件。

优点:默认连接体验完整,支持主题和按钮定制,适合已有 wagmi 的 React 项目。代价:需要维护依赖版本和钱包兼容,特殊交互仍要定制;连接成功不会自动建立后端业务会话。

与 RainbowKit 怎么选:主要比较支持的钱包、交互样式、定制能力、移动端体验和维护情况,不是比较谁更会调用合约。已有一套满足需求的 UI 时,没有必要再叠加另一套。

# Privy:让登录和钱包接入成为一套产品体验

Privy (opens new window) 不只是连接按钮。它提供身份认证、外部钱包连接和嵌入式钱包等能力,支持 EVM 和 Solana 场景。嵌入式钱包是集成在应用体验里的钱包:用户可以先通过产品配置的邮箱、社交账号等方式登录,再使用钱包,不一定要先安装浏览器钱包插件。

怎么接入:

  1. 在 Privy Dashboard 创建应用,取得 App ID,配置允许访问的域名和需要的登录方式。
  2. 安装 @privy-io/react-auth,用 PrivyProvider 包裹应用,传入 App ID 和配置;是否创建嵌入式钱包需要明确配置,不是所有接入都自动创建。
  3. 等 usePrivy 的 ready 为真后再使用登录状态和操作;钱包列表还要等待 useWallets 自己的加载状态。根据当前激活的钱包连接 viem 或对应链的客户端,不能默认取列表第一个钱包。
  4. 后端独立验证身份:采用 Privy 登录方案时验证它签发的访问令牌,再检查业务权限;只用它连接钱包时,可以像现有项目一样,用钱包签名挑战建立自己的后端 Session。

例如面向普通用户的应用,希望 “邮箱登录后就能使用链上功能” ,可以评估 Privy 的嵌入式钱包;面向已有钱包的交易用户,只需要钱包选择弹窗时,RainbowKit 或 ConnectKit 可能已经足够。

优点:减少登录、钱包创建和连接流程的集成工作,降低用户入门门槛。代价:增加第三方服务依赖,需要评估定价、可用性、隐私、钱包恢复和导出方式,以及谁被允许签名;不能仅凭 “嵌入式钱包” 就判断其完整信任边界。

已有的 Web3 大学 Privy 项目用法 可以直接复习:该项目主要用它连接当前钱包,后端仍通过签名挑战和 better-auth 建立会话,不能把 Privy 的邮箱登录、嵌入式钱包等全部能力都说成项目实践。

# Solana Kit:面向 Solana 的 TypeScript 客户端工具包

这里的 SolanaKit 指 Solana Kit (opens new window),npm 包名是 @solana/kit。它提供 RPC 请求、账户数据处理、交易构造与签名等能力,定位更接近 Solana 生态的链交互工具包,而不是 RainbowKit 那样的钱包弹窗。

Solana 的链上程序通常称为 Program,交易由一条或多条指令(Instruction)组成;每条指令说明调用哪个程序、访问哪些账户以及传入什么数据。因此不能只把 Ethereum 的 RPC URL 换成 Solana URL,就继续使用原来的 readContract 和 EVM ABI。

怎么使用:

  1. 安装 @solana/kit,按官方客户端方案搭配 @solana/kit-plugin-rpc;需要签名时再接入签名器,使用某个链上程序时再接它的程序插件。
  2. 用 createClient() 创建客户端,通过 .use(solanaDevnetRpc()) 等插件配置连接 Solana Devnet(开发测试网),先做账户和链上数据查询。
  3. 需要写入时,用程序提供的指令构造方法生成操作,再由钱包或受保护的服务端签名器签名、发送并跟踪确认。浏览器连接钱包应按官方签名器指南适配 Wallet Standard,不把私钥写进前端。
  4. 例如创建代币,官方入门使用 @solana-program/token:创建 Mint 账户并初始化代币配置,再读取 Mint 数据核对结果。这里的 Mint 账户保存代币供应量、精度和权限等信息,不是用户钱包余额。

优点:模块化、类型化,围绕 Solana 的真实账户与交易模型设计,能按需组合功能。代价:需要理解 Solana 的账户、指令、手续费与交易有效期;与旧 @solana/web3.js 1.x 的 Connection、PublicKey 等接口不同,旧代码不能只替换包名。

它适合 Solana DApp、脚本和服务端链交互。当前 Aladdin 的 EVM 合约逻辑不能靠引入它直接迁移到 Solana,合约和资产交互设计都需要重新评估。

# Truffle:历史项目中的合约开发框架

Truffle (opens new window) 用来组织 Solidity 合约的编译、测试和部署,职责更接近 Hardhat 或 Foundry,不负责提供网页的钱包连接按钮。

读旧项目时,先看 contracts/ 中的合约、migrations/ 中的部署脚本、test/ 中的测试,以及 truffle-config.js 中的网络和编译器配置。这里的 migration 指部署脚本及执行记录,不是自动修改已部署合约,也不是数据库迁移。

历史项目的典型命令如下,前提是已经按项目锁文件安装 Truffle,并在配置中把 development 指向本地测试链:

# 编译 contracts 中的合约,生成 ABI、字节码等构建产物。
npx --no-install truffle compile

# 把 migrations 中的部署脚本执行到配置好的本地 development 网络。
npx --no-install truffle migrate --network development

# 在本地测试网络上运行项目已有的测试。
npx --no-install truffle test --network development
1
2
3
4
5
6
7
8

优点:编译、部署和测试有统一项目约定,方便理解大量旧教程和遗留项目。代价:官方已停止维护,依赖、编译器与新网络兼容需要自行承担;它不是今天新建项目的默认推荐。

# Ganache:在本机运行的以太坊测试链

Ganache (opens new window) 提供本地 EVM、测试账户、测试 ETH 和 RPC 接口,让开发者不用等公共测试网就能部署和调用合约。它还支持快照与回退、控制出块和时间、Fork 网络状态,职责接近 Anvil。

怎么配合 Truffle 使用:启动 Ganache 提供本地 RPC → 在 Truffle 中配置该 RPC → 编译、部署和测试合约 → 前端通过 ethers 或 viem 连接同一个 RPC 调试。Ganache 不要求必须配 Truffle,也能供其他开发工具或脚本连接。

在已安装 Ganache 的历史项目中,可以手动运行:

# 仅监听本机;先确认 8545 未被 Anvil 或其他节点占用。
# chainId 用于钱包网络识别,前端、钱包和部署配置需要与它一致。
npx --no-install ganache --server.host 127.0.0.1 --server.port 8545 --chain.chainId 1337
1
2
3

优点:测试反馈快,可以重复构造失败、回滚和时间变化等场景。代价:本地环境不能完整复现主网拥堵、真实共识和外部服务,而且官方已停止维护。生成的测试私钥只能用于本地测试,不能存放真实资产;本地 RPC 也不应暴露到公网。

Truffle 与 Ganache 的官方仓库均已于 2024 年 2 月 26 日归档。读历史项目要认识它们,新建项目则优先评估 Foundry + Anvil 或 Hardhat。当前工具链的实际使用复用 合约测试、部署与升级,不用再维护一套重复教程。

# 实际项目怎么组合

  • React 项目需要现成钱包界面:RainbowKit + wagmi + viem,兼顾连接 UI、React 状态和链交互。
  • 更偏好 ConnectKit 的交互:ConnectKit + wagmi + viem,通常与 RainbowKit 二选一。
  • 需要邮箱、社交登录或嵌入式钱包:评估 Privy,并按 EVM 或 Solana 选择链交互库;不必把它所有登录与钱包能力都启用。
  • 开发 Solana 应用:Solana Kit + 相应程序插件与钱包接入方案,不能套用 EVM 合约调用方式。
  • React 项目需要自行设计钱包界面:wagmi + viem,不必为了连接钱包强制引入 RainbowKit。
  • 后端、脚本或非 React 页面:按团队经验选择 ethers.js 或 viem,不需要 React Provider 和钱包 UI 层。
  • 已有 web3-react + ethers 项目:先弄清连接器、Provider 和状态传递,结合归档后的维护风险决定是否迁移,不把它与新项目推荐组合混为一谈。
  • 已有 Truffle + Ganache 项目:先恢复原有测试基线,再评估迁移到维护中的合约开发工具;钱包 UI 与这次工具链迁移不是同一层问题。

项目实例可复习 Aladdin 的 wagmi、viem 与 SIWE,以及 Web3 大学的 viem 和 Privy。其余工具用于理解选型,不能都说成这两个项目的实际技术栈。

# 一个完整的浏览器调用入口

下面用于安装了 viem 的 Vite 浏览器项目,连接本地 Anvil(chainId 31337)。先按 合约工程 部署 CompletionRegistry,再把地址填入页面输入框。钱包应连接本地链,且使用该合约的 owner 账户;本地测试账户不能存放真实资产。

index.html 的最小页面:

<!-- 地址来自本地实际部署结果;不要填写主网地址。 -->
<input id="registry" placeholder="本地合约地址" />
<button id="complete">把当前账户标记为完成</button>
<pre id="result"></pre>
<script type="module" src="/src/main.ts"></script>
1
2
3
4
5

src/main.ts:

import {
  createPublicClient, createWalletClient, custom, getAddress,
  http, parseAbi, type EIP1193Provider,
} from 'viem';
import { foundry } from 'viem/chains';

// 本例演示单个注入钱包;多钱包产品可交给 wagmi/connectors 管理。
declare global {
  interface Window { ethereum?: EIP1193Provider }
}

const abi = parseAbi([
  'function markCompleted(address learner)',
  'function completed(address learner) view returns (bool)',
  'error Unauthorized(address caller)',
  'error ZeroAddress()',
  'error AlreadyCompleted(address learner)',
]);
const publicClient = createPublicClient({
  chain: foundry,
  transport: http('http://127.0.0.1:8545'), // 只查询本地节点。
});
const button = document.querySelector<HTMLButtonElement>('#complete')!;
const result = document.querySelector<HTMLElement>('#result')!;
const input = document.querySelector<HTMLInputElement>('#registry')!;

button.addEventListener('click', async () => {
  button.disabled = true; // 减少重复点击;不能替代合约幂等和权限检查。
  try {
    if (!window.ethereum) throw new Error('请先安装并连接测试钱包');
    const wallet = createWalletClient({
      chain: foundry, transport: custom(window.ethereum),
    });
    const [account] = await wallet.requestAddresses(); // 请求钱包账户访问权限。
    if (!account) throw new Error('未取得钱包账户');
    if (await wallet.getChainId() !== foundry.id) {
      throw new Error('请先切换到本地 Anvil 网络');
    }
    const address = getAddress(input.value.trim()); // 校验并规范化输入地址。
    result.textContent = '正在模拟调用';
    const { request } = await publicClient.simulateContract({
      address, abi, functionName: 'markCompleted',
      args: [account], account, // 模拟必须使用真实调用者,权限结果才有意义。
    });
    result.textContent = '等待钱包签名';
    const hash = await wallet.writeContract(request);
    result.textContent = `已提交,等待回执:${hash}`;
    const receipt = await publicClient.waitForTransactionReceipt({ hash });
    if (receipt.status !== 'success') throw new Error('交易执行失败');
    const completed = await publicClient.readContract({
      address, abi, functionName: 'completed', args: [account],
      blockNumber: receipt.blockNumber, // 查询回执所在区块的结果。
    });
    result.textContent = `本地区块已执行,完成状态:${completed}`;
  } catch (error) {
    // 使用 textContent,避免把外部错误内容作为 HTML 注入。
    result.textContent = error instanceof Error ? error.message : '操作失败';
  } finally {
    button.disabled = false;
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

本例只展示本地调用链;生产中还要增加账户切换失效处理、交易替换追踪、超时后的继续查询和链上最终性策略。simulateContract 与正式广播之间状态可能变化,所以仍需处理真实失败。

# 页面状态不能只有 loading 和 success

建议区分:等待签名、已广播、等待执行、执行失败、已执行待确认、业务确认完成。用户拒绝签名不等于链上失败;回执等待超时也不等于交易失败。

后端确认支付要核对 chainId、白名单合约、事件类型、付款人与业务 ID、金额、代币和确认策略。交易里的 to 不总能代表最终收款人,尤其是 Router、代理或批量调用,必须按业务合约语义验证。

# 刷新页面、切换账户后怎样继续跟踪

交易一旦广播,就不依赖当前页面存活。提交后保存业务操作 ID、chainId、发送者、nonce 和哈希,页面刷新后从服务端任务记录或本地待处理记录恢复查询;本地记录只是跟踪线索,最终仍由后端验证链上事实。

读请求的缓存键包含链、账户和合约,返回时检查上下文是否仍匹配。账户 A 的交易等待过程中切到 B,不能把 A 的成功结果写成 B 已购买。取消旧请求可以节省资源,但无法撤销已经广播的交易。

如果用户修改报价、支付币种或目标链,需要新业务操作并重新确认;如果只是网络超时后的同一次重试,应继续追踪原操作,避免重建订单或重复支付。前端状态和后端持久记录要通过业务 ID 关联,而不是只保留一个全局 loading 布尔值。

# 后端怎样验证一笔课程购买

后端先从可信配置取得允许的链、合约、代币和 ABI,不让用户自由指定验证对象;再读取成功回执,检查业务事件确实由允许的合约发出,并匹配课程、付款人、金额及业务标识。还要按合约语义区分付款人和受益人,不能在代付场景硬性假定它们等于 tx.from。

同一交易可能包含多条购买事件,应按具体事件及业务键落库;一笔已兑换过权益的事件,不能被另一个 HTTP 请求重复认领。事务完成后,页面从业务 API 读取确认结果;索引未赶上时显示同步状态,不要求用户重新付钱。

# 前端可以乐观更新到什么程度

点赞或收藏可以较容易地先展示再回退,资金权益则要更谨慎。可以立即显示已提交和预期变化,但需明确待确认;可提现余额和最终课程权限以已验证结果为准。RPC 故障时展示最后更新时间和恢复入口,而不是把旧余额当成实时数据。

交易失败应区分拒签、余额或授权不足、业务 revert、RPC 暂时不可用和未知确认状态。用户能决定的原因给具体提示,内部异常保留关联 ID,不把节点原始长错误或敏感配置直接展示。

# 事件、文件与部署

索引服务保存游标并补扫,流程见 事件索引与指标实践。图片、课程视频和 NFT 元数据通常存链下;IPFS 的 CID 是内容标识,不保证永远有人保存,仍需 pin、备份和访问策略。

前端配置链 ID、公开 RPC、合约地址和 ABI 版本;私钥、数据库密码和私有 API key 只能留在服务端。公开 RPC key 即使限制域名也不应被视为秘密。部署使用 AWS 工程路线,不在 Web3 下再复制一套云服务介绍。

# 面试时可以这样回答

ConnectKit、RainbowKit 和 Privy 怎么选?参考答案

我先看用户怎么进入产品。如果用户已经有钱包,只需要成熟的连接和切链界面,我会在 ConnectKit 与 RainbowKit 中按钱包支持和交互定制选一个,底层继续用 wagmi 和 viem。如果希望用户用邮箱或社交账号登录后就能使用钱包,我会评估 Privy 的身份和嵌入式钱包能力,但也会考虑服务依赖、费用和钱包恢复机制。无论选哪个,连接状态都不能直接替代后端身份验证和业务权限检查。

Solana Kit 和 ethers、viem 是什么关系?参考答案

它们都能帮助应用与区块链交互,但面向的链不同。ethers 和 viem 主要面向 EVM,围绕合约地址和 ABI 调用;Solana Kit 面向 Solana,围绕账户、程序和交易指令组织操作。所以它不是钱包弹窗,也不是把 viem 的 RPC 地址换一下就能使用的替代品。迁移到 Solana 时,要重新适配账户读取、指令构造、签名和交易确认。

Truffle 和 Ganache 分别干什么,现在还会选吗?参考答案

Truffle 负责合约的编译、部署脚本和测试,Ganache 提供执行这些合约的本地测试链。可以理解为一个组织开发流程,一个提供运行环境,二者也能独立使用。它们的官方仓库已经归档,所以新项目我会优先评估 Foundry 配 Anvil 或 Hardhat;维护老项目则先保证原有测试可重复执行,再决定怎么迁移,而不是直接换掉所有工具。

ethers、viem、wagmi 和 RainbowKit 有什么区别?参考答案

我按三层区分:ethers 和 viem 是链交互库,负责查数据、签名和调用合约;wagmi 把连接状态和链交互封装成 React Hooks,并结合 TanStack Query 管理请求;RainbowKit 则在这套能力上提供钱包连接界面。所以 ethers 和 viem 通常是底层选型,而 RainbowKit、wagmi、viem 是可以组合使用的。比如用户点击 RainbowKit 的连接按钮,wagmi 管理账户和链状态,之后业务通过 wagmi 或 viem 调用合约。

新项目为什么选择 wagmi,而不是 web3-react?参考答案

如果项目需要 React 钱包连接和较多合约读写,我会优先评估 wagmi:它与 viem 和 TanStack Query 配合,可以把连接状态、合约请求和缓存放在同一套方案里,也便于接入 RainbowKit。web3-react 更侧重连接器和连接状态,需要自己组合更多能力,而且官方仓库已经归档,新项目还要承担维护风险。但已有 web3-react 项目不应直接重写,需要先评估迁移成本,并验证连接、切链和签名这些关键流程。

DApp 为什么还要后端?参考答案

合约适合保存资产和权限等关键事实,但不适合复杂检索、大文件和所有业务计算。后端负责会话、查询、索引与外部服务,关键支付结果则按链上回执和事件核验。这样既保留链上的可验证性,也让用户获得正常网站的查询速度和交互体验。

用户刷新页面后交易进度丢了,怎么设计?参考答案

交易状态不能只存在组件里。广播后保存业务 ID、链、账户和哈希,刷新时恢复跟踪,后端继续验证回执和事件。替换交易还要跟踪 nonce;查不到结果时保留待确认,而不是直接失败并要求重付。切换账户后只显示当前账户数据,不能把旧请求结果写进新账户状态。

前端已经确认交易成功,后端为什么还要再查?参考答案

前端输入不可信,哈希可能来自别的链、别的合约或别人的付款。后端要核对成功回执、白名单合约和匹配的业务事件,再处理确认与幂等。即使链上真实成功,也要防止同一事件重复领取权益,所以不能只接受一个 success 字段。

# 官方参考