迁移一个已经上线的React质押面板到Swell + rswETH,核心不是替换几个合约地址,而是把前端对质押凭证的认知从静态余额切换为动态衍生品。原先面板可能只展示用户质押的ETH数量,以及对应的奖励累计;切换到流动性质押后,用户钱包里多出swETH和rswETH两个代币,它们的余额会随着协议收益增加或汇率变化而改变。如果前端继续沿用一次性读取ERC20余额的写法,页面数据很快会失真。

调整钱包连接层与合约抽象
迁移的第一步是梳理当前React应用的钱包连接层。不少现有项目使用ethers.js v5,而Swell相关的合约交互示例更偏向ethers v6或viem。虽然不强制更换库,但v6对BigInt的原生支持能减少余额处理中的精度错误。建议在迁移前将依赖升级到ethers v6,并统一使用JsonRpcProvider或BrowserProvider作为providers。如果应用已经使用了wagmi,可以考虑直接通过useReadContract读取Swell合约,但为了减少框架耦合,本文示例统一使用ethers v6。
合约抽象方面,需要把原先的Staking合约调用替换为两个核心合约:Swell Vault负责ETH到swETH的兑换,rswETH Vault负责将swETH封装为rswETH。这两个合约的地址不要硬编码在组件里,建议放入环境变量。ABI也不要用全量,只保留前端用到的函数,减少打包体积和实例化开销。下面是一个最小ABI声明示例,包含了质押和余额查询所需的核心函数。
import { ethers } from 'ethers';
const SWELL_VAULT_ABI = [
"function deposit() external payable returns (uint256)",
"function getSwellETHByEth(uint256 ethAmount) external view returns (uint256)"
];
const RSWETH_ABI = [
"function deposit(uint256 swethAmount) external returns (uint256)",
"function balanceOf(address account) external view returns (uint256)"
];
export { SWELL_VAULT_ABI, RSWETH_ABI };
状态管理也需要分层。原先可能只有一个stakedBalance字段,迁移后至少要维护ethBalance、swethBalance和rswethBalance三个独立字段。另外swETH和rswETH虽然都符合ERC20规范,但rswETH代表的是再质押凭证,解除质押可能需要异步等待期。前端要在store中区分可即时赎回的swETH和需要等待的rswETH,避免用户把两者混为一谈,也避免出现展示余额可以提现但实际仍在锁定期的误导。
实现ETH到swETH再到rswETH的质押流程
质押流程通常分为两步。第一步用户向Swell Vault发送ETH并调用deposit,交易成功后按照当前汇率获得swETH;第二步用户将swETH授权给rswETH Vault,再调用deposit把swETH封装为rswETH。这里要特别注意第一步的deposit函数是payable的,前端发起交易时必须带上value,而不能只发data。漏掉value会导致交易看似成功但实际没有存入任何ETH,或者直接触发合约回滚。
在React组件中,可以通过一个自定义Hook来封装这两个交易。初始化合约实例后,读取用户地址,再根据输入的ETH数量计算可质押金额。用户点击质押按钮时,先调用Swell Vault的deposit,并设置合理的gasLimit。因为Swell的质押合约在不同网络上的gas消耗差异较大,建议用estimateGas动态估算,再上浮20%到30%。第一步交易成功后,等待一个区块确认再读取用户swETH余额,避免因节点未同步导致余额为0。
import { ethers } from 'ethers';
import { SWELL_VAULT_ABI, RSWETH_ABI } from './abis';
const SWELL_VAULT = import.meta.env.VITE_SWELL_VAULT_ADDRESS;
const RSWETH_VAULT = import.meta.env.VITE_RSWETH_VAULT_ADDRESS;
export function useSwellStake(signer, account) {
const [step, setStep] = useState('idle');
const [txHash, setTxHash] = useState('');
async function stakeEth(ethAmount) {
if (!signer || !account) return;
setStep('depositing');
try {
const vault = new ethers.Contract(SWELL_VAULT, SWELL_VAULT_ABI, signer);
const tx = await vault.deposit({ value: ethers.parseEther(ethAmount) });
setTxHash(tx.hash);
await tx.wait();
setStep('sweth_received');
} catch (error) {
setStep('error');
throw error;
}
}
async function restakeSweth(swethAmount) {
const sweth = new ethers.Contract(SWELL_VAULT, SWELL_VAULT_ABI, signer);
const rsweth = new ethers.Contract(RSWETH_VAULT, RSWETH_ABI, signer);
// 注意spender必须是rswETH Vault地址
const allowance = await sweth.allowance(account, RSWETH_VAULT);
if (allowance < ethers.parseEther(swethAmount)) {
const approveTx = await sweth.approve(RSWETH_VAULT, ethers.MaxUint256);
await approveTx.wait();
}
const tx = await rsweth.deposit(ethers.parseEther(swethAmount));
await tx.wait();
}
return { stakeEth, restakeSweth, step, txHash };
}
第二步涉及ERC20授权。如果前端检测到用户对rswETH Vault的allowance不足,需要先发起approve交易。这里一个常见误区是把approve的spender写成Swell Vault,实际应该写rswETH Vault地址。完成approve后再调用rswETH Vault的deposit存入swETH。两步之间要显示明确的进度提示,不能只显示全局loading,否则用户无法判断当前是在等待ETH存款确认,还是在等待授权交易完成。
余额同步、事件监听与汇率展示
迁移到流动性质押后,余额更新策略必须改变。swETH余额在用户质押后会立即变化,但rswETH的兑换率会随着restaking收益持续增长。前端如果只在交易后手动刷新一次余额,无法反映账龄收益。因此需要设置合理的轮询间隔,同时监听ERC20的Transfer事件来触发刷新。这样当用户收到额外rswETH铸造时,页面能自动更新,而不需要用户手动刷新。
可以使用ethers的contract.on监听用户地址相关的Transfer事件。因为很多rswETH实现会在用户余额增加时发送Transfer事件,即使是非转账的收益累积也可能通过内部铸造完成。监听时要注意过滤掉历史事件,只处理最新区块。轮询方面,推荐每15秒至30秒读取一次余额,不必过于频繁。对于Layer2网络,可以利用更低延迟的RPC来缩短轮询间隔。
import { ethers } from 'ethers';
export function watchRswethBalance(contract, account, callback) {
const filter = contract.filters.Transfer(null, account);
contract.on(filter, (from, to, value, event) => {
if (to.toLowerCase() === account.toLowerCase()) {
callback(value);
}
});
}
展示汇率时,不要把1 swETH等同于1 ETH直接写死。Swell的汇率会变化,应该从合约读取getSwellETHByEth或类似的视图函数。rswETH与swETH之间的汇率也要单独获取。前端可以维护一个汇率缓存,在用户打开页面和余额更新时刷新。如果用户只质押少量ETH,展示时注意保留足够的小数位,避免因四舍五入产生误解。尤其在计算预期rswETH数量时,应使用BigInt运算而不是JavaScript的Number类型。
错误处理与安全边界
迁移后的React应用要处理更多合约错误。用户拒绝签名或交易、gas费高于预期、合约暂停、网络切换等场景都要有对应的UI提示。捕获错误时不能只弹出一个error.message,因为RPC返回的原始错误可能包含十六进制数据。建议解析常见的revert原因,例如用户取消时错误码是ACTION_REJECTED或消息包含user rejected。前端应该把这类情况与合约实际失败区分开,避免把用户主动取消展示成系统级故障。
在代码中,可以用try/catch包住交易调用,并针对错误码做分支。如果错误是用户拒绝,则只提示交易已取消,不显示失败状态。如果是其他错误,可以保留错误详情并展示区块浏览器链接,但正文中不提供可点击链接。安全方面,绝对不要在React代码中硬编码私钥或助记词,所有签名必须通过钱包插件完成。对于高金额质押,还可以增加交易预览和二次确认步骤。
try {
const tx = await vault.deposit({ value: amount });
await tx.wait();
} catch (error) {
if (error.code === 'ACTION_REJECTED') {
setMessage('交易已取消');
} else if (error.message && error.message.indexOf('user rejected') !== -1) {
setMessage('用户拒绝了签名');
} else {
setMessage('交易失败,请检查gas或网络');
}
}
迁移完成后,建议增加一个只读模式,在用户未连接钱包时也能看到Swell当前的ETH/swETH汇率和rswETH总锁仓量等公开数据。这样既能方便用户决策,也能降低迁移后空状态带来的困惑。最后把交易相关按钮统一加上防重复点击、gas提示和交易哈希展示,整个应用对流动性质押衍生品的支持才算完整。通过分层管理资产状态、动态同步余额、细化错误反馈,React应用能够稳定承载Swell与rswETH的质押流程。