基于 TUWA SDK 构建端到端全栈 Web3 React 应用
本系统化指南将带领您基于官方 TUWA SDK 从零搭建一个现代化 Next.js App Router 全栈应用。完成后您将拥有:
- 通过 Nova Connect 弹窗组件与底层的 Satellite Connect 无头状态机,实现轻量、免绑定的钱包连接;
- 集成基于 CAIP-122 标准的 SIWX 身份认证:用户签名链无关消息,服务端在安全签名的
HttpOnlyCookie 中保存权威会话; - 通过 Pulsar 交易状态机与 Nova Transactions 组合实现零配置的多链交易跟踪与吐司弹窗反馈;
- 编写类型安全的 Next.js Server Actions,零数据库依赖即可安全校验调用方钱包权限。
网络差异步骤提供 EVM 与 Solana 选项卡。选项卡全局同步:选择一次即可跨步骤生效。
准备工作
- Next.js 16 (App Router) 与 React 19.2 或更高版本。示例使用
'use client'指令及@/路径别名。 - 配置应用根域名于
NEXT_PUBLIC_APP_URL(如http://localhost:3000):服务端仅接受针对该域名的签名认证请求。
步骤 1:安装 SDK 与导入全局设计系统样式
pnpm add @tuwaio/sdk @tuwaio/evm-sdk @wagmi/core viem在全局 CSS 样式入口引入 Nova UI Kit 核心变量与样式定义:
src/app/globals.css
@import '@tuwaio/sdk/styles/all.css';步骤 2:配置链端连接池与 RPC 传输通道
在组件树外创建一次 @wagmi/core 配置。借助步骤 5 的 EVMConnectorsWatcher,完全无需引入 WagmiProvider 或 React Query:
src/configs/appConfig.ts
import { createDefaultTransports } from '@tuwaio/evm-sdk/satellite';import { createConfig, injected } from '@wagmi/core';import { mainnet, sepolia } from 'viem/chains'; export const appChains = [sepolia, mainnet] as const; export const wagmiConfig = createConfig({ chains: appChains, connectors: [injected()], transports: createDefaultTransports(appChains), ssr: true,});步骤 3:添加服务端 SIWX 认证路由
服务端负责下发随机 Nonce、验证密码学签名并维护 Session 凭证:
src/lib/authStores.ts
import { MemorySiwxNonceStore, MemorySiwxSessionStore } from '@tuwaio/sdk/siwx/server'; // In memory, for one server process. They refuse to run with NODE_ENV=production: use a shared store such as Redis// there (see the @tuwaio/siwx-server page).export const sessionStore = new MemorySiwxSessionStore();export const nonceStore = new MemorySiwxNonceStore();单个 Next.js Route Handler 即可统一处理全部认证生命周期端点:
src/app/api/siwx/[...siwx]/route.ts
import { createSiwxApiHandler } from '@tuwaio/sdk/siwx/server-next'; import { nonceStore, sessionStore } from '@/lib/authStores'; const appUrl = new URL(process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000'); export const { GET, POST, DELETE } = createSiwxApiHandler({ sessionStore, nonceStore, policy: { expectedDomain: appUrl.host, expectedUri: appUrl.origin, requireExpirationTime: true, maxIssuedAtAgeSeconds: 300, },});步骤 4:初始化 Local-First 交易存储库
src/hooks/pulsarStore.ts
import { pulsarEvmAdapter } from '@tuwaio/evm-sdk/pulsar';import { createBoundedUseStore, createPulsarStore, type Transaction } from '@tuwaio/sdk/pulsar'; import { appChains, wagmiConfig } from '@/configs/appConfig'; export const pulsarStore = createPulsarStore<Transaction>({ name: 'my-app-transactions', // the localStorage key adapter: [pulsarEvmAdapter(wagmiConfig, appChains)],}); export const usePulsarStore = createBoundedUseStore(pulsarStore);步骤 5:注入 Web3 顶层 Provider 组合
src/providers/Providers.tsx
'use client'; import { EVMConnectorsWatcher } from '@tuwaio/evm-sdk/nova-connect';import { satelliteEVMAdapter } from '@tuwaio/evm-sdk/satellite';import { NovaConnectProvider, type NovaConnectProviderProps } from '@tuwaio/sdk/nova-connect';import { NovaTransactionsProvider } from '@tuwaio/sdk/nova-transactions/providers';import { getAdapterFromConnectorType } from '@tuwaio/sdk/orbit';import { useInitializeTransactionsPool } from '@tuwaio/sdk/pulsar';import { SatelliteConnectProvider, useSatelliteConnectStore } from '@tuwaio/sdk/satellite';import type { ReactNode } from 'react'; import { appChains, wagmiConfig } from '@/configs/appConfig';import { usePulsarStore } from '@/hooks/pulsarStore'; const satelliteAdapter = satelliteEVMAdapter(wagmiConfig, appChains); const siwx: NovaConnectProviderProps['siwx'] = { getNonce: async () => { const res = await fetch('/api/siwx/nonce'); return ((await res.json()) as { nonce: string }).nonce; }, verifier: async (payload) => { const res = await fetch('/api/siwx/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); return res.ok ? res.json() : null; }, destroyer: async () => { await fetch('/api/siwx/logout', { method: 'POST' }); },}; // The modals and toasts of Nova Transactions, fed by the Pulsar store.function TransactionsUI() { const transactionsPool = usePulsarStore((state) => state.transactionsPool); const initialTx = usePulsarStore((state) => state.initialTx); const closeTxTrackedModal = usePulsarStore((state) => state.closeTxTrackedModal); const executeTxAction = usePulsarStore((state) => state.executeTxAction); const initializeTransactionsPool = usePulsarStore((state) => state.initializeTransactionsPool); const getAdapter = usePulsarStore((state) => state.getAdapter); const activeConnection = useSatelliteConnectStore((state) => state.activeConnection); // Restarts the trackers of pending transactions after a page reload. useInitializeTransactionsPool({ initializeTransactionsPool }); return ( <NovaTransactionsProvider transactionsPool={transactionsPool} initialTx={initialTx} closeTxTrackedModal={closeTxTrackedModal} executeTxAction={executeTxAction} connectedWalletAddress={activeConnection?.isConnected ? activeConnection.address : undefined} connectedAdapterType={getAdapterFromConnectorType(activeConnection?.connectorType ?? 'evm:')} adapter={getAdapter()} /> );} export function Providers({ children }: { children: ReactNode }) { const transactionsPool = usePulsarStore((state) => state.transactionsPool); const getAdapter = usePulsarStore((state) => state.getAdapter); return ( <SatelliteConnectProvider adapter={satelliteAdapter} autoConnect> <EVMConnectorsWatcher wagmiConfig={wagmiConfig} /> <TransactionsUI /> <NovaConnectProvider appChains={appChains} transactionPool={transactionsPool} pulsarAdapter={getAdapter()} siwx={siwx} withBalance withChain > {children} </NovaConnectProvider> </SatelliteConnectProvider> );}在根布局组件与首页渲染 Provider 与连接按钮:
src/app/layout.tsx
import './globals.css'; import type { ReactNode } from 'react'; import { Providers } from '@/providers/Providers'; export default function RootLayout({ children }: { children: ReactNode }) { return ( <html lang="en"> <body> <Providers>{children}</Providers> </body> </html> );}src/app/page.tsx
'use client'; import { ConnectButton } from '@tuwaio/sdk/nova-connect/components'; export default function HomePage() { return ( <main> <ConnectButton /> </main> );}步骤 6:发送并追踪区块链交易
发送交易的方式保持不变:构造智能合约调用(EVM)或程序指令(Solana),将返回的 Promise 传入 Store 的 executeTxAction。参考交易追踪指南的第 2、3、6 和 7 步,仅需调整包导入路径:
| 分包导入(指南示例) | 统一 TUWA SDK 导入 |
|---|---|
@tuwaio/pulsar-core | @tuwaio/sdk/pulsar |
@tuwaio/pulsar-react | @tuwaio/sdk/pulsar |
@tuwaio/orbit-core | @tuwaio/sdk/orbit |
@tuwaio/nova-connect/components | @tuwaio/sdk/nova-connect/components |
@tuwaio/nova-connect/satellite | @tuwaio/sdk/nova-connect/satellite |
@tuwaio/pulsar-evm | @tuwaio/evm-sdk/pulsar |
@tuwaio/pulsar-solana | @tuwaio/solana-sdk/pulsar |
交易发出后将立即展示在 Nova 弹出的 Toast 通知与历史模态框中,即使刷新页面也会被可靠追踪。借助 useTxFinished Hook 监听终态完成事件,即可驱动业务数据刷新。
步骤 7:在服务端 Server Actions 中读取身份凭证
src/app/actions.ts
'use server'; import { getSiwxServerSession, isSessionMatchingTarget } from '@tuwaio/sdk/siwx/server';import { cookies } from 'next/headers'; import { sessionStore } from '@/lib/authStores'; export async function saveProfile(walletAddress: string, nickname: string) { const session = await getSiwxServerSession({ cookieSource: await cookies(), sessionStore }); if (!session || !isSessionMatchingTarget(session, walletAddress)) { throw new Error('Unauthorized'); } // The request comes from the owner of `walletAddress`: save `nickname`. return { walletAddress, nickname };}切勿轻信前端传入的会话状态:前端 SIWX 仅为 UI 状态,只有服务端直接读取验证过的 HttpOnly Cookie 才是权威所有权证明。
🌐 EVM 与 Solana 双链协同
<SatelliteConnectProvider adapter={[satelliteEVMAdapter(wagmiConfig, appChains), satelliteSolanaAdapter({ rpcUrls: solanaRPCUrls })]} autoConnect> <EVMConnectorsWatcher wagmiConfig={wagmiConfig} /> <SolanaConnectorsWatcher /> <TransactionsUI /> <NovaConnectProvider appChains={appChains} solanaRPCUrls={solanaRPCUrls} transactionPool={transactionsPool} pulsarAdapter={getAdapter()} siwx={siwx} withBalance withChain > {children} </NovaConnectProvider></SatelliteConnectProvider>📚 后续指引
- 将交易同步至 Quasar Cloud 实现多设备持久化:Quasar 同步指南。
- 使用 JWT 与 JWKS 接入外部认证系统:SIWX JWT 外部认证。
- 通过 Supabase RLS 进行行级权限管控:Supabase RLS 指南。
最后更新于