初始化合約存儲的完整機(jī)制與實(shí)踐)
fuels-ts 中的合約 Storage Slots部署時(shí)初始化合約存儲的完整機(jī)制與實(shí)踐【免費(fèi)下載鏈接】fuels-tsFuel Network Typescript SDK項(xiàng)目地址: https://gitcode.com/GitHub_Trending/fu/fuels-ts在 Fuel 鏈上合約的存儲狀態(tài)在部署那一刻就被凍結(jié)進(jìn)交易里——你可以通過部署選項(xiàng)storageSlots指定合約初始化的存儲槽key/value 對從而讓新部署的合約天生攜帶狀態(tài)。本文基于 fuels-ts 倉庫的官方文檔 storage-slots.md 展開講清 storage slots 的兩種指定方式從 Sway 編譯器生成的 JSON 導(dǎo)入、或在代碼中內(nèi)聯(lián)書寫、Typegen 自動生成代碼如何自動加載 storage slots并深入 ContractFactory 的源碼揭示去重、排序、state root 與 contract ID 計(jì)算這一底層鏈路。核心概念Storage Slot 是什么在 fuels-ts 中一個(gè)存儲槽被定義為 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(), }); } }即每個(gè)槽都是key: string32 字節(jié)十六進(jìn)制加value: string32 字節(jié)十六進(jìn)制的十六進(jìn)制字符串對。StorageSlotCoder基于B256Coder結(jié)構(gòu)編碼說明每個(gè)字段都嚴(yán)格是 32 字節(jié)——這與 Sway 存儲模型中每個(gè)存儲項(xiàng)占一個(gè) 256 位槽位的設(shè)計(jì)一一對應(yīng)。這些 storage slots 會作為合約部署交易Create 交易的一部分被編碼進(jìn)交易體中。從 交易編碼器 的結(jié)構(gòu)可以看到storageSlots: StorageSlot[]是交易編碼/解碼流程中的一等公民字段// packages/transactions/src/coders/transaction.ts /** List of inputs (StorageSlot[]) */ storageSlots: StorageSlot[]; // 編碼時(shí) new ArrayCoder(new StorageSlotCoder(), value.storageSlotsCount.toNumber()).encode(...) // 解碼時(shí) [decoded, o] new ArrayCoder(new StorageSlotCoder(), storageSlotsCount.toNumber()).decode(...)換句話說storage slots 不是 SDK 的裝飾而是真實(shí)寫入鏈上交易、決定合約初始存儲根storage root的鏈上數(shù)據(jù)結(jié)構(gòu)。方式一從 Sway 編譯器生成的 JSON 導(dǎo)入 storage slots官方文檔給出的第一個(gè)例子是部署合約時(shí)把 Sway 編譯器forc build生成的 storage slots 直接傳給deploy選項(xiàng)。完整示例來自倉庫中的文檔片段 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 文件內(nèi)聯(lián)進(jìn)生成代碼的靜態(tài)屬性。其來源鏈路是Sway 編譯器為每個(gè)合約生成一份*-storage_slots.json工件Typegen 在收集合約文件時(shí)把-abi.json路徑替換為-storage_slots.json并讀取內(nèi)容邏輯見 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); } });注意兩個(gè)細(xì)節(jié)只有programType為合約ProgramTypeEnum.CONTRACT時(shí)才會去收集 storage slots 文件如果某個(gè)合約沒有對應(yīng)工件則返回空集合對應(yīng)生成代碼里storageSlots為空數(shù)組。生成模板 factory.hbs 把這些內(nèi)容織入工廠類的構(gòu)造函數(shù)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 生成的工廠類在構(gòu)造ContractFactory時(shí)就已經(jīng)把storageSlots作為第四個(gè)參數(shù)傳給了基類static deploy只需傳入錢包即可復(fù)用。方式二在代碼中內(nèi)聯(lián)書寫 storage slots官方文檔的第二個(gè)例子演示了不依賴 JSON 文件、直接在部署選項(xiàng)里手寫存儲槽的用法。示例來自 override-storage-slots-inline.ts對應(yīng) Sway 側(cè)帶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 字節(jié)64 個(gè)十六進(jìn)制字符的十六進(jìn)制字符串前綴0x可有可無SDK 會統(tǒng)一處理見下文。value 是完整的 32 字節(jié)槽值例如000000000000001e...實(shí)際承載的是一個(gè)u64 30之類的整數(shù)值右側(cè)大量零是補(bǔ)位。源碼縱深部署請求如何消費(fèi) 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);這里有四個(gè)關(guān)鍵行為合并與優(yōu)先級deployOptions.storageSlots排在this.storageSlotsTypegen 工廠傳入的那份之前合并后再去重——去重規(guī)則是保留第一次出現(xiàn)的項(xiàng)findIndex(...) index因此部署時(shí)顯式傳入的槽位會覆蓋工廠內(nèi)置的同 key 槽位。規(guī)范化所有 key/value 都經(jīng)hexlifyWithPrefix統(tǒng)一為帶0x前綴的十六進(jìn)制字符串所以內(nèi)聯(lián)寫法里給不給0x都能工作。去重 排序按 key 去重后按 key 的字典序排序。排序不是可有可無的getContractStorageRoot要基于這套槽位計(jì)算合約的初始 state root而 Merkle 根的計(jì)算對輸入順序敏感排序保證了相同輸入集合得到確定性的根。ID 決定于 state rootcontractId getContractId(bytecode, salt, stateRoot)state root 又來自存儲槽。這意味著改了 storage slots 就會得到不同的 contractId——初始狀態(tài)是合約身份的一部分。若你顯式傳入stateRoot選項(xiàng)則會跳過getContractStorageRoot的自動計(jì)算。此外部署入口deploy會根據(jù)鏈上consensusParameters.contractParameters.contractMaxSize自動在deployAsCreateTx與deployAsBlobTx分塊 loader 合約之間選擇詳見 deploying-contracts.md無論哪條路徑storage slots 都走上面同一套createTransactionRequest邏輯。測試驗(yàn)證slots 確實(shí)進(jìn)入了交易倉庫的集成測試 storage-test-contract.test.ts 驗(yàn)證了這條鏈路的端到端正確性部署時(shí)傳入StorageTestContract.storageSlots來自 storage_slots.json或手動構(gòu)造自定義storageSlots數(shù)組部署隨后斷言交易結(jié)果里的槽位與傳入一致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 })以及內(nèi)聯(lián)數(shù)組的混用測試證明工廠內(nèi)置 部署選項(xiàng)覆蓋的合并語義在實(shí)際部署中被反復(fù)驗(yàn)證。Typegen 的自動加載Auto-load官方文檔最后一段指出使用 Typegen 生成的代碼會 自動加載 Storage Slots。從生成模板可以看到其實(shí)現(xiàn)方式main.hbs模板會把storageSlotsJsonString即-storage_slots.json的原始內(nèi)容缺省為[]內(nèi)聯(lián)為static readonly storageSlots靜態(tài)屬性factory.hbs再把它傳給ContractFactory構(gòu)造函數(shù)前文已展示。因此實(shí)際工程中你通常不需要手寫任何 storage slots 代碼——只要 Typegen 在構(gòu)建時(shí)能找到合約的*-storage_slots.json工件XxxFactory.deploy(wallet)就會自動帶上初始狀態(tài)只有在需要覆蓋某些槽位例如給多租戶部署注入不同參數(shù)時(shí)才需要在deploy的選項(xiàng)中顯式傳入storageSlots數(shù)組來覆蓋同 key 的默認(rèn)值。小結(jié)storage slot 是 32 字節(jié) key 32 字節(jié) value 的十六進(jìn)制對類型與編碼器見 packages/transactions/src/coders/storage-slot.ts并作為 Create 交易的編碼字段上鏈兩種指定方式deployer側(cè)傳入 Typegen 從*-storage_slots.json生成的XxxContract.storageSlots或直接在deploy({ storageSlots: [...] })中內(nèi)聯(lián)書寫ContractFactory.createTransactionRequest負(fù)責(zé)合并、hexlifyWithPrefix規(guī)范化、按 key 去重部署選項(xiàng)優(yōu)先與排序并據(jù)此計(jì)算 state root 與 contractId——初始存儲直接影響合約 IDTypegen 工廠通過構(gòu)造函數(shù)自動攜帶 storage slots實(shí)現(xiàn)零配置的初始狀態(tài)部署這一機(jī)制由 factory.hbs 模板與 collectStorageSlotsFilePaths.ts 的文件收集邏輯共同保證并有 storage-test-contract.test.ts 等集成測試佐證?!久赓M(fèi)下載鏈接】fuels-tsFuel Network Typescript SDK項(xiàng)目地址: https://gitcode.com/GitHub_Trending/fu/fuels-ts創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考