Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): link and deploy public libraries #1910

Merged
merged 32 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
06222fc
feat(cli): link and deploy public libraries
dk1a Nov 11, 2023
9b5446e
Merge branch 'main' into dk1a/public-libraries
dk1a Nov 11, 2023
fca4367
Merge branch 'main' into dk1a/public-libraries
dk1a Nov 12, 2023
f1265bd
test, fixes
dk1a Nov 12, 2023
88049cd
test free func
dk1a Nov 12, 2023
c6ee27a
Create thin-boxes-sparkle.md
dk1a Nov 12, 2023
ad187a2
prettier
dk1a Nov 12, 2023
90da9c0
refactor: remove unused imports
yonadaa Mar 6, 2024
5f5a0c1
fix: do not try to get contractFullPath from AST
yonadaa Mar 6, 2024
dfaffa7
Merge remote-tracking branch 'origin/main' into dk1a/public-libraries
yonadaa Mar 6, 2024
cfa3a50
chore: fix after merge
yonadaa Mar 6, 2024
c6563ad
fix: resolveConfig argument
yonadaa Mar 6, 2024
cb5cdfe
chore: unused import
yonadaa Mar 7, 2024
85bb8eb
test: document tests
yonadaa Mar 7, 2024
80a4b7a
move out dep order
holic Mar 9, 2024
630eab1
rework
holic Mar 9, 2024
1566c4b
fix labels for root namespaces
holic Mar 9, 2024
d0ffbbf
rename file to match method name
holic Mar 9, 2024
f2e331c
update changeset
holic Mar 9, 2024
463fc88
not sure how that got here
holic Mar 9, 2024
9d387a4
small error message tweak
holic Mar 9, 2024
13d6740
remove console logs
holic Mar 9, 2024
48333c4
moved out this change to another PR
holic Mar 9, 2024
8f3514d
return this to how it was
holic Mar 9, 2024
3e958e4
doesn't need to be async
holic Mar 9, 2024
efa1299
clean up
holic Mar 9, 2024
49eb1aa
more clean up
holic Mar 9, 2024
54767dd
Merge remote-tracking branch 'origin/main' into dk1a/public-libraries
holic Mar 11, 2024
32fe6fe
deploy public libs with systems and modules
holic Mar 11, 2024
8817a96
typo
holic Mar 11, 2024
0e5730c
fill in placeholders for default modules
holic Mar 11, 2024
bcc2f28
fix import
holic Mar 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .changeset/thin-boxes-sparkle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
"@latticexyz/cli": major
---

- Added `getPublicLibraries` helper, which returns the data needed to link all public libraries within a given forge output directory

- Added a human-readable error to `ensureContract` about unlinked public libraries

- Added `getContractData` arguments
- - explicit `filename` argument, which used to be inferred based on `contractName`
- - `libraries` argument, which is used to replace bytecode placeholders, to support the use of public libraries

```diff
getContractData(
+ filename: string,
contractName: string,
forgeOutDirectory: string,
+ libraries: PublicLibrary[]
)
```

- Made `cli/src/deploy/resolveConfig` asynchronous, and added `libraries` to its result object, which can then be used by `getContractData`

- Added `PublicLibrary` type and `readonly libraries: readonly PublicLibrary[];` to the `Config` type

- Added public libraries to the deployment pipeline
10 changes: 10 additions & 0 deletions e2e/packages/contracts/src/Lib2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;

import { Lib3 } from "./systems/LibWrapperSystem.sol";

library Lib2 {
function call() public pure returns (string memory) {
return Lib3.call();
}
}
14 changes: 14 additions & 0 deletions e2e/packages/contracts/src/Lib4.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;

library Lib4 {
function call() public pure returns (string memory) {
return Lib5.call();
}
}

library Lib5 {
function call() public pure returns (string memory) {
return "success";
}
}
14 changes: 14 additions & 0 deletions e2e/packages/contracts/src/codegen/world/ILibWrapperSystem.sol

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion e2e/packages/contracts/src/codegen/world/IWorld.sol

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions e2e/packages/contracts/src/systems/LibWrapperSystem.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;

import { System } from "@latticexyz/world/src/System.sol";
import { Lib2 } from "../Lib2.sol";
import { Lib4 } from "../Lib4.sol";

contract LibWrapperSystem is System {
function callLib() public returns (string memory) {
return Lib1.call();
}

function callFreeFunc() public returns (string memory) {
return freeLibWrapper();
}
}

library Lib1 {
function call() public pure returns (string memory) {
return Lib2.call();
}
}

library Lib3 {
function call() public pure returns (string memory) {
return Lib4.call();
}
}

function freeLibWrapper() pure returns (string memory) {
return Lib1.call();
}
14 changes: 14 additions & 0 deletions e2e/packages/contracts/test/PublicLibrary.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;

import { MudTest } from "@latticexyz/world/test/MudTest.t.sol";

import { IWorld } from "../src/codegen/world/IWorld.sol";

contract PublicLibraryTest is MudTest {
// Test that the deployer can handle deeply nested public libraries
function testNesting() public {
assertEq(IWorld(worldAddress).callLib(), "success");
assertEq(IWorld(worldAddress).callFreeFunc(), "success");
}
}
2 changes: 2 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"path": "^0.12.7",
"rxjs": "7.5.5",
"throttle-debounce": "^5.0.0",
"toposort": "^2.0.2",
"typescript": "5.1.6",
"viem": "1.14.0",
"yargs": "^17.7.1",
Expand All @@ -72,6 +73,7 @@
"@types/node": "^18.15.11",
"@types/openurl": "^1.0.0",
"@types/throttle-debounce": "^5.0.0",
"@types/toposort": "^2.0.6",
"@types/yargs": "^17.0.10",
"ds-test": "https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0",
"forge-std": "https://github.com/foundry-rs/forge-std.git#74cfb77e308dd188d2f58864aaf44963ae6b88b1",
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/src/deploy/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,18 @@ export type Module = DeterministicContract & {
readonly installData: Hex; // TODO: figure out better naming for this
};

// https://docs.soliditylang.org/en/latest/using-the-compiler.html#library-linking
export type PublicLibrary = DeterministicContract & {
readonly fullyQualifiedName: string;
readonly filename: string;
readonly name: string;
readonly addressPlaceholder: string;
};

export type ConfigInput = StoreConfig & WorldConfig;
export type Config<config extends ConfigInput> = {
readonly tables: Tables<config>;
readonly systems: readonly System[];
readonly modules: readonly Module[];
readonly libraries: readonly PublicLibrary[];
};
15 changes: 14 additions & 1 deletion packages/cli/src/deploy/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { debug } from "./debug";
import { resourceLabel } from "./resourceLabel";
import { uniqueBy } from "@latticexyz/common/utils";
import { ensureContractsDeployed } from "./ensureContractsDeployed";
import { coreModuleBytecode, worldFactoryBytecode, worldFactoryContracts } from "./ensureWorldFactory";
import { worldFactoryContracts } from "./ensureWorldFactory";

type DeployOptions<configInput extends ConfigInput> = {
client: Client<Transport, Chain | undefined, Account>;
Expand All @@ -35,9 +35,22 @@ export async function deploy<configInput extends ConfigInput>({
}: DeployOptions<configInput>): Promise<WorldDeploy> {
const tables = Object.values(config.tables) as Table[];
const systems = Object.values(config.systems);
const libraries = Object.values(config.libraries);

await ensureDeployer(client);

// deploy all libraries ahead of other contracts
await ensureContractsDeployed({
client,
contracts: [
...uniqueBy(libraries, (library) => getAddress(library.address)).map((library) => ({
bytecode: library.bytecode,
deployedBytecodeSize: library.deployedBytecodeSize,
label: `${library.fullyQualifiedName} library`,
})),
],
});

// deploy all dependent contracts, because system registration, module install, etc. all expect these contracts to be callable.
await ensureContractsDeployed({
client,
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/deploy/ensureContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export async function ensureContract({
}: {
readonly client: Client<Transport, Chain | undefined, Account>;
} & Contract): Promise<readonly Hex[]> {
if (bytecode.includes("__$")) {
throw new Error(`Unlinked public library in: ${label}`);
}

const address = getCreate2Address({ from: deployer, salt, bytecode });

const contractCode = await getBytecode(client, { address, blockTag: "pending" });
Expand Down
79 changes: 79 additions & 0 deletions packages/cli/src/deploy/getPublicLibraries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { readFile } from "fs/promises";
import { getCreate2Address, keccak256, stringToHex } from "viem";
import toposort from "toposort";
import glob from "glob";
import { LinkReferences, getContractData } from "../utils/utils/getContractData";
import { deployer } from "./ensureDeployer";
import { PublicLibrary, salt } from "./common";
import path from "path";

export async function getPublicLibraries(forgeOutDir: string) {
const libraryDeps: {
libraryFilename: string;
libraryName: string;
contractFullyQualifiedName: string;
libraryFullyQualifiedName: string;
}[] = [];
const files = glob.sync(`${forgeOutDir}/**/*.json`, { ignore: "**/*.abi.json" });

for (const contractOutPath of files) {
const json = JSON.parse((await readFile(contractOutPath, "utf8")).trim());
const linkReferences = json.bytecode.linkReferences as LinkReferences;

const contractFullPath = Object.keys(json.metadata.settings.compilationTarget)[0];
holic marked this conversation as resolved.
Show resolved Hide resolved
// skip files that do not reference any contract/library
if (!json.metadata) continue;
const contractName = json.metadata.settings.compilationTarget[contractFullPath];

for (const [libraryFullPath, namePositions] of Object.entries(linkReferences)) {
const names = Object.keys(namePositions);
for (const libraryName of names) {
libraryDeps.push({
libraryFilename: path.basename(libraryFullPath),
libraryName,
contractFullyQualifiedName: `${contractFullPath}:${contractName}`,
libraryFullyQualifiedName: `${libraryFullPath}:${libraryName}`,
});
}
}
}

const directedGraphEdges: [string, string][] = libraryDeps.map(
({ contractFullyQualifiedName, libraryFullyQualifiedName }) => [
libraryFullyQualifiedName,
contractFullyQualifiedName,
],
);
const dependencyOrder = toposort(directedGraphEdges);

const orderedLibraryDeps = libraryDeps.sort((a, b) => {
return dependencyOrder.indexOf(a.libraryFullyQualifiedName) - dependencyOrder.indexOf(b.libraryFullyQualifiedName);
});

const libraries: PublicLibrary[] = [];
for (const { libraryFilename, libraryName, libraryFullyQualifiedName } of orderedLibraryDeps) {
const { bytecode, abi, deployedBytecodeSize } = getContractData(
libraryFilename,
libraryName,
forgeOutDir,
libraries,
);
const address = getCreate2Address({ from: deployer, bytecode, salt });

const hashPrefix = keccak256(stringToHex(libraryFullyQualifiedName)).slice(2, 36);
const addressPlaceholder = `__$${hashPrefix}$__`;

libraries.push({
address,
bytecode,
abi,
deployedBytecodeSize,
fullyQualifiedName: libraryFullyQualifiedName,
filename: libraryFilename,
name: libraryName,
addressPlaceholder,
});
}

return libraries;
}
27 changes: 15 additions & 12 deletions packages/cli/src/deploy/resolveConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
getCreate2Address,
getAddress,
hexToBytes,
Abi,
bytesToHex,
getFunctionSignature,
} from "viem";
Expand All @@ -20,24 +19,27 @@ import { getContractData } from "../utils/utils/getContractData";
import { configToTables } from "./configToTables";
import { deployer } from "./ensureDeployer";
import { resourceLabel } from "./resourceLabel";
import { getPublicLibraries } from "./getPublicLibraries";

// TODO: this should be replaced by https://github.com/latticexyz/mud/issues/1668

export function resolveConfig<config extends ConfigInput>({
export async function resolveConfig<config extends ConfigInput>({
config,
forgeSourceDir,
forgeOutDir,
}: {
config: config;
forgeSourceDir: string;
forgeOutDir: string;
}): Config<config> {
}): Promise<Config<config>> {
const libraries = await getPublicLibraries(forgeOutDir);

const tables = configToTables(config);

// TODO: should the config parser/loader help with resolving systems?
const contractNames = getExistingContracts(forgeSourceDir).map(({ basename }) => basename);
const resolvedConfig = resolveWorldConfig(config, contractNames);
const baseSystemContractData = getContractData("System", forgeOutDir);
const baseSystemContractData = getContractData("System.sol", "System", forgeOutDir, libraries);
const baseSystemFunctions = baseSystemContractData.abi
.filter((item): item is typeof item & { type: "function" } => item.type === "function")
.map(getFunctionSignature);
Expand All @@ -46,7 +48,7 @@ export function resolveConfig<config extends ConfigInput>({
const namespace = config.namespace;
const name = system.name;
const systemId = resourceToHex({ type: "system", namespace, name });
const contractData = getContractData(systemName, forgeOutDir);
const contractData = getContractData(`${systemName}.sol`, systemName, forgeOutDir, libraries);

const systemFunctions = contractData.abi
.filter((item): item is typeof item & { type: "function" } => item.type === "function")
Expand All @@ -71,7 +73,7 @@ export function resolveConfig<config extends ConfigInput>({
allowAll: system.openAccess,
allowedAddresses: system.accessListAddresses as Hex[],
allowedSystemIds: system.accessListSystems.map((name) =>
resourceToHex({ type: "system", namespace, name: resolvedConfig.systems[name].name })
resourceToHex({ type: "system", namespace, name: resolvedConfig.systems[name].name }),
),
address: getCreate2Address({ from: deployer, bytecode: contractData.bytecode, salt }),
bytecode: contractData.bytecode,
Expand All @@ -89,16 +91,16 @@ export function resolveConfig<config extends ConfigInput>({
if (!targetSystem) {
throw new Error(
`System ${resourceLabel(system)} wanted access to ${resourceLabel(
hexToResource(systemId)
)}, but it wasn't found in the config.`
hexToResource(systemId),
)}, but it wasn't found in the config.`,
);
}
return targetSystem.address;
});
return {
...system,
allowedAddresses: Array.from(
new Set([...allowedAddresses, ...allowedSystemAddresses].map((addr) => getAddress(addr)))
new Set([...allowedAddresses, ...allowedSystemAddresses].map((addr) => getAddress(addr))),
),
};
});
Expand All @@ -113,16 +115,16 @@ export function resolveConfig<config extends ConfigInput>({
type: table.offchainOnly ? "offchainTable" : "table",
namespace: config.namespace,
name: table.name,
})
}),
),
])
]),
),
};

const modules = config.modules.map((mod) => {
const contractData =
defaultModuleContracts.find((defaultMod) => defaultMod.name === mod.name) ??
getContractData(mod.name, forgeOutDir);
getContractData(`${mod.name}.sol`, mod.name, forgeOutDir, libraries);
const installArgs = mod.args
.map((arg) => resolveWithContext(arg, resolveContext))
.map((arg) => {
Expand All @@ -147,5 +149,6 @@ export function resolveConfig<config extends ConfigInput>({
tables,
systems: systemsWithAccess,
modules,
libraries,
};
}
Loading
Loading