fuels-ts 合约部署指南:ContractFactory 的 Create 与 Blob 双路径机制深度解析
本篇技术指南围绕 fuels-ts(Fuel Network TypeScript SDK)中的合约部署流程展开,以官方文档中的部署指南为主体,结合 packages/contract 包的源码实现,讲解 ContractFactory 如何根据合约字节码大小自动在“单次 Create 交易”与“Blob 分块 + 加载器合约”两种部署路径之间切换。读完后,你将能够独立完成从构建 Sway 产物、初始化工厂到部署并调用合约的完整流程,并理解 chunkSizeMultiplier、交易 ID 异步获取等关键参数背后的实现原理。
两种部署路径与合约大小阈值
使用 fuels-ts SDK 部署合约的核心入口是 ContractFactory。整个过程包括三个环节:收集合约构建产物(bytecode 与 ABI)→ 初始化合约工厂 → 提交部署交易。
SDK 内部维护着两套不同的部署流程,选择哪一套取决于合约的大小。这个大小阈值不是 SDK 写死的,而是由链上共识参数决定,可以通过 Provider 查询:
import { Provider } from 'fuels';
const provider = new Provider(LOCAL_NETWORK_URL);
const {
consensusParameters: {
contractParameters: { contractMaxSize },
},
} = await provider.getChain();
console.log('contractMaxSize', contractMaxSize);
两个流程的区别如下:
- 单笔 Create 交易:将完整的合约字节码作为 witness 塞入一笔 Create 交易一次性部署;
- Blob 分块部署:将字节码切分成多个 chunk,先以 blob 交易(链上可供 VM 访问的数据)逐块上传,再生成一个引用这些 blob ID 的“加载器合约”(loader contract),最后把这个加载器合约作为一笔 Create 交易部署。
ContractFactory 为这两套流程提供了三个部署方法:
| 方法 | 用途 |
|---|---|
deploy |
部署任意大小的合约,自动根据字节码长度选择合适的路径 |
deployAsCreateTx |
强制使用单笔 Create 交易部署完整字节码 |
deployAsBlobTx |
强制将合约分块为 blobs 上传,再部署加载器 Create 交易 |
注意:走 Blob 路径的部署需要多笔交易——每块 blob 各一笔,最后还有一笔 Create 交易。
从 contract-factory.ts 的源码可以确认这条自动分支逻辑:deploy 方法先通过 provider.getChain() 读取 consensusParameters.contractParameters.contractMaxSize,再比较 this.bytecode.length 与阈值:
// packages/contract/src/contract-factory.ts
async deploy<T extends Contract = TContract>(
deployOptions: DeployContractOptions = {}
): Promise<DeployContractResult<T>> {
const account = this.getAccount();
const { consensusParameters } = await account.provider.getChain();
const maxContractSize = consensusParameters.contractParameters.contractMaxSize.toNumber();
return this.bytecode.length > maxContractSize
? this.deployAsBlobTx(deployOptions)
: this.deployAsCreateTx<T>(deployOptions);
}
而如果显式调用 deployAsCreateTx 部署了超阈值的合约,源码中会直接抛出 CONTRACT_SIZE_EXCEEDS_LIMIT 错误,并提示改用 deployAsBlobTx。
实战指南:用 deploy 部署合约
以下指南演示使用推荐的 deploy 方法完成部署;当然,三种方法可以按合约大小互换使用。指南中使用的工厂类是通过 Typegen 生成的——这是 Fuels CLI 提供的工具,能为你的智能合约提供端到端的类型支持与更好的开发体验。
1. 准备:构建产物与工厂初始化
用 Sway 写完合约后,有两种方式构建部署所需产物:
- 直接运行
forc build(forc是 Fuel 官方的 Sway 构建工具); - 使用 Fuels CLI,通过你的包管理器运行
fuels build。
官方文档更推荐后者,因为 Fuels CLI 提供了更完整的工作流,包括端到端的类型支持。
拿到构建产物(bytecode 与 ABI)后,将其传给 ContractFactory 即可。以 Typegen 生成的 MyContractFactory 为例:
import { Provider, Wallet } from 'fuels';
import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../../env';
import { MyContractFactory } from '../../../../typegend';
const provider = new Provider(LOCAL_NETWORK_URL);
const wallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);
const factory = new MyContractFactory(wallet);
这里传入的是一个 Wallet。从 contract-factory.ts 的构造函数实现看,工厂接受 Account | Provider | null 作为第三个参数:如果该对象带有 provider 属性(即 Wallet 等 Account),SDK 会同时记录 provider 与 account;否则仅记录 provider。没有 account 的工厂无法发起部署,getAccount() 会抛出 ACCOUNT_REQUIRED 错误。Typegen 生成的工厂类(如 MyContractFactory)内部已封装了本合约的 bytecode 与 ABI,因此 new MyContractFactory(wallet) 一行即可完成初始化。
2. 发起部署与异步结果
再次强调,ContractFactory 封装了两条部署路径,deploy 会根据合约大小自动选择。该调用的 Promise 在部署交易提交后立即 resolve,并返回三项内容:contractId、waitForTransactionId 函数和 waitForResult 函数:
// Deploy the contract
const { waitForResult, contractId, waitForTransactionId } =
await factory.deploy();
// Retrieve the transactionId
const transactionId = await waitForTransactionId();
// Await it's deployment
const { contract, transactionResult } = await waitForResult();
这里有几个异步语义值得注意:
contractId是同步可得的。它在 createTransactionRequest 中通过getContractId(bytecode, salt, stateRoot)本地计算得出(默认 salt 为 32 字节随机数),无需等待链上确认;contract实例只在waitForResult完成、交易上链后才会返回。为避免阻塞主流程,可以把这个 Promise 挂到 hook 或监听器上,等合约完全部署后再使用它;- 交易 ID 的可用时机因路径而异。走 Blob 路径时,创建交易(create tx)必须等 blob 交易全部上链并为其注资后才能确定 ID——源码中
deployAsBlobTx里维护了一个txIdPromise,只有在assembleTx(createRequest)完成后才调用txIdResolver解析。因此文档建议用waitForTransactionId来等待其就绪,而不是假设 ID 立即可得。
waitForResult 最终返回 { contract, transactionResult },其中 transactionResult 是 TransactionResult<TransactionType.Create>,包含区块 ID、receipts、费用、gas 用量等信息。此外,deployAsCreateTx 路径的返回中还包含一个可选的 waitForPreConfirmation,可在交易进入内存池但尚未上链时就拿到预确认结果。
3. 执行合约调用
合约部署完成后,即可通过合约实例发起调用:
// Call the contract
const { waitForResult: waitForCallResult } = await contract.functions
.test_function()
.call();
// Await the result of the call
const { value } = await waitForCallResult();
console.log('value', value);
Typegen 生成的合约类为每个 Sway 方法提供了完整的类型签名,contract.functions.test_function() 的入参和返回值都会经过 TypeScript 检查。仓库中的测试用例 contract-factory.test.ts 展示了同样的模式:call() 立即返回 waitForResult,await 之后才能拿到 value(例如 initialize_counter(41) 后断言返回值为 41,increment_counter(1) 后断言为 42),并可用 dryRun 在不产生交易的情况下预估返回值。
大合约的 Blob 分块部署
上面的指南使用的是推荐的 deploy 方法。当你的合约大到无法装进单笔交易时,SDK 会自动切块并以 blobs 提交,供后续的 Create 交易访问。这一流程由 deployAsBlobTx 方法处理:
// Deploy the contract as blobs
const { waitForResult: waitForBlobsAndContractDeployment } =
await factory.deployAsBlobTx({
// setting chunk size multiplier to be 90% of the max chunk size
chunkSizeMultiplier: 0.9,
});
// Await its deployment
const { contract: contractFromBlobs } =
await waitForBlobsAndContractDeployment();
console.log('contractFromBlobs', contractFromBlobs);
chunkSizeMultiplier 参数详解
在上述示例中,我们向部署方法传入了 chunkSizeMultiplier 选项。SDK 会尝试把合约切分到最优大小,但交易的实际体积可能有波动,而且节点侧通常还有请求大小限制。SDK 默认的乘数是 0.95(源码常量 CHUNK_SIZE_MULTIPLIER = 0.95,见 contract-factory.ts),即 chunk 大小为潜在最大值的 95%;你可以按需调整它来确保交易能通过校验。该值必须介于 0 和 1 之间,否则会抛出 INVALID_CHUNK_SIZE_MULTIPLIER 错误:
// packages/contract/src/contract-factory.ts
if (chunkSizeMultiplier < 0 || chunkSizeMultiplier > 1) {
throw new FuelError(
ErrorCode.INVALID_CHUNK_SIZE_MULTIPLIER,
'Chunk size multiplier must be between 0 and 1'
);
}
从 getMaxChunkSize 的实现看,最大 chunk 大小的计算逻辑是:
- 取三个上限中的最小值:链上
contractMaxSize、链上maxSize(交易大小上限)、以及一个硬编码的64000字节上限; - 构造一笔携带 32 字节假数据的最小 blob 交易,量出其基础开销
blobTx.byteLength(); maxChunkSize = (sizeLimit - blobTx.byteLength() - WORD_SIZE) * chunkSizeMultiplier,最后按WORD_SIZE取整保证字节对齐。
Blob 路径的完整执行流程
阅读 deployAsBlobTx 源码 可以还原出一条比文档描述更细致的执行链路:
- 切块:
getContractChunks(bytecode, chunkSize)将字节码切成若干 chunk,每块对应一个BlobTransactionRequest,其blobId = hash(bytecode); - 生成加载器字节码:
getLoaderInstructions(blobIds)用汇编指令生成一段极短的“加载器合约”字节码,随后以这段字节码(而非原合约)发起createTransactionRequest,算出最终contractId; - 跳过已上传的 blob:通过
provider.getBlobs(uniqueBlobIds)查询链上已存在的 blob,只对未上传的部分提交 blob 交易;重复上传同一 blobId 时节点会报 “BlobId is already taken”,源码对此做了容错——该 blobId 依然有效,直接视为已上传继续; - 费用预检:在真正发交易前,SDK 会为每笔 blob 交易和 create 交易分别计算
minGas与calculateGasFee估算的最小费用,累加得到totalCost;如果钱包余额不足,抛出FUNDS_TOO_LOW("Insufficient balance to deploy contract."); - 顺序上链:
waitForResult内部依次发送每个 blob 交易并waitForResult(任一块失败即抛出TRANSACTION_FAILED),全部成功后再组装并发送 create 交易。此时才解析waitForTransactionId的 Promise,最终返回{ contract, transactionResult }。
其中加载器字节码的构造是这条路径最有意思的细节。loader-script.ts 中的 getLoaderInstructions 生成一段 12 条 FuelVM 指令的汇编,其后拼接所有 blob ID 的二进制数据。其执行语义(参考 fuels-rs 中 loader 合约的实现)分两步:
- 加载阶段:从 PC 之后找到硬编码的 blob ID 数组,循环执行
bsiz+ldc,把每个 blob 的内容顺序拷入以 SP 为起点的内存区; - 跳转阶段:执行
jmp跳到内存中刚装载好的完整合约代码起点,之后 VM 按正常合约流程执行——读取函数选择器并跳转到对应方法。
也就是说,链上真正注册的是这段几十字节的加载器合约;当你调用它时,VM 每次都会先执行这段“把 blob 拉进内存再跳转”的前置代码。这也解释了为什么 Blob 部署的合约后续调用需要 blob 数据保持可访问。
注意:使用 blob 交易部署大合约需要更长时间。每一笔 blob 交易都是相互依赖的,必须等待出块才会被打包;之后再按正常流程提交 Create 交易。因此你需要比平时等待更久,合约才能完全部署并可被调用。
DeployContractOptions 部署选项参考
deploy / deployAsCreateTx / deployAsBlobTx 三个方法都接受同一个选项类型 DeployContractOptions,定义于 contract-factory.ts:
export type DeployContractOptions = {
salt?: BytesLike; // 参与 contractId 计算,默认 32 字节随机值
storageSlots?: StorageSlot[]; // 预填充的存储槽(key/value 均为 BytesLike)
stateRoot?: BytesLike; // 默认由 storageSlots 计算得出
configurableConstants?: { [name: string]: unknown }; // 部署时写入字节码的可配置常量
chunkSizeMultiplier?: number; // 仅 blob 路径使用,默认 0.95,取值 0~1
} & CreateTransactionRequestLike; // 继承 tip、maxFee 等 Create 交易参数
几个实现层面的行为值得留意:
salt与contractId确定性:createTransactionRequest中若未传 salt,会用randomBytes(32)生成。也就是说,两次部署同一份字节码会得到不同的contractId;如需确定性地址,应显式传入 salt;storageSlots会被去重排序:传入的 slots 与构造工厂时提供的 slots 合并后,按 key 去重并排序,stateRoot未显式指定时由这些 slots 计算;configurableConstants直接改写字节码:setConfigurableConstants会把每个常量按 ABI 编码后写回字节码对应offset处(bytes.set(encoded, offset)),并在常量名不存在时抛出CONFIGURABLE_NOT_FOUND;maxFee可覆盖:assembleTx内部先经account.provider.assembleTx自动估算 gas 与费用,但若选项中显式提供了maxFee则予以保留——这一点在 contract-factory.test.ts 中有专门的测试用例验证("should not override user input maxFee when calling deploy")。
小结
fuels-ts 的合约部署可以归纳为一张决策表:
| 场景 | 推荐方法 | 交易笔数 |
|---|---|---|
字节码 ≤ contractMaxSize |
deploy(自动走 Create)或 deployAsCreateTx |
1 笔 Create |
字节码 > contractMaxSize |
deploy(自动走 Blob)或 deployAsBlobTx |
N 笔 Blob + 1 笔 Create |
核心要点:用 provider.getChain() 查询链上 contractMaxSize 判断路径;deploy 返回值中 contractId 立即可用、contract 需等 waitForResult;Blob 路径下用 chunkSizeMultiplier(0~1,默认 0.95)控制分块大小以规避节点请求体限制;多笔 blob 交易串行上链导致部署耗时更长。完整的可运行示例位于 deployment.ts 与 get-max-size.ts,核心实现在 packages/contract/src/contract-factory.ts 与 packages/contract/src/loader/loader-script.ts。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00