首页
/ fuels-ts 合约部署指南:ContractFactory 的 Create 与 Blob 双路径机制深度解析

fuels-ts 合约部署指南:ContractFactory 的 Create 与 Blob 双路径机制深度解析

2026-09-05 23:56:04作者:傅爽业Veleda

本篇技术指南围绕 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 buildforc 是 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,并返回三项内容:contractIdwaitForTransactionId 函数和 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 },其中 transactionResultTransactionResult<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() 立即返回 waitForResultawait 之后才能拿到 value(例如 initialize_counter(41) 后断言返回值为 41increment_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 大小的计算逻辑是:

  1. 取三个上限中的最小值:链上 contractMaxSize、链上 maxSize(交易大小上限)、以及一个硬编码的 64000 字节上限;
  2. 构造一笔携带 32 字节假数据的最小 blob 交易,量出其基础开销 blobTx.byteLength()
  3. maxChunkSize = (sizeLimit - blobTx.byteLength() - WORD_SIZE) * chunkSizeMultiplier,最后按 WORD_SIZE 取整保证字节对齐。

Blob 路径的完整执行流程

阅读 deployAsBlobTx 源码 可以还原出一条比文档描述更细致的执行链路:

  1. 切块getContractChunks(bytecode, chunkSize) 将字节码切成若干 chunk,每块对应一个 BlobTransactionRequest,其 blobId = hash(bytecode)
  2. 生成加载器字节码getLoaderInstructions(blobIds) 用汇编指令生成一段极短的“加载器合约”字节码,随后以这段字节码(而非原合约)发起 createTransactionRequest,算出最终 contractId
  3. 跳过已上传的 blob:通过 provider.getBlobs(uniqueBlobIds) 查询链上已存在的 blob,只对未上传的部分提交 blob 交易;重复上传同一 blobId 时节点会报 “BlobId is already taken”,源码对此做了容错——该 blobId 依然有效,直接视为已上传继续;
  4. 费用预检:在真正发交易前,SDK 会为每笔 blob 交易和 create 交易分别计算 minGascalculateGasFee 估算的最小费用,累加得到 totalCost;如果钱包余额不足,抛出 FUNDS_TOO_LOW("Insufficient balance to deploy contract.");
  5. 顺序上链waitForResult 内部依次发送每个 blob 交易并 waitForResult(任一块失败即抛出 TRANSACTION_FAILED),全部成功后再组装并发送 create 交易。此时才解析 waitForTransactionId 的 Promise,最终返回 { contract, transactionResult }

其中加载器字节码的构造是这条路径最有意思的细节。loader-script.ts 中的 getLoaderInstructions 生成一段 12 条 FuelVM 指令的汇编,其后拼接所有 blob ID 的二进制数据。其执行语义(参考 fuels-rs 中 loader 合约的实现)分两步:

  1. 加载阶段:从 PC 之后找到硬编码的 blob ID 数组,循环执行 bsiz + ldc,把每个 blob 的内容顺序拷入以 SP 为起点的内存区;
  2. 跳转阶段:执行 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 交易参数

几个实现层面的行为值得留意:

  • saltcontractId 确定性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.tsget-max-size.ts,核心实现在 packages/contract/src/contract-factory.tspackages/contract/src/loader/loader-script.ts

登录后查看全文
热门项目推荐
相关项目推荐