ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

fuels-ts 中的合约 Storage Slots:部署时初始化合约存储的完整机制与实践

2026/9/6 20:52:44 拓冰建站 浏览量
fuels-ts 中的合约 Storage Slots:部署时初始化合约存储的完整机制与实践 fuels-ts 中的合约 Storage Slots部署时初始化合约存储的完整机制与实践【免费下载链接】fuels-tsFuel Network Typescript SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-ts在 Fuel 链上合约的存储状态在部署那一刻就被冻结进交易里——你可以通过部署选项storageSlots指定合约初始化的存储槽key/value 对从而让新部署的合约天生携带状态。本文基于 fuels-ts 仓库的官方文档 storage-slots.md 展开讲清 storage slots 的两种指定方式从 Sway 编译器生成的 JSON 导入、或在代码中内联书写、Typegen 自动生成代码如何自动加载 storage slots并深入 ContractFactory 的源码揭示去重、排序、state root 与 contract ID 计算这一底层链路。核心概念Storage Slot 是什么在 fuels-ts 中一个存储槽被定义为 256 位的键与 256 位的值其类型声明位于 packages/transactions/src/coders/storage-slot.tsexport type StorageSlot { /** Key (b256) */ key: string; /** Value (b256) */ value: string; }; export class StorageSlotCoder extends StructCoder{ key: B256Coder; value: B256Coder; } { constructor() { super(StorageSlot, { key: new B256Coder(), value: new B256Coder(), }); } }即每个槽都是key: string32 字节十六进制加value: string32 字节十六进制的十六进制字符串对。StorageSlotCoder基于B256Coder结构编码说明每个字段都严格是 32 字节——这与 Sway 存储模型中每个存储项占一个 256 位槽位的设计一一对应。这些 storage slots 会作为合约部署交易Create 交易的一部分被编码进交易体中。从 交易编码器 的结构可以看到storageSlots: StorageSlot[]是交易编码/解码流程中的一等公民字段// packages/transactions/src/coders/transaction.ts /** List of inputs (StorageSlot[]) */ storageSlots: StorageSlot[]; // 编码时 new ArrayCoder(new StorageSlotCoder(), value.storageSlotsCount.toNumber()).encode(...) // 解码时 [decoded, o] new ArrayCoder(new StorageSlotCoder(), storageSlotsCount.toNumber()).decode(...)换句话说storage slots 不是 SDK 的装饰而是真实写入链上交易、决定合约初始存储根storage root的链上数据结构。方式一从 Sway 编译器生成的 JSON 导入 storage slots官方文档给出的第一个例子是部署合约时把 Sway 编译器forc build生成的 storage slots 直接传给deploy选项。完整示例来自仓库中的文档片段 override-storage-slots.tsimport { Provider, Wallet } from fuels; import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from ../../../../env; import { StorageTestContract, StorageTestContractFactory, } from ../../../../typegend; const provider new Provider(LOCAL_NETWORK_URL); const deployer Wallet.fromPrivateKey(WALLET_PVT_KEY, provider); const deploymentTx await StorageTestContractFactory.deploy(deployer, { storageSlots: StorageTestContract.storageSlots, }); await deploymentTx.waitForResult();这里的StorageTestContract.storageSlots就是 Typegen 从 Sway 编译器输出的 JSON 文件内联进生成代码的静态属性。其来源链路是Sway 编译器为每个合约生成一份*-storage_slots.json工件Typegen 在收集合约文件时把-abi.json路径替换为-storage_slots.json并读取内容逻辑见 collectStorageSlotsFilePaths.tsfilepaths.forEach((abiFilepath) { const storageSlotsFilepath abiFilepath.replace(-abi.json, -storage_slots.json); const storageSlotsExists existsSync(storageSlotsFilepath); if (storageSlotsExists) { const storageSlots: IFile { path: storageSlotsFilepath, contents: readFileSync(storageSlotsFilepath, utf-8), }; storageSlotsFiles.push(storageSlots); } });注意两个细节只有programType为合约ProgramTypeEnum.CONTRACT时才会去收集 storage slots 文件如果某个合约没有对应工件则返回空集合对应生成代码里storageSlots为空数组。生成模板 factory.hbs 把这些内容织入工厂类的构造函数export class {{capitalizedName}}Factory extends __ContractFactory{{capitalizedName}} { static readonly bytecode bytecode; constructor(accountOrProvider: Account | Provider) { super( bytecode, {{capitalizedName}}.abi, accountOrProvider, {{capitalizedName}}.storageSlots ); } static deploy (wallet: Account, options: DeployContractOptions {}) { const factory new {{capitalizedName}}Factory(wallet); return factory.deploy(options); } }也就是说Typegen 生成的工厂类在构造ContractFactory时就已经把storageSlots作为第四个参数传给了基类static deploy只需传入钱包即可复用。方式二在代码中内联书写 storage slots官方文档的第二个例子演示了不依赖 JSON 文件、直接在部署选项里手写存储槽的用法。示例来自 override-storage-slots-inline.ts对应 Sway 侧带storage声明的测试合约storage-test-contractimport { Provider, Wallet } from fuels; import { StorageTestContractFactory } from ../../../../typegend; const provider new Provider(LOCAL_NETWORK_URL); const deployer Wallet.fromPrivateKey(WALLET_PVT_KEY, provider); const deploymentTx await StorageTestContractFactory.deploy(deployer, { storageSlots: [ { key: 02dac99c283f16bc91b74f6942db7f012699a2ad51272b15207b9cc14a70dbae, value: 0000000000000001000000000000000000000000000000000000000000000000, }, { key: 6294951dcb0a9111a517be5cf4785670ff4e166fb5ab9c33b17e6881b48e964f, value: 0000000000000001000000000000003200000000000000000000000000000000, }, { key: b48b753af346966d0d169c0b2e3234611f65d5cfdb57c7b6e7cd6ca93707bee0, value: 000000000000001e000000000000000000000000000000000000000000000000, }, { key: de9090cb50e71c2588c773487d1da7066d0c719849a7e58dc8b6397a25c567c0, value: 0000000000000014000000000000000000000000000000000000000000000000, }, { key: f383b0ce51358be57daa3b725fe44acdb2d880604e367199080b4379c41bb6ed, value: 000000000000000a000000000000000000000000000000000000000000000000, }, ], }); await deploymentTx.waitForResult();注意这里的key/value格式要求两者都必须是 32 字节64 个十六进制字符的十六进制字符串前缀0x可有可无SDK 会统一处理见下文。value 是完整的 32 字节槽值例如000000000000001e...实际承载的是一个u64 30之类的整数值右侧大量零是补位。源码纵深部署请求如何消费 storageSlots两种写法最终都汇入ContractFactory.createTransactionRequest。contract-factory.ts 中的处理逻辑值得逐行理解createTransactionRequest(deployOptions?: DeployContractOptions { bytecode?: BytesLike }) { const storageSlots (deployOptions?.storageSlots ?? []) .concat(this.storageSlots) .map(({ key, value }) ({ key: hexlifyWithPrefix(key), value: hexlifyWithPrefix(value), })) .filter((el, index, self) self.findIndex((s) s.key el.key) index) .sort(({ key: keyA }, { key: keyB }) keyA.localeCompare(keyB)); const options { salt: randomBytes(32), ...(deployOptions ?? {}), storageSlots, }; // ... const bytecode deployOptions?.bytecode || this.bytecode; const stateRoot options.stateRoot || getContractStorageRoot(options.storageSlots); const contractId getContractId(bytecode, options.salt, stateRoot);这里有四个关键行为合并与优先级deployOptions.storageSlots排在this.storageSlotsTypegen 工厂传入的那份之前合并后再去重——去重规则是保留第一次出现的项findIndex(...) index因此部署时显式传入的槽位会覆盖工厂内置的同 key 槽位。规范化所有 key/value 都经hexlifyWithPrefix统一为带0x前缀的十六进制字符串所以内联写法里给不给0x都能工作。去重 排序按 key 去重后按 key 的字典序排序。排序不是可有可无的getContractStorageRoot要基于这套槽位计算合约的初始 state root而 Merkle 根的计算对输入顺序敏感排序保证了相同输入集合得到确定性的根。ID 决定于 state rootcontractId getContractId(bytecode, salt, stateRoot)state root 又来自存储槽。这意味着改了 storage slots 就会得到不同的 contractId——初始状态是合约身份的一部分。若你显式传入stateRoot选项则会跳过getContractStorageRoot的自动计算。此外部署入口deploy会根据链上consensusParameters.contractParameters.contractMaxSize自动在deployAsCreateTx与deployAsBlobTx分块 loader 合约之间选择详见 deploying-contracts.md无论哪条路径storage slots 都走上面同一套createTransactionRequest逻辑。测试验证slots 确实进入了交易仓库的集成测试 storage-test-contract.test.ts 验证了这条链路的端到端正确性部署时传入StorageTestContract.storageSlots来自 storage_slots.json或手动构造自定义storageSlots数组部署随后断言交易结果里的槽位与传入一致const { waitForResult: waitForDeploy } await factory.deploy({ storageSlots }); // ... expect(transactionResultConstructor.transaction.storageSlots).toEqual(expectedStorageSlots); expect(transactionResultStatically.transaction.storageSlots).toEqual(expectedStorageSlots);contract-factory.test.ts 中也存在同一模式deploy({ storageSlots: StorageTestContract.storageSlots })以及内联数组的混用测试证明工厂内置 部署选项覆盖的合并语义在实际部署中被反复验证。Typegen 的自动加载Auto-load官方文档最后一段指出使用 Typegen 生成的代码会 自动加载 Storage Slots。从生成模板可以看到其实现方式main.hbs模板会把storageSlotsJsonString即-storage_slots.json的原始内容缺省为[]内联为static readonly storageSlots静态属性factory.hbs再把它传给ContractFactory构造函数前文已展示。因此实际工程中你通常不需要手写任何 storage slots 代码——只要 Typegen 在构建时能找到合约的*-storage_slots.json工件XxxFactory.deploy(wallet)就会自动带上初始状态只有在需要覆盖某些槽位例如给多租户部署注入不同参数时才需要在deploy的选项中显式传入storageSlots数组来覆盖同 key 的默认值。小结storage slot 是 32 字节 key 32 字节 value 的十六进制对类型与编码器见 packages/transactions/src/coders/storage-slot.ts并作为 Create 交易的编码字段上链两种指定方式deployer侧传入 Typegen 从*-storage_slots.json生成的XxxContract.storageSlots或直接在deploy({ storageSlots: [...] })中内联书写ContractFactory.createTransactionRequest负责合并、hexlifyWithPrefix规范化、按 key 去重部署选项优先与排序并据此计算 state root 与 contractId——初始存储直接影响合约 IDTypegen 工厂通过构造函数自动携带 storage slots实现零配置的初始状态部署这一机制由 factory.hbs 模板与 collectStorageSlotsFilePaths.ts 的文件收集逻辑共同保证并有 storage-test-contract.test.ts 等集成测试佐证。【免费下载链接】fuels-tsFuel Network Typescript SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-ts创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考