如果把一个基于React和以太坊钱包的dApp迁到Sui,最先卡住的往往不是合约重写,而是前端如何与Sui的对象模型打交道。以太坊上的React组件通常围绕账户地址、uint256余额和事件日志来组织数据,而Sui把链上资产建模为可拥有、可传递的对象,Move合约则通过模块函数来操作这些对象。本文会从实际前端改造出发,梳理React迁移到Sui + Move时需要替换的依赖、重构的账户逻辑以及新的合约调用方式。

理解Sui与Move带来的心智模型变化
迁移前必须先理解Sui的对象模型。以太坊是账户余额模型,ERC-20代币在合约里维护一个mapping,前端读取某个地址的余额其实就是查合约存储中的一个键值。Sui则不同,代币本身是一个泛型Coin对象,钱包地址拥有的Coin对象直接存放在链上地址下。React组件想要显示余额,需要查询地址拥有的Coin<T>对象,再累加它们的value字段。这个差异会导致原来的useEffect余额拉取逻辑完全重写。
Move合约的调用方式也从ABI编码转向了更显式的交易块构造。在ethers.js里,调用合约函数通常是contract.methods.transfer(to, amount),框架帮你编码参数并发送交易。在Sui的React前端里,你需要用TransactionBlock对象逐个加入moveCall命令,指定目标模块、函数名、类型参数和函数实参。这样的好处是交易意图非常清晰,前端可以组合多个操作一次提交,但缺点是代码量会有所增加,需要开发者重新适应。
另一个重要区别是事件和订阅机制。Sui没有以太坊那种全局indexed事件日志,而是提供事件查询接口和WebSocket订阅,React组件需要根据事件类型和过滤条件来监听。比如监听某个对象的转移事件,要使用client.queryEvents并指定MoveEventType,而不是简单地按合约地址收日志。迁移时如果继续沿用旧的事件监听思路,很容易漏掉对象变更通知。
用@mysten/dapp-kit重构钱包连接层
在React里接入Sui钱包,官方推荐使用@mysten/dapp-kit。它提供了SuiClientProvider、WalletProvider和ConnectButton等组件,能够快速完成钱包检测、连接、断连和账户切换。开发者在迁移时不需要再手写provider注入和chainId判断,只需要在应用入口包一层Provider即可。
import { SuiClientProvider, WalletProvider } from '@mysten/dapp-kit';
import { getFullnodeUrl } from '@mysten/sui.js/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
const networks = {
testnet: { url: getFullnodeUrl('testnet') },
mainnet: { url: getFullnodeUrl('mainnet') }
};
export default function App({ children }) {
return (
<QueryClientProvider client={queryClient}>
<SuiClientProvider networks={networks} defaultNetwork="testnet">
<WalletProvider autoConnect>
{children}
</WalletProvider>
</SuiClientProvider>
</QueryClientProvider>
);
}这里的QueryClientProvider是dapp-kit依赖的React Query缓存层,用于管理链上数据请求。原来以太坊React应用往往自己写的SWR或useEffect拉取逻辑,迁移后可以借助dapp-kit内置的hooks直接获取客户端实例和当前账户。例如useSuiClient可以拿到SuiClient,useCurrentAccount返回当前连接的钱包地址,useSignAndExecuteTransactionBlock则封装了签名和执行的完整流程。
需要注意的是,Sui钱包地址格式与以太坊不同,它使用0x加64位十六进制字符串。如果原来的React组件里有地址截断显示逻辑,迁移时要调整截断长度和校验规则。另外钱包断开或切换网络时,dapp-kit会触发对应状态更新,之前依赖window.ethereum的监听代码可以全部移除。
从React组件调用Move合约与读取对象
迁移后最常见的需求是调用一个Move合约函数,比如一个计数器合约的increment函数。与以太坊合约实例化不同,Sui前端直接使用TransactionBlock来构造交易,然后通过签名执行hook提交。下面是一个React组件内调用Move函数的示例,假设链上已经部署了counter模块。
import { useSignAndExecuteTransactionBlock, useCurrentAccount } from '@mysten/dapp-kit';
import { TransactionBlock } from '@mysten/sui.js/transactions';
function IncrementButton() {
const account = useCurrentAccount();
const { mutate: signAndExecute } = useSignAndExecuteTransactionBlock();
const handleClick = () => {
if (!account) return;
const txb = new TransactionBlock();
txb.moveCall({
target: '0x1234...::counter::increment',
arguments: []
});
signAndExecute(
{ transactionBlock: txb },
{
onSuccess: (result) => {
console.log('交易摘要', result.digest);
}
}
);
};
return <button onClick={handleClick}>增加计数</button>;
}这段代码里target字符串由三部分组成:包ID、模块名和函数名。包ID是部署Move合约后得到的对象ID,迁移时可以把包ID和模块名统一配置到环境变量或常量文件。如果函数需要传入对象或基础类型,txb.moveCall的arguments数组按顺序填写,对象参数使用txb.object或txb.objectRef传入。
读取链上数据同样变化很大。React组件要显示某个对象的内容,不能再通过合约getter函数自动返回,而要先用client.getObject或client.getOwnedObjects查询对象。对象返回的是Bcs类型数据,前端需要根据Move结构体的字段定义逐层解析。例如查询一个用户拥有的Coin余额,可以先取到所有Coin对象,再把各个对象的balance字段累加。这样虽然多一次查询,但数据来源更直接,不存在合约调用读取和链上存储之间的黑盒。
处理签名、赞助交易与数据同步
签名是迁移过程中最容易忽略的差异点。以太坊React应用通常把交易发送给钱包签名,钱包只负责对EIP-1559交易哈希签名。Sui的交易块包含多个moveCall命令,签名的是整个TransactionBlock的摘要。dapp-kit的useSignAndExecuteTransactionBlock会自动完成签名和广播,但如果你的应用需要离线构建交易或使用自定义签名器,就必须理解TransactionBlock的serialize和sign流程。
赞助交易是Sui提供给React应用的一个实用能力。很多面向普通用户的dApp希望帮用户支付gas费,降低使用门槛。Sui允许gas所有者与签名者分离,前端可以构造交易块后设置gasOwner为赞助方地址,由赞助方签名支付gas。React组件可以结合后端API获取赞助签名,再让用户对交易主体签名。与以太坊的元交易方案相比,Sui赞助交易是协议原生支持,不需要引入复杂的gas relay合约。
数据同步方面,原来React应用里监听以太坊事件通常用ethers的contract.on或者轮询日志。Sui前端推荐使用client.queryEvents配合时间游标做增量拉取,也可以使用WebSocket订阅实时事件。对于频繁变化的对象,可以用useSuiClientQuery这类hook来自动管理请求缓存,但要注意对象版本号变化。一个对象每次被修改后版本都会递增,前端如果缓存了旧版本ID,再次交易时可能遇到对象已更新的错误,所以读取对象后要保留最新version字段。
还有一个实际问题是本地开发网络。以太坊React项目常使用Hardhat或Foundry起本地节点,Sui则使用sui start命令运行本地网络,并配合Sui CLI发布Move包。前端迁移时只需要把dapp-kit的networks配置指向本地RPC地址,钱包可以使用开发模式生成的地址,不需要额外的测试币水龙头。这样整个React开发循环可以保持接近原来的体验,只是合约部署和调试命令从npx hardhat变成了sui client publish。
总体来看,React应用迁移到Sui + Move的核心不是把组件全部重写,而是调整数据获取方式、交易构造方式和钱包交互方式。理解了Sui对象模型和Move模块调用后,原有的UI组件、路由结构和状态管理大部分可以复用,真正需要改动的集中在与链交互的service层和hooks层。按照本文的思路逐层替换,可以避免把旧有的账户余额模型和事件监听逻辑直接照搬,也能更充分利用Sui在对象所有权、赞助交易和并行执行上的优势。