fuels-ts 如何手动部署 SRC14 代理合约并升级合约目标
【免费下载链接】fuels-tsFuel Network Typescript SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-ts
在 fuels-ts(Fuel Network TypeScript SDK)中,如果你希望合约可以后续升级、且对外暴露的地址保持不变,需要把合约部署在一个代理合约后面:先部署真实合约,再部署一个 SRC14 兼容的 owned 代理合约并把目标指向该合约,之后所有调用都通过代理合约 ID 进行;升级时只需部署新版合约并更新代理的目标。fuels包导出了Src14OwnedProxy和Src14OwnedProxyFactory,就是官方推荐使用的 SRC14 合规 owned 代理合约的 TypeScript 实现(与fuels deploy命令内部使用的代理一致)。
官方文档也提示:自动化场景下更推荐直接用fuels deploy命令,它会替你完成部署与升级的全部流程;下面的手动流程适合你想自己控制每一步、或需要定制部署逻辑的情况。完整来源见 Proxy Contracts 指南 和 Deploying Contracts 指南。
准备条件
- 项目中安装
fuels包(版本以 版本页 为准,安装命令为pnpm add fuels@<fuels版本号>)。 - 编写 Sway 合约并构建产物:运行
forc build,或推荐通过 Fuels CLI 运行fuels build(fuels build提供端到端的类型支持)。 - 用 Typegen 为合约生成 TypeScript 类型:
pnpm fuels typegen -i ./abis/*-abi.json -o ./types-i后面是合约构建后生成的、以-abi.json结尾的 ABI 文件路径;-o是生成类型的输出目录,合约类型默认就会生成(--contract是默认值,可省略)。
- 需要一个可连接的 Fuel 节点地址和一个部署者钱包的私钥,用于创建
Provider和Wallet。
文档示例使用一个计数器合约演示整个流程。初始版本(v1)只有一个counter存储槽:
contract; abi Counter { #[storage(read)] fn get_count() -> u64; #[storage(write, read)] fn increment_count(amount: u64) -> u64; #[storage(write, read)] fn decrement_count(amount: u64) -> u64; } storage { counter: u64 = 0, }后续升级版本(v2)新增了increments存储槽和get_increments()方法,用来模拟“新版本合约多了一个存储槽”的升级场景。两个完整 Sway 文件分别在 counter 和 counter-v2,两者都要构建并各自生成类型(示例中的Counter、CounterFactory、CounterV2、CounterV2Factory就来自这一步的产物)。
第一步:部署被代理的真实合约
使用 Typegen 生成的CounterFactory部署合约:
import { Provider, Wallet } from 'fuels'; import { CounterFactory } from './types'; // fuels typegen 的输出目录 const provider = new Provider('<本地节点URL>'); const wallet = Wallet.fromPrivateKey('<部署者钱包私钥>', provider); const counterContractFactory = new CounterFactory(wallet); const deploy = await counterContractFactory.deploy(); const { contract: counterContract } = await deploy.waitForResult();上面两处<本地节点URL>和<部署者钱包私钥>需要替换为你自己的值。deploy方法会按合约大小自动选择部署方式(小合约用单个 create 交易,大合约自动拆成 blob 分块),提交交易后即返回contractId、waitForTransactionId和waitForResult;waitForResult()解析后才会拿到contract实例。注意如果用 blob 方式部署大合约,需要多个交易且每个交易都要等待出块,部署耗时会明显变长。
第二步:部署 SRC14 代理合约并初始化目标
部署代理时有两个必须传的参数:
storageSlots:要把真实合约的存储槽和Src14OwnedProxy自身的存储槽合并后传给代理,以便在部署时初始化全部存储槽;configurableConstants:这是该 SRC14 合约特有的可配置常量,部署时必须传入,之后再调用initialize_proxy完成代理初始化。INITIAL_TARGET指向第一步部署的合约 ID,INITIAL_OWNER是代理的所有者地址(文档用的是部署者钱包地址)。
import { Src14OwnedProxy, Src14OwnedProxyFactory } from 'fuels'; /** * 必须把全部存储槽传给代理,以完成存储槽初始化 */ const storageSlots = counterContractFactory.storageSlots.concat( Src14OwnedProxy.storageSlots ); /** * 这些可配置常量是文档推荐的 SRC14 合规合约特有的: * 部署时必须传入,之后还要调用 `initialize_proxy` 完成代理初始化 */ const configurableConstants = { INITIAL_TARGET: { bits: counterContract.id.toB256() }, INITIAL_OWNER: { Initialized: { Address: { bits: wallet.address.toB256() } }, }, }; const proxyContractFactory = new Src14OwnedProxyFactory(wallet); const proxyDeploy = await proxyContractFactory.deploy({ storageSlots, configurableConstants, }); const { contract: proxyContract } = await proxyDeploy.waitForResult(); const { waitForResult } = await proxyContract.functions .initialize_proxy() .call(); await waitForResult();initialize_proxy()调用完成并waitForResult解析后,代理合约才算部署并初始化完毕。
第三步:通过代理合约 ID 调用合约
关键约束:实例化合约时只使用代理合约的 ID。代理 ID 在后续升级中保持不变,这样升级后调用方代码无需改动:
import { Counter } from './types'; /** * 实例化时只用代理合约 ID: * 即使未来升级,它也是静态不变的 */ const proxiedContract = new Counter(proxyContract.id, wallet); const incrementCall = await proxiedContract.functions.increment_count(1).call(); await incrementCall.waitForResult(); const { value: count } = await proxiedContract.functions.get_count().get(); console.log('count:', count.toNumber() === 1);文档示例的输出是count: true,表示通过代理调用increment_count(1)后读到的计数为 1(示例结果,供对照格式,不是固定数值承诺)。
第四步:部署 v2 并升级代理目标
把合约改成 v2(新增increments存储槽和get_increments()方法)后,升级分两步:部署新版合约,再调用代理的set_proxy_target把目标指向新合约 ID:
import { CounterV2, CounterV2Factory } from './types'; const deployV2 = await CounterV2Factory.deploy(wallet); const { contract: contractV2 } = await deployV2.waitForResult(); const updateTargetCall = await proxyContract.functions .set_proxy_target({ bits: contractV2.id.toB256() }) .call(); await updateTargetCall.waitForResult();然后仍用同一个代理合约 ID、换成 v2 的合约类重新实例化,验证升级后的新方法可用:
/** * 仍然用同一个代理 ID 实例化, * 只是换成了新的合约类 */ const upgradedContract = new CounterV2(proxyContract.id, wallet); const incrementCall2 = await upgradedContract.functions .increment_count(1) .call(); await incrementCall2.waitForResult(); const { value: increments } = await upgradedContract.functions .get_increments() .get(); const { value: count2 } = await upgradedContract.functions.get_count().get(); console.log('secondCount', count2.toNumber() === 2); console.log('increments', increments);文档示例输出为secondCount true和increments的值:count2为 2 说明代理在目标切换后仍保留原有counter存储(升级前累计 1,升级后再 +1 变成 2),get_increments()能读到值说明 v2 新增的存储槽工作正常。
限制与已知问题
- 新增存储槽必须先初始化:当新版本合约新增了存储槽时,这些槽必须先通过代理合约写入(初始化),之后才能被读取,否则交易会 revert。文档建议的做法是在代理合约上先对新存储槽执行一次写入。
- 存储槽必须完整传递:部署代理时漏传任何存储槽都会导致初始化不完整,
counterContractFactory.storageSlots.concat(Src14OwnedProxy.storageSlots)这一步不能省略。 - 本文流程是手动实现的参考路径;文档明确建议,如果不需要自己控制每一步,直接用
fuels deploy命令部署和升级合约,它会用同样的 SRC14 代理自动处理全部工作(见 fuels CLI 命令文档)。 - 大合约以 blob 方式部署时耗时更长,每个 blob 交易都要等待出块后再执行下一步,部署与可交互之间的等待时间要相应放大。
参考文件
- 手动部署与升级代理合约指南
- 合约部署指南
- typegen 示例代码
- 生成类型文档
【免费下载链接】fuels-tsFuel Network Typescript SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-ts
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考