fuels-ts 实战指南:在部署 Sway 合约时设置 Configurable Constants(可配置常量)
本篇基于 fuels-ts 文档《Configurable Constants》展开,讲解 Fuel SDK 中 Sway 合约"可配置常量"(configurable constants)的完整用法:如何在合约中用 configurable 块声明带默认值的常量,如何在部署时通过 configurableConstants 选项按需覆盖其中任意常量,以及配置不完整(例如 Struct 缺字段)时的报错行为。读完本文,你将掌握可配置常量的声明、覆盖与验证全流程,并理解 SDK 在底层"改写字节码"的实现原理。
一、什么是 Configurable Constants
Sway 提供了强大的可配置常量特性:在创建合约时,可以定义一批常量并为每个常量指定默认值;在合约部署之前,你可以重新定义这些常量的值——可以只改其中一部分,也可以全部覆盖。
这一特性为动态的合约环境提供了灵活性:同一份合约代码可以在不同环境下以不同的常量配置部署,实现高定制化,从而编写出更高效、更易适应不同场景的智能合约。
二、在 Sway 合约中声明可配置常量
下面是一个声明了四个可配置常量的示例合约(来自仓库文档配套 Sway 工程 echo-configurables):
contract;
enum MyEnum {
Checked: (),
Pending: (),
}
struct MyStruct {
x: u8,
y: u8,
state: MyEnum,
}
configurable {
age: u8 = 25,
tag: str[4] = __to_str_array("fuel"),
grades: [u8; 4] = [3, 4, 3, 2],
my_struct: MyStruct = MyStruct {
x: 1,
y: 2,
state: MyEnum::Pending,
},
}
abi EchoConfigurables {
fn echo_configurables() -> (u8, str[4], [u8; 4], MyStruct);
}
impl EchoConfigurables for Contract {
fn echo_configurables() -> (u8, str[4], [u8; 4], MyStruct) {
(age, tag, grades, my_struct)
}
}
该合约中,echo_configurables 函数会返回四个可配置常量的当前值,供我们用它来演示通过 SDK 设置常量配置。示例覆盖了多种典型类型:无符号整数(u8)、定长字符串(str[4])、固定长度数组([u8; 4])以及嵌套了枚举的 Struct。
三、部署时为新值覆盖常量
在合约部署阶段,可以为任意一个或全部可配置常量指定新值。下面的示例(对应 文档代码片段)只覆盖了 age 一个常量,其余常量保持 Sway 中定义的默认值:
import { Provider, Wallet } from 'fuels';
import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../env';
import { EchoConfigurablesFactory } from '../../../typegend';
const provider = new Provider(LOCAL_NETWORK_URL);
const wallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);
const configurableConstants = {
age: 10,
};
const deploy = await EchoConfigurablesFactory.deploy(wallet, {
configurableConstants,
});
const { contract } = await deploy.waitForResult();
const {
value: [age, tag, grades, myStruct],
} = await contract.functions.echo_configurables().get();
// age got updated
console.log('age', age); // 10
// while the rest are default values
console.log('tag', tag); // 'fuel'
console.log('grades', grades); // [3, 4, 3, 2]
console.log('myStruct', myStruct); // { x: 1, y: 2, state: 'Pending' }
要点说明:
EchoConfigurablesFactory由 fuels-ts 的类型生成(typegen)流程基于合约 ABI 生成,deploy方法接收的第二个参数即 DeployContractOptions;configurableConstants是一个以"常量名"为键的对象,类型签名为{ [name: string]: unknown },只需给出你想覆盖的常量,未提及的常量自动沿用 Sway 源码中的默认值;- 调用
deploy(wallet, { configurableConstants })后等待交易结果拿到contract实例,再通过contract.functions.echo_configurables().get()验证:age变为 10,而tag、grades、myStruct仍为'fuel'、[3, 4, 3, 2]、{ x: 1, y: 2, state: 'Pending' }。
四、Struct 常量必须完整配置,否则部署报错
文档特别强调:为 Struct 类型常量赋新值时,必须定义该 Struct 的全部属性,否则会抛出错误。仓库文档片段中给出了反例:
const invalidConfigurables = {
my_struct: {
x: 10,
},
};
try {
await EchoConfigurablesFactory.deploy(wallet, {
configurableConstants: invalidConfigurables,
});
} catch (e) {
console.log('error', e);
// error: Error setting configurable constants on contract:
// Invalid struct MyStruct. Field "y" not present.
}
只写了 x: 10 而遗漏了 y 和 state 字段,deploy 会同步抛出 Error setting configurable constants on contract: Invalid struct MyStruct. Field "y" not present.。该错误信息恰好对应 ContractFactory.setConfigurableConstants 中统一的错误包装逻辑(见下文原理分析)。
五、源码级原理:常量值是如何"写进"合约的
从源码结构看,可配置常量的本质是在部署交易发出之前,把编码后的常量值直接覆写到合约字节码的固定偏移位置,即对字节码做"打补丁"。关键调用链如下:
-
入口:
ContractFactory.deploy→deployAsCreateTx→prepareDeploy。在 prepareDeploy 中,只要deployOptions.configurableConstants存在,就会先调用this.setConfigurableConstants(configurableConstants),之后才创建交易请求。由于合约 ID(contractId)是在字节码改写之后基于bytecode + salt + stateRoot计算的(见 createTransactionRequest 中getContractId(bytecode, options.salt, stateRoot)调用),可以推断:不同configurableConstants配置会产生不同的字节码与不同的合约 ID。 -
核心逻辑:setConfigurableConstants 逐条处理用户传入的键值对:
- 先校验合约 ABI 中确实声明了
configurables,否则抛出Contract does not have configurables to be set; - 再校验每个键都在
this.interface.configurables中存在,否则抛出Contract does not have a configurable named: '${key}'; - 随后通过 Interface.encodeConfigurable 按 ABI 中的
configurableType将 JS 值编码为字节序列,并从this.interface.configurables[key].offset取出该常量在字节码中的偏移地址,执行bytes.set(encoded, offset)完成覆写,最后把改写后的字节序列回写到this.bytecode; - 所有异常都会被捕获并统一包装为
INVALID_CONFIGURABLE_CONSTANTS错误,消息前缀即文档示例中看到的Error setting configurable constants on contract: ...,Struct 缺字段时内部抛出Invalid struct MyStruct. Field "y" not present.后同样走这条包装路径。
- 先校验合约 ABI 中确实声明了
-
ABI 侧支撑:Interface 构造函数 在初始化时就把 JSON ABI 中的
configurables数组转成以名称为键的映射,每条记录包含常量名、类型与offset,这正是部署时能"定位到字节码哪一段"的依据。 -
Blob 分片部署同样支持:当合约超过链上
contractMaxSize限制时,deploy会自动走 deployAsBlobTx 分片路径;该方法在分块之前同样会调用setConfigurableConstants,因此无论走 Create 交易还是 Blob 分片部署,configurableConstants都能生效。
六、测试验证:SDK 支持的可配置常量类型
仓库集成测试 configurable-contract.test.ts 用 ConfigurableContractFactory 系统性地验证了各类型常量的默认值断言与覆盖能力,可作为"哪些类型可以安全配置"的权威参考。测试中定义的默认值与覆盖用例涵盖:
| 类型 | 默认值 | 覆盖值示例 |
|---|---|---|
U8 / U16 / U32 / U64 |
10 / 301 / 799 / 100000 | 99 / 499 / 854 / 999999 |
BOOL |
true | false |
B256 |
0x1d6ebd57... |
随机 256 位值 |
ENUM |
'red' |
'blue'(以字符串传枚举变体名) |
ARRAY(二维数组) |
[[253,254],[255,256]] |
[[666,667],[656,657]] |
STR_4(定长字符串) |
'fuel' |
'leuf' |
TUPLE |
[12, false, 'hi'] |
[99, true, 'by'] |
STRUCT_1 |
{ tag:'000', age:21, scores:[1,3,4] } |
{ tag:'007', age:30, scores:[10,10,10] } |
测试的部署方式值得注意:它并没有手动 deploy,而是使用 fuels/test-utils 提供的 launchTestNode,通过 contractsConfigs 参数把工厂与选项一并交给测试节点:
function setupContract(configurableConstants?: { [name: string]: unknown }) {
return launchTestNode({
contractsConfigs: [
{
factory: ConfigurableContractFactory,
options: { configurableConstants },
},
],
});
}
这说明 configurableConstants 作为 DeployContractOptions 的一部分,同样适用于测试节点批量部署场景。该测试文件同时标注了 @group node 与 @group browser,即同一套用法在 Node 与浏览器环境下均已验证。此外,仓库中还存在 predicate-configurables.test.ts 等用例,表明该机制不只服务于合约部署。
七、一个真实应用场景:SDK CLI 部署代理合约
fuels-ts 的 CLI 部署命令内部就依赖了 configurableConstants。在 deployContracts.ts 中,部署 SR-C14 兼容的代理合约(Proxy Contract)时,SDK 会把目标合约 ID 与部署者地址写入代理合约的两个可配置常量:
const proxyDeployConfig: DeployContractOptions = {
...commonDeployConfig,
storageSlots: mergedStorageSlots,
configurableConstants: {
INITIAL_TARGET: { bits: targetContract.id.toB256() },
INITIAL_OWNER: { Initialized: { Address: { bits: wallet.address.toB256() } } },
},
};
示例体现了两个实践细节:b256 类常量需以 { bits: ... } 的包装结构传入,枚举型常量则使用 { 变体名: { ... } } 的 tagged union 结构——这与 AbiCoder 的编码规则保持一致。
八、小结与注意事项
- 只能覆盖、不能新增:
configurableConstants的键必须存在于 ABI 声明的configurables中,且合约必须至少声明一个可配置常量,否则 SDK 直接抛错; - Struct 必须完整:覆盖 Struct 常量时缺一不可字段,否则报错
Invalid struct Xxx. Field "yyy" not present.; - 时机是部署时:从源码实现看,常量覆盖发生在
deploy创建交易请求之前,属于"部署时一次性写入字节码"的语义,并非部署后可通过链上调用的存储写入; - 影响合约 ID:常量值被覆写进字节码后才计算 contractId,因此同一份合约代码配不同的
configurableConstants,得到的合约 ID 不同; - 全类型支持:u8/u16/u32/u64、bool、b256、enum、数组、定长字符串、元组、struct 等类型均有集成测试覆盖,可放心使用。
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 StartedRust0624
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