diff --git a/.changeset/tough-laws-whisper.md b/.changeset/tough-laws-whisper.md new file mode 100644 index 0000000000..07e21a9a31 --- /dev/null +++ b/.changeset/tough-laws-whisper.md @@ -0,0 +1,6 @@ +--- +"@latticexyz/store": patch +"@latticexyz/world": patch +--- + +Adds an experimental feature to automatically generate Solidity libraries from systems, making it easier to perform calls between systems. diff --git a/docs/pages/world/systems.mdx b/docs/pages/world/systems.mdx index 5cd3cde5ea..d4d359bdcd 100644 --- a/docs/pages/world/systems.mdx +++ b/docs/pages/world/systems.mdx @@ -227,6 +227,43 @@ If your `System` needs run both from the root namespace and from other namespace +#### System Libraries + + + This is an experimental feature and is not recommended for production use yet. Its API is expected to change in the + future. + + +By setting the `generateSystemLibraries` codegen setting to true in your MUD config, the `worldgen` CLI will automatically generate a Solidity library for each system in your project. This feature simplifies inter-system communication by providing a convenient way of calling functions between systems. + +Each generated library defines and exports a custom user-defined type that includes all the functions defined in the corresponding system. Internally, these system libraries utilize the World's `call` and `callFrom` methods to execute the actual system contracts. + +```solidity filename="MySystem.sol" +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +import { System } from "@latticexyz/world/src/System.sol"; + +// Library generated for MoveSystem, +import { moveSystem } from "./codegen/systems/MoveSystemLib.sol"; + +contract MySystem is System { + function someAction(uint256 x, uint256 y) external { + // All functions defined in the MoveSystem system are available globally for the exported `moveSystem` value + moveSystem.move(x, y); + + // You can also use delegations (if they exist) by using `callFrom` + moveSystem.callFrom(_msgSender()).move(x, y); + + // If MySystem is a root system, you need to use `callAsRoot` + moveSystem.callAsRoot().move(x, y); + + // If MySystem is a root system, it can call other systems on behalf of any address with `callAsRootFrom` + moveSystem.callAsRootFrom(_msgSender()).move(x, y); + } +} +``` + ## Registering systems For a `System` to be callable from a `World` it has to be [registered](https://github.com/latticexyz/mud/blob/main/packages/world/src/modules/init/implementations/WorldRegistrationSystem.sol#L115-L178). diff --git a/packages/store/ts/codegen/tablegen.ts b/packages/store/ts/codegen/tablegen.ts index d69ed131f5..ce1008ebb7 100644 --- a/packages/store/ts/codegen/tablegen.ts +++ b/packages/store/ts/codegen/tablegen.ts @@ -8,6 +8,7 @@ import { uniqueBy } from "@latticexyz/common/utils"; import { getUserTypes } from "./getUserTypes"; import { getUserTypesFilename } from "./getUserTypesFilename"; import { getTableOptions } from "./getTableOptions"; +import debug from "debug"; export type TablegenOptions = { /** @@ -65,4 +66,6 @@ export async function tablegen({ rootDir, config }: TablegenOptions) { } }), ); + + debug("Generated tables"); } diff --git a/packages/world/ts/config/v2/defaults.ts b/packages/world/ts/config/v2/defaults.ts index 9cff871f29..61fd8d7a95 100644 --- a/packages/world/ts/config/v2/defaults.ts +++ b/packages/world/ts/config/v2/defaults.ts @@ -26,6 +26,8 @@ export type MODULE_DEFAULTS = typeof MODULE_DEFAULTS; export const CODEGEN_DEFAULTS = { worldInterfaceName: "IWorld", worldgenDirectory: "world", + systemLibrariesDirectory: "systems", + generateSystemLibraries: false, worldImportPath: "@latticexyz/world/src", } as const satisfies CodegenInput; diff --git a/packages/world/ts/config/v2/output.ts b/packages/world/ts/config/v2/output.ts index 8bc9359b7f..c448113487 100644 --- a/packages/world/ts/config/v2/output.ts +++ b/packages/world/ts/config/v2/output.ts @@ -113,6 +113,10 @@ export type Codegen = { readonly worldInterfaceName: string; /** Directory to output system and world interfaces of `worldgen` (Default "world") */ readonly worldgenDirectory: string; + /** Directory to output system libraries (Default "libraries") */ + readonly systemLibrariesDirectory: string; + /** Generate libraries for each system (Default false) */ + readonly generateSystemLibraries: boolean; /** * @internal * Absolute import path for a package import or starting with `.` for an import relative to project root dir. diff --git a/packages/world/ts/config/v2/world.test.ts b/packages/world/ts/config/v2/world.test.ts index 09f546966a..02a67466e7 100644 --- a/packages/world/ts/config/v2/world.test.ts +++ b/packages/world/ts/config/v2/world.test.ts @@ -143,8 +143,8 @@ describe("defineWorld", () => { Example: { label: "Example", type: "table", - namespaceLabel: "", namespace: "", + namespaceLabel: "", name: "Example", tableId: "0x746200000000000000000000000000004578616d706c65000000000000000000", schema: { @@ -164,8 +164,8 @@ describe("defineWorld", () => { Example: { label: "Example", type: "table", - namespaceLabel: "", namespace: "", + namespaceLabel: "", name: "Example", tableId: "0x746200000000000000000000000000004578616d706c65000000000000000000", schema: { @@ -189,6 +189,8 @@ describe("defineWorld", () => { indexFilename: "index.sol", worldInterfaceName: "IWorld", worldgenDirectory: "world", + systemLibrariesDirectory: "systems", + generateSystemLibraries: false, worldImportPath: "@latticexyz/world/src", }, systems: {}, @@ -242,6 +244,8 @@ describe("defineWorld", () => { } & { readonly worldInterfaceName: "IWorld" readonly worldgenDirectory: "world" + readonly systemLibrariesDirectory: "systems" + readonly generateSystemLibraries: false readonly worldImportPath: "@latticexyz/world/src" } readonly sourceDirectory: "src" @@ -326,8 +330,8 @@ describe("defineWorld", () => { Example: { label: "Example", type: "table", - namespaceLabel: "root", namespace: "", + namespaceLabel: "root", name: "Example", tableId: "0x746200000000000000000000000000004578616d706c65000000000000000000", schema: { @@ -347,8 +351,8 @@ describe("defineWorld", () => { root__Example: { label: "Example", type: "table", - namespaceLabel: "root", namespace: "", + namespaceLabel: "root", name: "Example", tableId: "0x746200000000000000000000000000004578616d706c65000000000000000000", schema: { @@ -372,6 +376,8 @@ describe("defineWorld", () => { indexFilename: "index.sol", worldInterfaceName: "IWorld", worldgenDirectory: "world", + systemLibrariesDirectory: "systems", + generateSystemLibraries: false, worldImportPath: "@latticexyz/world/src", }, systems: {}, @@ -425,6 +431,8 @@ describe("defineWorld", () => { } & { readonly worldInterfaceName: "IWorld" readonly worldgenDirectory: "world" + readonly systemLibrariesDirectory: "systems" + readonly generateSystemLibraries: false readonly worldImportPath: "@latticexyz/world/src" } readonly sourceDirectory: "src" diff --git a/packages/world/ts/node/render-solidity/index.ts b/packages/world/ts/node/render-solidity/index.ts index 67ab048d2d..d7368e472d 100644 --- a/packages/world/ts/node/render-solidity/index.ts +++ b/packages/world/ts/node/render-solidity/index.ts @@ -1,4 +1,5 @@ export * from "./renderSystemInterface"; +export * from "./renderSystemLibrary"; export * from "./renderWorldInterface"; export * from "./types"; export * from "./worldgen"; diff --git a/packages/world/ts/node/render-solidity/renderSystemLibrary.ts b/packages/world/ts/node/render-solidity/renderSystemLibrary.ts new file mode 100644 index 0000000000..85f4925a86 --- /dev/null +++ b/packages/world/ts/node/render-solidity/renderSystemLibrary.ts @@ -0,0 +1,270 @@ +import { + renderArguments, + renderList, + renderedSolidityHeader, + renderImports, + ContractInterfaceFunction, +} from "@latticexyz/common/codegen"; +import { RenderSystemLibraryOptions } from "./types"; +import { ContractInterfaceError } from "@latticexyz/common/codegen"; + +export function renderSystemLibrary(options: RenderSystemLibraryOptions) { + const { + imports: systemImports, + libraryName, + systemLabel, + systemName, + namespace, + resourceId, + functions, + errors: systemErrors, + worldImportPath, + storeImportPath, + } = options; + + // Add required imports, if they are already included they will get removed in renderImports + const imports = [ + ...systemImports, + { + symbol: "revertWithBytes", + path: `${worldImportPath}/revertWithBytes.sol`, + }, + { + symbol: "IWorldCall", + path: `${worldImportPath}/IWorldKernel.sol`, + }, + { + symbol: "SystemCall", + path: `${worldImportPath}/SystemCall.sol`, + }, + { + symbol: "Systems", + path: `${worldImportPath}/codegen/tables/Systems.sol`, + }, + { + symbol: "ResourceId", + path: `${storeImportPath}/ResourceId.sol`, + }, + { + symbol: "StoreSwitch", + path: `${storeImportPath}/StoreSwitch.sol`, + }, + ]; + + const callingFromRootSystemErrorName = `${libraryName}_CallingFromRootSystem`; + const errors = [{ name: callingFromRootSystemErrorName, parameters: [] }, ...systemErrors]; + + const camelCaseSystemLabel = systemLabel.charAt(0).toLowerCase() + systemLabel.slice(1); + const userTypeName = `${systemLabel}Type`; + + return ` + ${renderedSolidityHeader} + + ${renderImports(imports)} + + type ${userTypeName} is bytes32; + + // equivalent to WorldResourceIdLib.encode({ typeId: RESOURCE_SYSTEM, namespace: "${namespace}", name: "${systemName}" })) + ${userTypeName} constant ${camelCaseSystemLabel} = ${userTypeName}.wrap(${resourceId}); + + struct CallWrapper { + ResourceId systemId; + address from; + } + + struct RootCallWrapper { + ResourceId systemId; + address from; + } + + /** + * @title ${libraryName} + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @dev This library is automatically generated from the corresponding system contract. Do not edit manually. + */ + library ${libraryName} { + ${renderErrors(errors)} + + ${renderList(functions, (contractFunction) => renderUserTypeFunction(contractFunction, userTypeName))} + + ${renderList(functions, (contractFunction) => renderCallWrapperFunction(contractFunction, systemLabel, callingFromRootSystemErrorName))} + + ${renderList(functions, (contractFunction) => renderRootCallWrapperFunction(contractFunction, systemLabel, namespace))} + + function callFrom(${userTypeName} self, address from) internal pure returns (CallWrapper memory) { + return CallWrapper(self.toResourceId(), from); + } + + function callAsRoot(${userTypeName} self) internal view returns (RootCallWrapper memory) { + return RootCallWrapper(self.toResourceId(), msg.sender); + } + + function callAsRootFrom(${userTypeName} self, address from) internal pure returns (RootCallWrapper memory) { + return RootCallWrapper(self.toResourceId(), from); + } + + function toResourceId(${userTypeName} self) internal pure returns (ResourceId) { + return ResourceId.wrap(${userTypeName}.unwrap(self)); + } + + function fromResourceId(ResourceId resourceId) internal pure returns (${userTypeName}) { + return ${userTypeName}.wrap(resourceId.unwrap()); + } + + function getAddress(${userTypeName} self) internal view returns(address) { + return Systems.getSystem(self.toResourceId()); + } + + function _world() private view returns (IWorldCall) { + return IWorldCall(StoreSwitch.getStoreAddress()); + } + } + + using ${libraryName} for ${userTypeName} global; + using ${libraryName} for CallWrapper global; + using ${libraryName} for RootCallWrapper global; + `; +} + +function renderErrors(errors: ContractInterfaceError[]) { + return renderList(errors, ({ name, parameters }) => ` error ${name}(${renderArguments(parameters)});`); +} + +function renderUserTypeFunction(contractFunction: ContractInterfaceFunction, userTypeName: string) { + const { name, parameters, stateMutability, returnParameters } = contractFunction; + + const allParameters = [`${userTypeName} self`, ...parameters]; + + const functionSignature = ` + function ${name}( + ${renderArguments(allParameters)} + ) internal + ${stateMutability === "pure" ? "view" : stateMutability} + ${renderReturnParameters(returnParameters)} + `; + + const callWrapperArguments = parameters.map((param) => param.split(" ").slice(-1)[0]).join(", "); + + return ` + ${functionSignature} { + return CallWrapper(self.toResourceId(), address(0)).${name}(${callWrapperArguments}); + } + `; +} + +function renderCallWrapperFunction( + contractFunction: ContractInterfaceFunction, + systemLabel: string, + callingFromRootSystemErrorName: string, +) { + const { name, parameters, stateMutability, returnParameters } = contractFunction; + + const functionArguments = [`CallWrapper memory self`, ...parameters]; + + const functionSignature = ` + function ${name}( + ${renderArguments(functionArguments)} + ) internal + ${stateMutability === "pure" ? "view" : stateMutability} + ${renderReturnParameters(returnParameters)} + `; + + const rootSystemCheck = ` + // if the contract calling this function is a root system, it should use \`callAsRoot\` + if (address(_world()) == address(this)) revert ${callingFromRootSystemErrorName}(); + `; + + const encodedSystemCall = renderEncodeSystemCall(systemLabel, name, parameters); + + if (stateMutability === "") { + return ` + ${functionSignature} { + ${rootSystemCheck} + bytes memory systemCall = ${encodedSystemCall}; + ${renderAbiDecode( + "self.from == address(0) ? _world().call(self.systemId, systemCall) : _world().callFrom(self.from, self.systemId, systemCall)", + returnParameters, + )} + } + `; + } else { + return ` + ${functionSignature} { + ${rootSystemCheck} + bytes memory systemCall = ${encodedSystemCall}; + bytes memory worldCall = self.from == address(0) + ? abi.encodeCall(IWorldCall.call, (self.systemId, systemCall)) + : abi.encodeCall(IWorldCall.callFrom, (self.from, self.systemId, systemCall)); + (bool success, bytes memory returnData) = address(_world()).staticcall(worldCall); + if (!success) revertWithBytes(returnData); + ${renderAbiDecode("abi.decode(returnData, (bytes))", returnParameters)} + } + `; + } +} + +function renderRootCallWrapperFunction( + contractFunction: ContractInterfaceFunction, + systemLabel: string, + namespace: string, +) { + const { name, parameters, stateMutability, returnParameters } = contractFunction; + + // Staticcalls are not supported between root systems yet, due to the additional + // complexity of checking that we are in a staticall context at runtime + if (namespace === "" && stateMutability != "") { + return ""; + } + + const functionArguments = ["RootCallWrapper memory self", ...parameters]; + + const functionSignature = ` + function ${name}( + ${renderArguments(functionArguments)} + ) internal + ${stateMutability === "pure" ? "view" : stateMutability} + ${renderReturnParameters(returnParameters)} + `; + + const encodedSystemCall = renderEncodeSystemCall(systemLabel, name, parameters); + + if (stateMutability === "") { + return ` + ${functionSignature} { + bytes memory systemCall = ${encodedSystemCall}; + ${renderAbiDecode( + "SystemCall.callWithHooksOrRevert(self.from, self.systemId, systemCall, msg.value)", + returnParameters, + )} + } + `; + } else { + return ` + ${functionSignature} { + bytes memory systemCall = ${encodedSystemCall}; + ${renderAbiDecode("SystemCall.staticcallOrRevert(self.from, self.systemId, systemCall)", returnParameters)} + } + `; + } +} + +function renderEncodeSystemCall(interfaceName: string, functionName: string, parameters: string[]) { + const paramNames = parameters.map((param) => param.split(" ").slice(-1)[0]).join(", "); + return `abi.encodeCall(${interfaceName}.${functionName}, (${paramNames}))`; +} + +function renderAbiDecode(expression: string, returnParameters: string[]) { + if (returnParameters.length === 0) return `${expression};`; + + const returnTypes = returnParameters.map((param) => param.split(" ")[0]).join(", "); + return ` + bytes memory result = ${expression}; + return abi.decode(result, (${returnTypes})); + `; +} + +function renderReturnParameters(returnParameters: string[]) { + if (returnParameters.length == 0) return ""; + + return `returns (${renderArguments(returnParameters)})`; +} diff --git a/packages/world/ts/node/render-solidity/types.ts b/packages/world/ts/node/render-solidity/types.ts index d8b9772551..4172a84137 100644 --- a/packages/world/ts/node/render-solidity/types.ts +++ b/packages/world/ts/node/render-solidity/types.ts @@ -8,3 +8,21 @@ export interface RenderSystemInterfaceOptions { functions: ContractInterfaceFunction[]; errors: ContractInterfaceError[]; } + +export interface RenderSystemLibraryOptions { + /** List of symbols to import, and their file paths */ + imports: ImportDatum[]; + systemLabel: string; + systemName: string; + namespace: string; + interfaceName: string; + libraryName: string; + resourceId: string; + functions: ContractInterfaceFunction[]; + errors: ContractInterfaceError[]; + + /** Path for store package imports */ + storeImportPath: string; + /** Path for world package imports */ + worldImportPath: string; +} diff --git a/packages/world/ts/node/render-solidity/worldgen.ts b/packages/world/ts/node/render-solidity/worldgen.ts index fcb62bf6c4..645cdfaebe 100644 --- a/packages/world/ts/node/render-solidity/worldgen.ts +++ b/packages/world/ts/node/render-solidity/worldgen.ts @@ -3,8 +3,10 @@ import path from "node:path"; import { formatAndWriteSolidity, contractToInterface, type ImportDatum } from "@latticexyz/common/codegen"; import { renderSystemInterface } from "./renderSystemInterface"; import { renderWorldInterface } from "./renderWorldInterface"; +import { renderSystemLibrary } from "./renderSystemLibrary"; import { World as WorldConfig } from "../../config/v2/output"; import { resolveSystems } from "../resolveSystems"; +import { resourceToHex } from "@latticexyz/common"; export async function worldgen({ rootDir, @@ -15,31 +17,50 @@ export async function worldgen({ config: WorldConfig; clean?: boolean; }) { - const outDir = path.join( + const worldgenOutDir = path.join( rootDir, config.sourceDirectory, config.codegen.outputDirectory, config.codegen.worldgenDirectory, ); - if (clean) { - await fs.rm(outDir, { recursive: true, force: true }); - } - - const outputPath = path.join(outDir, config.codegen.worldInterfaceName + ".sol"); - const systems = (await resolveSystems({ rootDir, config })) // TODO: move to codegen option or generate "system manifest" and codegen from that .filter((system) => system.deploy.registerWorldFunctions) .map((system) => { const interfaceName = `I${system.label}`; + const libraryName = `${system.label}Lib`; + + const sourceDir = config.multipleNamespaces + ? path.join(config.sourceDirectory, "namespaces", system.namespaceLabel) + : config.sourceDirectory; + + const libraryOutDir = path.join( + sourceDir, + config.codegen.outputDirectory, + config.codegen.systemLibrariesDirectory, + ); + return { ...system, interfaceName, - interfacePath: path.join(path.dirname(outputPath), `${interfaceName}.sol`), + libraryName, + interfacePath: path.join(worldgenOutDir, `${interfaceName}.sol`), + libraryPath: path.join(libraryOutDir, `${libraryName}.sol`), }; }); + if (clean) { + const libraryDirs = [...new Set(systems.map(({ libraryPath }) => path.dirname(libraryPath)))]; + + await Promise.all([ + fs.rm(worldgenOutDir, { recursive: true, force: true }), + ...libraryDirs.map((dir) => fs.rm(dir, { recursive: true, force: true })), + ]); + } + + const outputPath = path.join(worldgenOutDir, config.codegen.worldInterfaceName + ".sol"); + const worldImports = systems.map( (system): ImportDatum => ({ symbol: system.interfaceName, @@ -47,6 +68,13 @@ export async function worldgen({ }), ); + const storeImportPath = config.codegen.storeImportPath.startsWith(".") + ? "./" + path.relative(path.dirname(outputPath), path.join(rootDir, config.codegen.storeImportPath)) + : config.codegen.storeImportPath; + const worldImportPath = config.codegen.worldImportPath.startsWith(".") + ? "./" + path.relative(path.dirname(outputPath), path.join(rootDir, config.codegen.worldImportPath)) + : config.codegen.worldImportPath; + await Promise.all( systems.map(async (system) => { const source = await fs.readFile(path.join(rootDir, system.sourcePath), "utf8"); @@ -56,11 +84,11 @@ export async function worldgen({ ({ symbol, path: importPath }): ImportDatum => ({ symbol, path: importPath.startsWith(".") - ? "./" + path.relative(outDir, path.join(rootDir, path.dirname(system.sourcePath), importPath)) + ? "./" + path.relative(worldgenOutDir, path.join(rootDir, path.dirname(system.sourcePath), importPath)) : importPath, }), ); - const output = renderSystemInterface({ + const systemInterface = renderSystemInterface({ name: system.interfaceName, functionPrefix: system.namespace === "" ? "" : `${system.namespace}__`, functions, @@ -68,7 +96,30 @@ export async function worldgen({ imports, }); // write to file - await formatAndWriteSolidity(output, system.interfacePath, "Generated system interface"); + await formatAndWriteSolidity(systemInterface, system.interfacePath, "Generated system interface"); + + const systemImport = { + symbol: system.label, + path: "./" + path.relative(path.dirname(system.libraryPath), system.sourcePath), + }; + + if (config.codegen.generateSystemLibraries) { + const systemLibrary = renderSystemLibrary({ + libraryName: system.libraryName, + interfaceName: system.interfaceName, + systemLabel: system.label, + systemName: system.name, + namespace: system.namespace, + resourceId: resourceToHex({ type: "system", namespace: system.namespace, name: system.name }), + functions, + errors, + imports: [systemImport, ...imports], + storeImportPath, + worldImportPath, + }); + // write to file + await formatAndWriteSolidity(systemLibrary, system.libraryPath, "Generated system library"); + } }), ); @@ -76,12 +127,8 @@ export async function worldgen({ const output = renderWorldInterface({ interfaceName: config.codegen.worldInterfaceName, imports: worldImports, - storeImportPath: config.codegen.storeImportPath.startsWith(".") - ? "./" + path.relative(path.dirname(outputPath), path.join(rootDir, config.codegen.storeImportPath)) - : config.codegen.storeImportPath, - worldImportPath: config.codegen.worldImportPath.startsWith(".") - ? "./" + path.relative(path.dirname(outputPath), path.join(rootDir, config.codegen.worldImportPath)) - : config.codegen.worldImportPath, + storeImportPath, + worldImportPath, }); // write to file await formatAndWriteSolidity(output, outputPath, "Generated world interface"); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 190e56648c..dd7bc8d0f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1491,6 +1491,33 @@ importers: specifier: https://github.com/foundry-rs/forge-std.git#74cfb77e308dd188d2f58864aaf44963ae6b88b1 version: https://codeload.github.com/foundry-rs/forge-std/tar.gz/74cfb77e308dd188d2f58864aaf44963ae6b88b1 + test/system-libraries: + devDependencies: + '@latticexyz/cli': + specifier: workspace:* + version: link:../../packages/cli + '@latticexyz/schema-type': + specifier: workspace:* + version: link:../../packages/schema-type + '@latticexyz/store': + specifier: workspace:* + version: link:../../packages/store + '@latticexyz/world': + specifier: workspace:* + version: link:../../packages/world + '@latticexyz/world-modules': + specifier: workspace:* + version: link:../../packages/world-modules + dotenv: + specifier: ^16.0.3 + version: 16.0.3 + ds-test: + specifier: https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0 + version: https://codeload.github.com/dapphub/ds-test/tar.gz/e282159d5170298eb2455a6c05280ab5a73a4ef0 + forge-std: + specifier: https://github.com/foundry-rs/forge-std.git#74cfb77e308dd188d2f58864aaf44963ae6b88b1 + version: https://codeload.github.com/foundry-rs/forge-std/tar.gz/74cfb77e308dd188d2f58864aaf44963ae6b88b1 + test/ts-benchmarks: devDependencies: '@latticexyz/recs': @@ -18845,7 +18872,7 @@ snapshots: debug: 4.3.7 enhanced-resolve: 5.17.1 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.5 @@ -18857,7 +18884,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -18878,7 +18905,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.1(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.15.0 is-glob: 4.0.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 29e13c6291..472b0d2f0c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,7 @@ packages: - packages/* - test/mock-game-contracts - test/puppet-modules + - test/system-libraries - test/ts-benchmarks catalog: diff --git a/test/system-libraries/.env b/test/system-libraries/.env new file mode 100644 index 0000000000..d02fd07732 --- /dev/null +++ b/test/system-libraries/.env @@ -0,0 +1,2 @@ +# Default Anvil private key +PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 diff --git a/test/system-libraries/.gitignore b/test/system-libraries/.gitignore new file mode 100644 index 0000000000..d2b0438d96 --- /dev/null +++ b/test/system-libraries/.gitignore @@ -0,0 +1,9 @@ +out/ +cache/ +node_modules/ +bindings/ +artifacts/ +broadcast/ + +# Ignore all deploy artifacts +deploys/**/*.json diff --git a/test/system-libraries/foundry.toml b/test/system-libraries/foundry.toml new file mode 100644 index 0000000000..c5a42ac161 --- /dev/null +++ b/test/system-libraries/foundry.toml @@ -0,0 +1,21 @@ +[profile.default] +solc = "0.8.28" +ffi = false +fuzz_runs = 256 +optimizer = true +optimizer_runs = 3000 +verbosity = 2 +src = "src" +test = "test" +out = "out" +allow_paths = [ + # pnpm symlinks to the project root's node_modules + "../../node_modules", + # we're also using linked mud packages from the monorepo + "../../../packages" +] +extra_output_files = [ + "abi", + "evm.bytecode" +] +fs_permissions = [{ access = "read", path = "./"}] diff --git a/test/system-libraries/mud.config.ts b/test/system-libraries/mud.config.ts new file mode 100644 index 0000000000..71785f0960 --- /dev/null +++ b/test/system-libraries/mud.config.ts @@ -0,0 +1,36 @@ +import { defineWorld } from "@latticexyz/world"; + +export default defineWorld({ + namespaces: { + a: { + tables: { + Value: { + schema: { + value: "uint256", + }, + key: [], + }, + AddressValue: { + schema: { + value: "address", + }, + key: [], + }, + }, + }, + b: {}, + root: { + namespace: "", + }, + }, + codegen: { + generateSystemLibraries: true, + }, + modules: [ + { + artifactPath: "@latticexyz/world-modules/out/StandardDelegationsModule.sol/StandardDelegationsModule.json", + root: true, + args: [], + }, + ], +}); diff --git a/test/system-libraries/package.json b/test/system-libraries/package.json new file mode 100644 index 0000000000..3abb439ccd --- /dev/null +++ b/test/system-libraries/package.json @@ -0,0 +1,22 @@ +{ + "name": "system-libraries-test", + "version": "0.0.0", + "private": true, + "license": "MIT", + "scripts": { + "build": "mud build", + "clean": "forge clean && shx rm -rf src/**/codegen", + "test": "mud test --port 4243", + "test:ci": "pnpm run test" + }, + "devDependencies": { + "@latticexyz/cli": "workspace:*", + "@latticexyz/schema-type": "workspace:*", + "@latticexyz/store": "workspace:*", + "@latticexyz/world": "workspace:*", + "@latticexyz/world-modules": "workspace:*", + "dotenv": "^16.0.3", + "ds-test": "https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0", + "forge-std": "https://github.com/foundry-rs/forge-std.git#74cfb77e308dd188d2f58864aaf44963ae6b88b1" + } +} diff --git a/test/system-libraries/remappings.txt b/test/system-libraries/remappings.txt new file mode 100644 index 0000000000..c4d992480e --- /dev/null +++ b/test/system-libraries/remappings.txt @@ -0,0 +1,3 @@ +ds-test/=node_modules/ds-test/src/ +forge-std/=node_modules/forge-std/src/ +@latticexyz/=node_modules/@latticexyz/ diff --git a/test/system-libraries/src/codegen/world/IASystem.sol b/test/system-libraries/src/codegen/world/IASystem.sol new file mode 100644 index 0000000000..859eab22e4 --- /dev/null +++ b/test/system-libraries/src/codegen/world/IASystem.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +/** + * @title IASystem + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @dev This interface is automatically generated from the corresponding system contract. Do not edit manually. + */ +interface IASystem { + function a__setValue(uint256 value) external; + + function a__getValue() external view returns (uint256); + + function a__getTwoValues() external view returns (uint256, uint256); + + function a__setAddress() external returns (address); +} diff --git a/test/system-libraries/src/codegen/world/IBSystem.sol b/test/system-libraries/src/codegen/world/IBSystem.sol new file mode 100644 index 0000000000..b0db384230 --- /dev/null +++ b/test/system-libraries/src/codegen/world/IBSystem.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +/** + * @title IBSystem + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @dev This interface is automatically generated from the corresponding system contract. Do not edit manually. + */ +interface IBSystem { + function b__setValueInA(uint256 value) external; + + function b__getValueFromA() external view returns (uint256); +} diff --git a/test/system-libraries/src/codegen/world/IRootSystem.sol b/test/system-libraries/src/codegen/world/IRootSystem.sol new file mode 100644 index 0000000000..e7d8d7c8fd --- /dev/null +++ b/test/system-libraries/src/codegen/world/IRootSystem.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +/** + * @title IRootSystem + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @dev This interface is automatically generated from the corresponding system contract. Do not edit manually. + */ +interface IRootSystem { + function setValueInA(uint256 value) external; + + function getValueFromA() external view returns (uint256); +} diff --git a/test/system-libraries/src/codegen/world/IWorld.sol b/test/system-libraries/src/codegen/world/IWorld.sol new file mode 100644 index 0000000000..2836d04158 --- /dev/null +++ b/test/system-libraries/src/codegen/world/IWorld.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol"; +import { IASystem } from "./IASystem.sol"; +import { IBSystem } from "./IBSystem.sol"; +import { IRootSystem } from "./IRootSystem.sol"; + +/** + * @title IWorld + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @notice This interface integrates all systems and associated function selectors + * that are dynamically registered in the World during deployment. + * @dev This is an autogenerated file; do not edit manually. + */ +interface IWorld is IBaseWorld, IASystem, IBSystem, IRootSystem {} diff --git a/test/system-libraries/src/namespaces/a/ASystem.sol b/test/system-libraries/src/namespaces/a/ASystem.sol new file mode 100644 index 0000000000..a0edaa4f6a --- /dev/null +++ b/test/system-libraries/src/namespaces/a/ASystem.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.28; + +import { System } from "@latticexyz/world/src/System.sol"; +import { Value } from "./codegen/tables/Value.sol"; +import { AddressValue } from "./codegen/tables/AddressValue.sol"; + +contract ASystem is System { + function setValue(uint256 value) external { + Value.set(value); + } + + function getValue() external view returns (uint256) { + return Value.get(); + } + + function getTwoValues() external view returns (uint256, uint256) { + return (Value.get(), Value.get()); + } + + function setAddress() external returns (address) { + address addr = _msgSender(); + AddressValue.set(addr); + return addr; + } +} diff --git a/test/system-libraries/src/namespaces/a/codegen/index.sol b/test/system-libraries/src/namespaces/a/codegen/index.sol new file mode 100644 index 0000000000..690f6bc5b3 --- /dev/null +++ b/test/system-libraries/src/namespaces/a/codegen/index.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +import { Value } from "./tables/Value.sol"; +import { AddressValue } from "./tables/AddressValue.sol"; diff --git a/test/system-libraries/src/namespaces/a/codegen/systems/ASystemLib.sol b/test/system-libraries/src/namespaces/a/codegen/systems/ASystemLib.sol new file mode 100644 index 0000000000..9732eb90e7 --- /dev/null +++ b/test/system-libraries/src/namespaces/a/codegen/systems/ASystemLib.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +import { ASystem } from "../../ASystem.sol"; +import { revertWithBytes } from "@latticexyz/world/src/revertWithBytes.sol"; +import { IWorldCall } from "@latticexyz/world/src/IWorldKernel.sol"; +import { SystemCall } from "@latticexyz/world/src/SystemCall.sol"; +import { Systems } from "@latticexyz/world/src/codegen/tables/Systems.sol"; +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; +import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; + +type ASystemType is bytes32; + +// equivalent to WorldResourceIdLib.encode({ typeId: RESOURCE_SYSTEM, namespace: "a", name: "ASystem" })) +ASystemType constant aSystem = ASystemType.wrap(0x737961000000000000000000000000004153797374656d000000000000000000); + +struct CallWrapper { + ResourceId systemId; + address from; +} + +struct RootCallWrapper { + ResourceId systemId; + address from; +} + +/** + * @title ASystemLib + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @dev This library is automatically generated from the corresponding system contract. Do not edit manually. + */ +library ASystemLib { + error ASystemLib_CallingFromRootSystem(); + + function setValue(ASystemType self, uint256 value) internal { + return CallWrapper(self.toResourceId(), address(0)).setValue(value); + } + + function getValue(ASystemType self) internal view returns (uint256) { + return CallWrapper(self.toResourceId(), address(0)).getValue(); + } + + function getTwoValues(ASystemType self) internal view returns (uint256, uint256) { + return CallWrapper(self.toResourceId(), address(0)).getTwoValues(); + } + + function setAddress(ASystemType self) internal returns (address) { + return CallWrapper(self.toResourceId(), address(0)).setAddress(); + } + + function setValue(CallWrapper memory self, uint256 value) internal { + // if the contract calling this function is a root system, it should use `callAsRoot` + if (address(_world()) == address(this)) revert ASystemLib_CallingFromRootSystem(); + + bytes memory systemCall = abi.encodeCall(ASystem.setValue, (value)); + self.from == address(0) + ? _world().call(self.systemId, systemCall) + : _world().callFrom(self.from, self.systemId, systemCall); + } + + function getValue(CallWrapper memory self) internal view returns (uint256) { + // if the contract calling this function is a root system, it should use `callAsRoot` + if (address(_world()) == address(this)) revert ASystemLib_CallingFromRootSystem(); + + bytes memory systemCall = abi.encodeCall(ASystem.getValue, ()); + bytes memory worldCall = self.from == address(0) + ? abi.encodeCall(IWorldCall.call, (self.systemId, systemCall)) + : abi.encodeCall(IWorldCall.callFrom, (self.from, self.systemId, systemCall)); + (bool success, bytes memory returnData) = address(_world()).staticcall(worldCall); + if (!success) revertWithBytes(returnData); + + bytes memory result = abi.decode(returnData, (bytes)); + return abi.decode(result, (uint256)); + } + + function getTwoValues(CallWrapper memory self) internal view returns (uint256, uint256) { + // if the contract calling this function is a root system, it should use `callAsRoot` + if (address(_world()) == address(this)) revert ASystemLib_CallingFromRootSystem(); + + bytes memory systemCall = abi.encodeCall(ASystem.getTwoValues, ()); + bytes memory worldCall = self.from == address(0) + ? abi.encodeCall(IWorldCall.call, (self.systemId, systemCall)) + : abi.encodeCall(IWorldCall.callFrom, (self.from, self.systemId, systemCall)); + (bool success, bytes memory returnData) = address(_world()).staticcall(worldCall); + if (!success) revertWithBytes(returnData); + + bytes memory result = abi.decode(returnData, (bytes)); + return abi.decode(result, (uint256, uint256)); + } + + function setAddress(CallWrapper memory self) internal returns (address) { + // if the contract calling this function is a root system, it should use `callAsRoot` + if (address(_world()) == address(this)) revert ASystemLib_CallingFromRootSystem(); + + bytes memory systemCall = abi.encodeCall(ASystem.setAddress, ()); + + bytes memory result = self.from == address(0) + ? _world().call(self.systemId, systemCall) + : _world().callFrom(self.from, self.systemId, systemCall); + return abi.decode(result, (address)); + } + + function setValue(RootCallWrapper memory self, uint256 value) internal { + bytes memory systemCall = abi.encodeCall(ASystem.setValue, (value)); + SystemCall.callWithHooksOrRevert(self.from, self.systemId, systemCall, msg.value); + } + + function getValue(RootCallWrapper memory self) internal view returns (uint256) { + bytes memory systemCall = abi.encodeCall(ASystem.getValue, ()); + + bytes memory result = SystemCall.staticcallOrRevert(self.from, self.systemId, systemCall); + return abi.decode(result, (uint256)); + } + + function getTwoValues(RootCallWrapper memory self) internal view returns (uint256, uint256) { + bytes memory systemCall = abi.encodeCall(ASystem.getTwoValues, ()); + + bytes memory result = SystemCall.staticcallOrRevert(self.from, self.systemId, systemCall); + return abi.decode(result, (uint256, uint256)); + } + + function setAddress(RootCallWrapper memory self) internal returns (address) { + bytes memory systemCall = abi.encodeCall(ASystem.setAddress, ()); + + bytes memory result = SystemCall.callWithHooksOrRevert(self.from, self.systemId, systemCall, msg.value); + return abi.decode(result, (address)); + } + + function callFrom(ASystemType self, address from) internal pure returns (CallWrapper memory) { + return CallWrapper(self.toResourceId(), from); + } + + function callAsRoot(ASystemType self) internal view returns (RootCallWrapper memory) { + return RootCallWrapper(self.toResourceId(), msg.sender); + } + + function callAsRootFrom(ASystemType self, address from) internal pure returns (RootCallWrapper memory) { + return RootCallWrapper(self.toResourceId(), from); + } + + function toResourceId(ASystemType self) internal pure returns (ResourceId) { + return ResourceId.wrap(ASystemType.unwrap(self)); + } + + function fromResourceId(ResourceId resourceId) internal pure returns (ASystemType) { + return ASystemType.wrap(resourceId.unwrap()); + } + + function getAddress(ASystemType self) internal view returns (address) { + return Systems.getSystem(self.toResourceId()); + } + + function _world() private view returns (IWorldCall) { + return IWorldCall(StoreSwitch.getStoreAddress()); + } +} + +using ASystemLib for ASystemType global; +using ASystemLib for CallWrapper global; +using ASystemLib for RootCallWrapper global; diff --git a/test/system-libraries/src/namespaces/a/codegen/tables/AddressValue.sol b/test/system-libraries/src/namespaces/a/codegen/tables/AddressValue.sol new file mode 100644 index 0000000000..604c212bc0 --- /dev/null +++ b/test/system-libraries/src/namespaces/a/codegen/tables/AddressValue.sol @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +// Import store internals +import { IStore } from "@latticexyz/store/src/IStore.sol"; +import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; +import { StoreCore } from "@latticexyz/store/src/StoreCore.sol"; +import { Bytes } from "@latticexyz/store/src/Bytes.sol"; +import { Memory } from "@latticexyz/store/src/Memory.sol"; +import { SliceLib } from "@latticexyz/store/src/Slice.sol"; +import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol"; +import { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol"; +import { Schema } from "@latticexyz/store/src/Schema.sol"; +import { EncodedLengths, EncodedLengthsLib } from "@latticexyz/store/src/EncodedLengths.sol"; +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; + +library AddressValue { + // Hex below is the result of `WorldResourceIdLib.encode({ namespace: "a", name: "AddressValue", typeId: RESOURCE_TABLE });` + ResourceId constant _tableId = ResourceId.wrap(0x746261000000000000000000000000004164647265737356616c756500000000); + + FieldLayout constant _fieldLayout = + FieldLayout.wrap(0x0014010014000000000000000000000000000000000000000000000000000000); + + // Hex-encoded key schema of () + Schema constant _keySchema = Schema.wrap(0x0000000000000000000000000000000000000000000000000000000000000000); + // Hex-encoded value schema of (address) + Schema constant _valueSchema = Schema.wrap(0x0014010061000000000000000000000000000000000000000000000000000000); + + /** + * @notice Get the table's key field names. + * @return keyNames An array of strings with the names of key fields. + */ + function getKeyNames() internal pure returns (string[] memory keyNames) { + keyNames = new string[](0); + } + + /** + * @notice Get the table's value field names. + * @return fieldNames An array of strings with the names of value fields. + */ + function getFieldNames() internal pure returns (string[] memory fieldNames) { + fieldNames = new string[](1); + fieldNames[0] = "value"; + } + + /** + * @notice Register the table with its config. + */ + function register() internal { + StoreSwitch.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); + } + + /** + * @notice Register the table with its config. + */ + function _register() internal { + StoreCore.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); + } + + /** + * @notice Get value. + */ + function getValue() internal view returns (address value) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Get value. + */ + function _getValue() internal view returns (address value) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Get value. + */ + function get() internal view returns (address value) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Get value. + */ + function _get() internal view returns (address value) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Set value. + */ + function setValue(address value) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); + } + + /** + * @notice Set value. + */ + function _setValue(address value) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); + } + + /** + * @notice Set value. + */ + function set(address value) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); + } + + /** + * @notice Set value. + */ + function _set(address value) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); + } + + /** + * @notice Delete all data for given keys. + */ + function deleteRecord() internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.deleteRecord(_tableId, _keyTuple); + } + + /** + * @notice Delete all data for given keys. + */ + function _deleteRecord() internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); + } + + /** + * @notice Tightly pack static (fixed length) data using this table's schema. + * @return The static data, encoded into a sequence of bytes. + */ + function encodeStatic(address value) internal pure returns (bytes memory) { + return abi.encodePacked(value); + } + + /** + * @notice Encode all of a record's fields. + * @return The static (fixed length) data, encoded into a sequence of bytes. + * @return The lengths of the dynamic fields (packed into a single bytes32 value). + * @return The dynamic (variable length) data, encoded into a sequence of bytes. + */ + function encode(address value) internal pure returns (bytes memory, EncodedLengths, bytes memory) { + bytes memory _staticData = encodeStatic(value); + + EncodedLengths _encodedLengths; + bytes memory _dynamicData; + + return (_staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Encode keys as a bytes32 array using this table's field layout. + */ + function encodeKeyTuple() internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](0); + + return _keyTuple; + } +} diff --git a/test/system-libraries/src/namespaces/a/codegen/tables/Value.sol b/test/system-libraries/src/namespaces/a/codegen/tables/Value.sol new file mode 100644 index 0000000000..00b391b5d7 --- /dev/null +++ b/test/system-libraries/src/namespaces/a/codegen/tables/Value.sol @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +// Import store internals +import { IStore } from "@latticexyz/store/src/IStore.sol"; +import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; +import { StoreCore } from "@latticexyz/store/src/StoreCore.sol"; +import { Bytes } from "@latticexyz/store/src/Bytes.sol"; +import { Memory } from "@latticexyz/store/src/Memory.sol"; +import { SliceLib } from "@latticexyz/store/src/Slice.sol"; +import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol"; +import { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol"; +import { Schema } from "@latticexyz/store/src/Schema.sol"; +import { EncodedLengths, EncodedLengthsLib } from "@latticexyz/store/src/EncodedLengths.sol"; +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; + +library Value { + // Hex below is the result of `WorldResourceIdLib.encode({ namespace: "a", name: "Value", typeId: RESOURCE_TABLE });` + ResourceId constant _tableId = ResourceId.wrap(0x7462610000000000000000000000000056616c75650000000000000000000000); + + FieldLayout constant _fieldLayout = + FieldLayout.wrap(0x0020010020000000000000000000000000000000000000000000000000000000); + + // Hex-encoded key schema of () + Schema constant _keySchema = Schema.wrap(0x0000000000000000000000000000000000000000000000000000000000000000); + // Hex-encoded value schema of (uint256) + Schema constant _valueSchema = Schema.wrap(0x002001001f000000000000000000000000000000000000000000000000000000); + + /** + * @notice Get the table's key field names. + * @return keyNames An array of strings with the names of key fields. + */ + function getKeyNames() internal pure returns (string[] memory keyNames) { + keyNames = new string[](0); + } + + /** + * @notice Get the table's value field names. + * @return fieldNames An array of strings with the names of value fields. + */ + function getFieldNames() internal pure returns (string[] memory fieldNames) { + fieldNames = new string[](1); + fieldNames[0] = "value"; + } + + /** + * @notice Register the table with its config. + */ + function register() internal { + StoreSwitch.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); + } + + /** + * @notice Register the table with its config. + */ + function _register() internal { + StoreCore.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); + } + + /** + * @notice Get value. + */ + function getValue() internal view returns (uint256 value) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get value. + */ + function _getValue() internal view returns (uint256 value) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get value. + */ + function get() internal view returns (uint256 value) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get value. + */ + function _get() internal view returns (uint256 value) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Set value. + */ + function setValue(uint256 value) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); + } + + /** + * @notice Set value. + */ + function _setValue(uint256 value) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); + } + + /** + * @notice Set value. + */ + function set(uint256 value) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); + } + + /** + * @notice Set value. + */ + function _set(uint256 value) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); + } + + /** + * @notice Delete all data for given keys. + */ + function deleteRecord() internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.deleteRecord(_tableId, _keyTuple); + } + + /** + * @notice Delete all data for given keys. + */ + function _deleteRecord() internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); + } + + /** + * @notice Tightly pack static (fixed length) data using this table's schema. + * @return The static data, encoded into a sequence of bytes. + */ + function encodeStatic(uint256 value) internal pure returns (bytes memory) { + return abi.encodePacked(value); + } + + /** + * @notice Encode all of a record's fields. + * @return The static (fixed length) data, encoded into a sequence of bytes. + * @return The lengths of the dynamic fields (packed into a single bytes32 value). + * @return The dynamic (variable length) data, encoded into a sequence of bytes. + */ + function encode(uint256 value) internal pure returns (bytes memory, EncodedLengths, bytes memory) { + bytes memory _staticData = encodeStatic(value); + + EncodedLengths _encodedLengths; + bytes memory _dynamicData; + + return (_staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Encode keys as a bytes32 array using this table's field layout. + */ + function encodeKeyTuple() internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](0); + + return _keyTuple; + } +} diff --git a/test/system-libraries/src/namespaces/b/BSystem.sol b/test/system-libraries/src/namespaces/b/BSystem.sol new file mode 100644 index 0000000000..9546a8d7b1 --- /dev/null +++ b/test/system-libraries/src/namespaces/b/BSystem.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.28; + +import { System } from "@latticexyz/world/src/System.sol"; +import { aSystem } from "../a/codegen/systems/ASystemLib.sol"; + +contract BSystem is System { + function setValueInA(uint256 value) external { + aSystem.setValue(value); + } + + function getValueFromA() external view returns (uint256) { + return aSystem.getValue(); + } +} diff --git a/test/system-libraries/src/namespaces/b/codegen/systems/BSystemLib.sol b/test/system-libraries/src/namespaces/b/codegen/systems/BSystemLib.sol new file mode 100644 index 0000000000..f71c305fb1 --- /dev/null +++ b/test/system-libraries/src/namespaces/b/codegen/systems/BSystemLib.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +import { BSystem } from "../../BSystem.sol"; +import { revertWithBytes } from "@latticexyz/world/src/revertWithBytes.sol"; +import { IWorldCall } from "@latticexyz/world/src/IWorldKernel.sol"; +import { SystemCall } from "@latticexyz/world/src/SystemCall.sol"; +import { Systems } from "@latticexyz/world/src/codegen/tables/Systems.sol"; +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; +import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; + +type BSystemType is bytes32; + +// equivalent to WorldResourceIdLib.encode({ typeId: RESOURCE_SYSTEM, namespace: "b", name: "BSystem" })) +BSystemType constant bSystem = BSystemType.wrap(0x737962000000000000000000000000004253797374656d000000000000000000); + +struct CallWrapper { + ResourceId systemId; + address from; +} + +struct RootCallWrapper { + ResourceId systemId; + address from; +} + +/** + * @title BSystemLib + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @dev This library is automatically generated from the corresponding system contract. Do not edit manually. + */ +library BSystemLib { + error BSystemLib_CallingFromRootSystem(); + + function setValueInA(BSystemType self, uint256 value) internal { + return CallWrapper(self.toResourceId(), address(0)).setValueInA(value); + } + + function getValueFromA(BSystemType self) internal view returns (uint256) { + return CallWrapper(self.toResourceId(), address(0)).getValueFromA(); + } + + function setValueInA(CallWrapper memory self, uint256 value) internal { + // if the contract calling this function is a root system, it should use `callAsRoot` + if (address(_world()) == address(this)) revert BSystemLib_CallingFromRootSystem(); + + bytes memory systemCall = abi.encodeCall(BSystem.setValueInA, (value)); + self.from == address(0) + ? _world().call(self.systemId, systemCall) + : _world().callFrom(self.from, self.systemId, systemCall); + } + + function getValueFromA(CallWrapper memory self) internal view returns (uint256) { + // if the contract calling this function is a root system, it should use `callAsRoot` + if (address(_world()) == address(this)) revert BSystemLib_CallingFromRootSystem(); + + bytes memory systemCall = abi.encodeCall(BSystem.getValueFromA, ()); + bytes memory worldCall = self.from == address(0) + ? abi.encodeCall(IWorldCall.call, (self.systemId, systemCall)) + : abi.encodeCall(IWorldCall.callFrom, (self.from, self.systemId, systemCall)); + (bool success, bytes memory returnData) = address(_world()).staticcall(worldCall); + if (!success) revertWithBytes(returnData); + + bytes memory result = abi.decode(returnData, (bytes)); + return abi.decode(result, (uint256)); + } + + function setValueInA(RootCallWrapper memory self, uint256 value) internal { + bytes memory systemCall = abi.encodeCall(BSystem.setValueInA, (value)); + SystemCall.callWithHooksOrRevert(self.from, self.systemId, systemCall, msg.value); + } + + function getValueFromA(RootCallWrapper memory self) internal view returns (uint256) { + bytes memory systemCall = abi.encodeCall(BSystem.getValueFromA, ()); + + bytes memory result = SystemCall.staticcallOrRevert(self.from, self.systemId, systemCall); + return abi.decode(result, (uint256)); + } + + function callFrom(BSystemType self, address from) internal pure returns (CallWrapper memory) { + return CallWrapper(self.toResourceId(), from); + } + + function callAsRoot(BSystemType self) internal view returns (RootCallWrapper memory) { + return RootCallWrapper(self.toResourceId(), msg.sender); + } + + function callAsRootFrom(BSystemType self, address from) internal pure returns (RootCallWrapper memory) { + return RootCallWrapper(self.toResourceId(), from); + } + + function toResourceId(BSystemType self) internal pure returns (ResourceId) { + return ResourceId.wrap(BSystemType.unwrap(self)); + } + + function fromResourceId(ResourceId resourceId) internal pure returns (BSystemType) { + return BSystemType.wrap(resourceId.unwrap()); + } + + function getAddress(BSystemType self) internal view returns (address) { + return Systems.getSystem(self.toResourceId()); + } + + function _world() private view returns (IWorldCall) { + return IWorldCall(StoreSwitch.getStoreAddress()); + } +} + +using BSystemLib for BSystemType global; +using BSystemLib for CallWrapper global; +using BSystemLib for RootCallWrapper global; diff --git a/test/system-libraries/src/namespaces/root/RootSystem.sol b/test/system-libraries/src/namespaces/root/RootSystem.sol new file mode 100644 index 0000000000..eceb396834 --- /dev/null +++ b/test/system-libraries/src/namespaces/root/RootSystem.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.28; + +import { System } from "@latticexyz/world/src/System.sol"; +import { aSystem } from "../a/codegen/systems/ASystemLib.sol"; + +contract RootSystem is System { + function setValueInA(uint256 value) external { + aSystem.callAsRoot().setValue(value); + } + + function getValueFromA() external view returns (uint256) { + return aSystem.callAsRoot().getValue(); + } +} diff --git a/test/system-libraries/src/namespaces/root/codegen/systems/RootSystemLib.sol b/test/system-libraries/src/namespaces/root/codegen/systems/RootSystemLib.sol new file mode 100644 index 0000000000..de6d5dcee7 --- /dev/null +++ b/test/system-libraries/src/namespaces/root/codegen/systems/RootSystemLib.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +import { RootSystem } from "../../RootSystem.sol"; +import { revertWithBytes } from "@latticexyz/world/src/revertWithBytes.sol"; +import { IWorldCall } from "@latticexyz/world/src/IWorldKernel.sol"; +import { SystemCall } from "@latticexyz/world/src/SystemCall.sol"; +import { Systems } from "@latticexyz/world/src/codegen/tables/Systems.sol"; +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; +import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; + +type RootSystemType is bytes32; + +// equivalent to WorldResourceIdLib.encode({ typeId: RESOURCE_SYSTEM, namespace: "", name: "RootSystem" })) +RootSystemType constant rootSystem = RootSystemType.wrap( + 0x73790000000000000000000000000000526f6f7453797374656d000000000000 +); + +struct CallWrapper { + ResourceId systemId; + address from; +} + +struct RootCallWrapper { + ResourceId systemId; + address from; +} + +/** + * @title RootSystemLib + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @dev This library is automatically generated from the corresponding system contract. Do not edit manually. + */ +library RootSystemLib { + error RootSystemLib_CallingFromRootSystem(); + + function setValueInA(RootSystemType self, uint256 value) internal { + return CallWrapper(self.toResourceId(), address(0)).setValueInA(value); + } + + function getValueFromA(RootSystemType self) internal view returns (uint256) { + return CallWrapper(self.toResourceId(), address(0)).getValueFromA(); + } + + function setValueInA(CallWrapper memory self, uint256 value) internal { + // if the contract calling this function is a root system, it should use `callAsRoot` + if (address(_world()) == address(this)) revert RootSystemLib_CallingFromRootSystem(); + + bytes memory systemCall = abi.encodeCall(RootSystem.setValueInA, (value)); + self.from == address(0) + ? _world().call(self.systemId, systemCall) + : _world().callFrom(self.from, self.systemId, systemCall); + } + + function getValueFromA(CallWrapper memory self) internal view returns (uint256) { + // if the contract calling this function is a root system, it should use `callAsRoot` + if (address(_world()) == address(this)) revert RootSystemLib_CallingFromRootSystem(); + + bytes memory systemCall = abi.encodeCall(RootSystem.getValueFromA, ()); + bytes memory worldCall = self.from == address(0) + ? abi.encodeCall(IWorldCall.call, (self.systemId, systemCall)) + : abi.encodeCall(IWorldCall.callFrom, (self.from, self.systemId, systemCall)); + (bool success, bytes memory returnData) = address(_world()).staticcall(worldCall); + if (!success) revertWithBytes(returnData); + + bytes memory result = abi.decode(returnData, (bytes)); + return abi.decode(result, (uint256)); + } + + function setValueInA(RootCallWrapper memory self, uint256 value) internal { + bytes memory systemCall = abi.encodeCall(RootSystem.setValueInA, (value)); + SystemCall.callWithHooksOrRevert(self.from, self.systemId, systemCall, msg.value); + } + + function callFrom(RootSystemType self, address from) internal pure returns (CallWrapper memory) { + return CallWrapper(self.toResourceId(), from); + } + + function callAsRoot(RootSystemType self) internal view returns (RootCallWrapper memory) { + return RootCallWrapper(self.toResourceId(), msg.sender); + } + + function callAsRootFrom(RootSystemType self, address from) internal pure returns (RootCallWrapper memory) { + return RootCallWrapper(self.toResourceId(), from); + } + + function toResourceId(RootSystemType self) internal pure returns (ResourceId) { + return ResourceId.wrap(RootSystemType.unwrap(self)); + } + + function fromResourceId(ResourceId resourceId) internal pure returns (RootSystemType) { + return RootSystemType.wrap(resourceId.unwrap()); + } + + function getAddress(RootSystemType self) internal view returns (address) { + return Systems.getSystem(self.toResourceId()); + } + + function _world() private view returns (IWorldCall) { + return IWorldCall(StoreSwitch.getStoreAddress()); + } +} + +using RootSystemLib for RootSystemType global; +using RootSystemLib for CallWrapper global; +using RootSystemLib for RootCallWrapper global; diff --git a/test/system-libraries/test/Libraries.t.sol b/test/system-libraries/test/Libraries.t.sol new file mode 100644 index 0000000000..988d5d1504 --- /dev/null +++ b/test/system-libraries/test/Libraries.t.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +import { console } from "forge-std/console.sol"; +import { ResourceIds } from "@latticexyz/store/src/codegen/index.sol"; + +import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol"; +import { ResourceId, WorldResourceIdLib } from "@latticexyz/world/src/WorldResourceId.sol"; +import { ResourceIds } from "@latticexyz/store/src/codegen/tables/ResourceIds.sol"; +import { MudTest } from "@latticexyz/world/test/MudTest.t.sol"; + +import { SystemboundDelegationControl } from "@latticexyz/world-modules/src/modules/std-delegations/SystemboundDelegationControl.sol"; +import { SYSTEMBOUND_DELEGATION } from "@latticexyz/world-modules/src/modules/std-delegations/StandardDelegationsModule.sol"; + +import { Value } from "../src/namespaces/a/codegen/tables/Value.sol"; +import { AddressValue } from "../src/namespaces/a/codegen/tables/AddressValue.sol"; +import { aSystem } from "../src/namespaces/a/codegen/systems/ASystemLib.sol"; +import { bSystem } from "../src/namespaces/b/codegen/systems/BSystemLib.sol"; +import { rootSystem } from "../src/namespaces/root/codegen/systems/RootSystemLib.sol"; + +contract LibrariesTest is MudTest { + function testNamespaceIdExists() public { + assertTrue(ResourceIds.get(aSystem.toResourceId())); + assertTrue(ResourceIds.get(bSystem.toResourceId())); + } + + function testCanCallSystemWithLibrary() public { + uint256 value = 0xDEADBEEF; + aSystem.setValue(value); + assertEq(Value.get(), value); + assertEq(aSystem.getValue(), value); + } + + function testCanCallSystemFromOtherSystem() public { + uint256 value = 0xDEADBEEF; + bSystem.setValueInA(value); + assertEq(Value.get(), value); + assertEq(bSystem.getValueFromA(), value); + } + + function testCallFrom() public { + address alice = address(0xDEADBEEF); + + // Alice delegates control to the test contract to call the aSystem on her behalf + vm.prank(alice); + IBaseWorld(worldAddress).registerDelegation( + address(this), + SYSTEMBOUND_DELEGATION, + abi.encodeCall(SystemboundDelegationControl.initDelegation, (address(this), aSystem.toResourceId(), 2)) + ); + + address sender = aSystem.callFrom(alice).setAddress(); + assertEq(sender, alice); + assertEq(AddressValue.get(), alice); + } + + function testCanCallFromRootSystemWithLibrary() public { + uint256 value = 0xDEADBEEF; + // internally, rootSystem uses callAsRoot to call aSystem + rootSystem.setValueInA(value); + assertEq(Value.get(), value); + assertEq(aSystem.getValue(), value); + assertEq(rootSystem.getValueFromA(), value); + } +} diff --git a/turbo.json b/turbo.json index fac6dcab0c..fa151fa625 100644 --- a/turbo.json +++ b/turbo.json @@ -20,8 +20,7 @@ }, "@latticexyz/store#clean": { "dependsOn": [ - "@latticexyz/world-module-erc20#clean", - "@latticexyz/world-modules#clean", + "@latticexyz/world#clean", "puppet-modules-test#clean", "mock-game-contracts#clean" ],