From 706f2e0192d20a8ea95f5b8e701dc51e2ffcf497 Mon Sep 17 00:00:00 2001 From: alvarius Date: Mon, 6 Jan 2025 15:12:17 +0100 Subject: [PATCH 1/9] docs: add indexerUrl to deploy CLI docs (#3416) --- docs/pages/cli/deploy.mdx | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/docs/pages/cli/deploy.mdx b/docs/pages/cli/deploy.mdx index a81a0b56cc..676d363e60 100644 --- a/docs/pages/cli/deploy.mdx +++ b/docs/pages/cli/deploy.mdx @@ -37,21 +37,22 @@ Again, there are several ways to do this: These are the command line options you can specify on `mud deploy`: -| Option | Meaning | Type | Default value | -| ----------------------- | ------------------------------------------------ | ------- | ---------------------------------------------------------- | -| `--configPath` | Path to the config file | string | `mud.config.ts` | -| `--printConfig` | Print the resolved config | boolean | `false` | -| `--saveDeployment` | Save the deployment info to a file | boolean | `true` | -| `--profile` | The foundry profile to use | string | `local` | -| `--rpc`1 | The RPC URL to use | string | RPC url from `foundry.toml` | -| `--rpcBatch` | Enable batch processing of RPC requests | boolean | `false` | -| `--worldAddress` | Deploy to an existing World at the given address | string | Empty, deploy new `World` | -| `--srcDir` | Source directory | string | Foundry `src` directory | -| `--skipBuild` | Skip rebuilding the contracts before deploying | boolean | `false` | -| `--alwaysRunPostDeploy` | Run `PostDeploy.s.sol` after each deploy | boolean | `false` (run the script only when deploying a new `World`) | -| `--help` | Show help | boolean | `false` | -| `--version` | Show version number | boolean | `false` | -| `--forgeScriptOptions` | Command line options for forge | string | empty | +| Option | Meaning | Type | Default value | +| ----------------------- | -------------------------------------------------------------------------------- | ------- | ---------------------------------------------------------- | +| `--configPath` | Path to the config file | string | `mud.config.ts` | +| `--printConfig` | Print the resolved config | boolean | `false` | +| `--saveDeployment` | Save the deployment info to a file | boolean | `true` | +| `--profile` | The foundry profile to use | string | `local` | +| `--rpc`1 | The RPC URL to use | string | RPC url from `foundry.toml` | +| `--rpcBatch` | Enable batch processing of RPC requests | boolean | `false` | +| `--worldAddress` | Deploy to an existing World at the given address | string | Empty, deploy new `World` | +| `--srcDir` | Source directory | string | Foundry `src` directory | +| `--skipBuild` | Skip rebuilding the contracts before deploying | boolean | `false` | +| `--alwaysRunPostDeploy` | Run `PostDeploy.s.sol` after each deploy | boolean | `false` (run the script only when deploying a new `World`) | +| `--help` | Show help | boolean | `false` | +| `--version` | Show version number | boolean | `false` | +| `--forgeScriptOptions` | Command line options for forge | string | empty | +| `--indexerUrl` | If provided, read records from the indexer instead of fetching logs from the RPC | string | empty | (1) The hostname `localhost` may not work. If that is the case, use `127.0.0.1` instead. From 1e092407da570f8ba52e89e73dda50cdff161a93 Mon Sep 17 00:00:00 2001 From: Jackie Xu Date: Mon, 6 Jan 2025 17:09:01 +0100 Subject: [PATCH 2/9] feat(cli): fetch world deploy block number if available (#3417) Co-authored-by: alvrs --- .changeset/curly-apples-bake.md | 5 +++++ packages/cli/src/deploy/deploy.ts | 8 +++++++- packages/cli/src/deploy/getWorldDeploy.ts | 17 ++++++++++++----- packages/cli/src/runDeploy.ts | 22 ++++++++++++++++++++++ 4 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 .changeset/curly-apples-bake.md diff --git a/.changeset/curly-apples-bake.md b/.changeset/curly-apples-bake.md new file mode 100644 index 0000000000..4f2a8075f7 --- /dev/null +++ b/.changeset/curly-apples-bake.md @@ -0,0 +1,5 @@ +--- +"@latticexyz/cli": patch +--- + +When upgrading an existing world, the deployer now attempts to read the deploy block number from the `worlds.json` file. If it is found, the `HelloWorld` and `HelloStore` event are fetched from this block instead of searching for the events starting from the genesis block. diff --git a/packages/cli/src/deploy/deploy.ts b/packages/cli/src/deploy/deploy.ts index 6d6e7c427a..d2d00f3906 100644 --- a/packages/cli/src/deploy/deploy.ts +++ b/packages/cli/src/deploy/deploy.ts @@ -38,6 +38,11 @@ type DeployOptions = { artifacts: readonly ContractArtifact[]; salt?: Hex; worldAddress?: Address; + /** + * Block number of an existing world deployment. + * Only used if `worldAddress` is provided. + */ + worldDeployBlock?: bigint; /** * Address of determinstic deployment proxy: https://github.com/Arachnid/deterministic-deployment-proxy * By default, we look for a deployment at 0x4e59b44847b379578588920ca78fbf26c0b4956c and, if not, deploy one. @@ -64,6 +69,7 @@ export async function deploy({ artifacts, salt, worldAddress: existingWorldAddress, + worldDeployBlock, deployerAddress: initialDeployerAddress, indexerUrl, chainId, @@ -71,7 +77,7 @@ export async function deploy({ const deployerAddress = initialDeployerAddress ?? (await ensureDeployer(client)); const worldDeploy = existingWorldAddress - ? await getWorldDeploy(client, existingWorldAddress) + ? await getWorldDeploy(client, existingWorldAddress, worldDeployBlock) : config.deploy.customWorld ? await deployCustomWorld({ client, diff --git a/packages/cli/src/deploy/getWorldDeploy.ts b/packages/cli/src/deploy/getWorldDeploy.ts index 86982740ff..5259a7488f 100644 --- a/packages/cli/src/deploy/getWorldDeploy.ts +++ b/packages/cli/src/deploy/getWorldDeploy.ts @@ -7,7 +7,11 @@ import { logsToWorldDeploy } from "./logsToWorldDeploy"; const deploys = new Map(); -export async function getWorldDeploy(client: Client, worldAddress: Address): Promise { +export async function getWorldDeploy( + client: Client, + worldAddress: Address, + deployBlock?: bigint, +): Promise { const address = getAddress(worldAddress); let deploy = deploys.get(address); @@ -20,10 +24,9 @@ export async function getWorldDeploy(client: Client, worldAddress: Address): Pro debug("looking up world deploy for", address); - const [fromBlock, toBlock] = await Promise.all([ - getBlock(client, { blockTag: "earliest" }), - getBlock(client, { blockTag: "latest" }), - ]); + const [fromBlock, toBlock] = deployBlock + ? [{ number: deployBlock }, { number: deployBlock }] + : await Promise.all([getBlock(client, { blockTag: "earliest" }), getBlock(client, { blockTag: "latest" })]); const blockLogs = await fetchBlockLogs({ publicClient: client, @@ -34,6 +37,10 @@ export async function getWorldDeploy(client: Client, worldAddress: Address): Pro maxBlockRange: 100_000n, }); + if (blockLogs.length === 0) { + throw new Error("could not find `HelloWorld` or `HelloStore` event"); + } + deploy = { ...logsToWorldDeploy(blockLogs.flatMap((block) => block.logs)), stateBlock: toBlock.number, diff --git a/packages/cli/src/runDeploy.ts b/packages/cli/src/runDeploy.ts index 4f66c180d0..b2c1ce4c0d 100644 --- a/packages/cli/src/runDeploy.ts +++ b/packages/cli/src/runDeploy.ts @@ -144,6 +144,13 @@ export async function runDeploy(opts: DeployOptions): Promise { const chainId = await getChainId(client); const indexerUrl = opts.indexerUrl ?? defaultChains.find((chain) => chain.id === chainId)?.indexerUrl; + const worldDeployBlock = opts.worldAddress + ? getWorldDeployBlock({ + worldAddress: opts.worldAddress, + worldsFile: config.deploy.worldsFile, + chainId, + }) + : undefined; console.log("Deploying from", client.account.address); @@ -156,6 +163,7 @@ export async function runDeploy(opts: DeployOptions): Promise { deployerAddress: opts.deployerAddress as Hex | undefined, salt, worldAddress: opts.worldAddress as Hex | undefined, + worldDeployBlock, client, tables, systems, @@ -215,3 +223,17 @@ export async function runDeploy(opts: DeployOptions): Promise { return worldDeploy; } + +function getWorldDeployBlock({ + chainId, + worldAddress, + worldsFile, +}: { + chainId: number; + worldAddress: string; + worldsFile: string; +}): bigint | undefined { + const deploys = existsSync(worldsFile) ? JSON.parse(readFileSync(worldsFile, "utf-8")) : {}; + const worldDeployBlock = deploys[chainId]?.address === worldAddress ? deploys[chainId].blockNumber : undefined; + return worldDeployBlock ? BigInt(worldDeployBlock) : undefined; +} From 6aff69a91c627ddcc0c1dbccf7b71580e8e0a8fa Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Tue, 7 Jan 2025 08:14:27 -0800 Subject: [PATCH 3/9] chore: allow unused vars with _ prefix (#3420) --- .eslintrc | 10 ++++++++++ .gitignore | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index b67a090914..ed24fbd425 100644 --- a/.eslintrc +++ b/.eslintrc @@ -19,5 +19,15 @@ }, }, ], + // disable the base rule as it can report incorrect errors + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_", + }, + ], }, } diff --git a/.gitignore b/.gitignore index 610cfff0db..00b3d0e4ad 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,10 @@ node_modules package-lock.json yarn.lock -*.log +# npm pack +*.tgz +*.log .eslintcache .parcel-cache .docs From 1a2b3c8f585e72b0f8aae42a1c11abef7193059b Mon Sep 17 00:00:00 2001 From: Karolis Ramanauskas Date: Tue, 7 Jan 2025 18:42:45 +0200 Subject: [PATCH 4/9] feat(explorer): show event logs for interact function (#3418) --- .changeset/fresh-chairs-obey.md | 5 + .../[worldAddress]/interact/FunctionField.tsx | 222 ++++++++++++------ .../[worldAddress]/interact/InteractForm.tsx | 2 +- .../interact/useContractMutation.ts | 80 ------- .../observe/TransactionTableRow.tsx | 59 +++-- 5 files changed, 193 insertions(+), 175 deletions(-) create mode 100644 .changeset/fresh-chairs-obey.md delete mode 100644 packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/useContractMutation.ts diff --git a/.changeset/fresh-chairs-obey.md b/.changeset/fresh-chairs-obey.md new file mode 100644 index 0000000000..133f5f2727 --- /dev/null +++ b/.changeset/fresh-chairs-obey.md @@ -0,0 +1,5 @@ +--- +"@latticexyz/explorer": patch +--- + +The functions in the Interact tab now display the emitted logs with the block explorer URL for the submitted transaction. diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/FunctionField.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/FunctionField.tsx index 5c99e7522a..4b429cd10b 100644 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/FunctionField.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/FunctionField.tsx @@ -1,8 +1,12 @@ "use client"; -import { Coins, Eye, Send } from "lucide-react"; -import { Abi, AbiFunction } from "viem"; -import { useAccount } from "wagmi"; +import { Coins, ExternalLinkIcon, Eye, LoaderIcon, Send } from "lucide-react"; +import Link from "next/link"; +import { useParams } from "next/navigation"; +import { toast } from "sonner"; +import { Abi, AbiFunction, Address, Hex, decodeEventLog } from "viem"; +import { useAccount, useConfig } from "wagmi"; +import { readContract, waitForTransactionReceipt, writeContract } from "wagmi/actions"; import { z } from "zod"; import { useState } from "react"; import { useForm } from "react-hook-form"; @@ -12,7 +16,8 @@ import { Button } from "../../../../../../components/ui/Button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "../../../../../../components/ui/Form"; import { Input } from "../../../../../../components/ui/Input"; import { Separator } from "../../../../../../components/ui/Separator"; -import { useContractMutation } from "./useContractMutation"; +import { useChain } from "../../../../hooks/useChain"; +import { blockExplorerTransactionUrl } from "../../../../utils/blockExplorerTransactionUrl"; export enum FunctionType { READ, @@ -24,6 +29,11 @@ type Props = { functionAbi: AbiFunction; }; +type DecodedEvent = { + eventName: string | undefined; + args: readonly unknown[] | undefined; +}; + const formSchema = z.object({ inputs: z.array(z.string()), value: z.string().optional(), @@ -34,10 +44,16 @@ export function FunctionField({ worldAbi, functionAbi }: Props) { functionAbi.stateMutability === "view" || functionAbi.stateMutability === "pure" ? FunctionType.READ : FunctionType.WRITE; - const [result, setResult] = useState(null); const { openConnectModal } = useConnectModal(); - const mutation = useContractMutation({ worldAbi, functionAbi, operationType }); + const wagmiConfig = useConfig(); const account = useAccount(); + const { worldAddress } = useParams(); + const { id: chainId } = useChain(); + const [isLoading, setIsLoading] = useState(false); + const [result, setResult] = useState(); + const [events, setEvents] = useState(); + const [txHash, setTxHash] = useState(); + const txUrl = blockExplorerTransactionUrl({ hash: txHash, chainId }); const form = useForm>({ resolver: zodResolver(formSchema), @@ -51,74 +67,142 @@ export function FunctionField({ worldAbi, functionAbi }: Props) { return openConnectModal?.(); } - const mutationResult = await mutation.mutateAsync({ - inputs: values.inputs, - value: values.value, - }); + setIsLoading(true); + let toastId; + try { + if (operationType === FunctionType.READ) { + const result = await readContract(wagmiConfig, { + abi: worldAbi, + address: worldAddress as Address, + functionName: functionAbi.name, + args: values.inputs, + chainId, + }); + + setResult(JSON.stringify(result, null, 2)); + } else { + toastId = toast.loading("Transaction submitted"); + const txHash = await writeContract(wagmiConfig, { + abi: worldAbi, + address: worldAddress as Address, + functionName: functionAbi.name, + args: values.inputs, + ...(values.value && { value: BigInt(values.value) }), + chainId, + }); + setTxHash(txHash); - if (operationType === FunctionType.READ && "result" in mutationResult) { - setResult(JSON.stringify(mutationResult.result, null, 2)); + const receipt = await waitForTransactionReceipt(wagmiConfig, { hash: txHash }); + const events = receipt?.logs.map((log) => decodeEventLog({ ...log, abi: worldAbi })); + setEvents(events); + + toast.success(`Transaction successful with hash: ${txHash}`, { + id: toastId, + }); + } + } catch (error) { + console.error(error); + toast.error((error as Error).message || "Something went wrong. Please try again.", { + id: toastId, + }); + } finally { + setIsLoading(false); } } const inputsLabel = functionAbi?.inputs.map((input) => input.type).join(", "); return ( -
- -

- {functionAbi?.name} - {inputsLabel && ` (${inputsLabel})`} - - {functionAbi.stateMutability === "payable" && } - {(functionAbi.stateMutability === "view" || functionAbi.stateMutability === "pure") && ( - - )} - {functionAbi.stateMutability === "nonpayable" && } - -

- - {functionAbi?.inputs.map((input, index) => ( - ( - - {input.name} - - - - - - )} - /> - ))} - - {functionAbi.stateMutability === "payable" && ( - ( - - ETH value - - - - - - )} - /> - )} - - - - {result &&
{result}
} - - - - +
+
+ +

+ {functionAbi?.name} + {inputsLabel && ` (${inputsLabel})`} + + {functionAbi.stateMutability === "payable" && } + {(functionAbi.stateMutability === "view" || functionAbi.stateMutability === "pure") && ( + + )} + {functionAbi.stateMutability === "nonpayable" && } + +

+ + {functionAbi?.inputs.map((input, index) => ( + ( + + {input.name} + + + + + + )} + /> + ))} + + {functionAbi.stateMutability === "payable" && ( + ( + + ETH value + + + + + + )} + /> + )} + + + + + + {result &&
{result}
} + {events && ( +
+
    + {events.map((event, idx) => ( +
  • + {event.eventName && {event.eventName}:} + {event.args && ( +
      + {Object.entries(event.args).map(([key, value]) => ( +
    • + {key}:{" "} + {String(value)} +
    • + ))} +
    + )} + {idx < events.length - 1 && } +
  • + ))} +
+
+ )} + {txUrl && ( +
+ + View on block explorer + +
+ )} + + +
); } diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/InteractForm.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/InteractForm.tsx index 763a6e04d3..b32e3b38b0 100644 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/InteractForm.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/InteractForm.tsx @@ -44,7 +44,7 @@ export function InteractForm() { {!isFetched && Array.from({ length: 10 }).map((_, index) => { return ( -
  • +
  • ); diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/useContractMutation.ts b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/useContractMutation.ts deleted file mode 100644 index 9411e055f1..0000000000 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/useContractMutation.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { useParams } from "next/navigation"; -import { toast } from "sonner"; -import { Abi, AbiFunction, Hex } from "viem"; -import { useAccount, useConfig } from "wagmi"; -import { readContract, waitForTransactionReceipt, writeContract } from "wagmi/actions"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { useChain } from "../../../../hooks/useChain"; -import { FunctionType } from "./FunctionField"; - -type UseContractMutationProps = { - worldAbi: Abi; - functionAbi: AbiFunction; - operationType: FunctionType; -}; - -export function useContractMutation({ worldAbi, functionAbi, operationType }: UseContractMutationProps) { - const { worldAddress } = useParams(); - const { id: chainId } = useChain(); - const queryClient = useQueryClient(); - const wagmiConfig = useConfig(); - const account = useAccount(); - - return useMutation({ - mutationFn: async ({ inputs, value }: { inputs: unknown[]; value?: string }) => { - if (operationType === FunctionType.READ) { - const result = await readContract(wagmiConfig, { - abi: worldAbi, - address: worldAddress as Hex, - functionName: functionAbi.name, - args: inputs, - chainId, - }); - - return { result }; - } else { - const txHash = await writeContract(wagmiConfig, { - abi: worldAbi, - address: worldAddress as Hex, - functionName: functionAbi.name, - args: inputs, - ...(value && { value: BigInt(value) }), - chainId, - }); - - const receipt = await waitForTransactionReceipt(wagmiConfig, { hash: txHash }); - - return { txHash, receipt }; - } - }, - onMutate: () => { - if (operationType === FunctionType.WRITE) { - const toastId = toast.loading("Transaction submitted"); - return { toastId }; - } - }, - onSuccess: (data, _, context) => { - if (operationType === FunctionType.WRITE && "txHash" in data) { - toast.success(`Transaction successful with hash: ${data.txHash}`, { - id: context?.toastId, - }); - } - - queryClient.invalidateQueries({ - queryKey: [ - "balance", - { - address: account, - chainId, - }, - ], - }); - }, - onError: (error: Error, _, context) => { - console.error("Error:", error); - toast.error(error.message || "Something went wrong. Please try again.", { - id: context?.toastId, - }); - }, - }); -} diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TransactionTableRow.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TransactionTableRow.tsx index 8d808b3430..ae6b2c544e 100644 --- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TransactionTableRow.tsx +++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TransactionTableRow.tsx @@ -35,6 +35,7 @@ export function TransactionTableRow({ row }: { row: Row }) const status = data.status; const logs = data?.logs; const receipt = data?.receipt; + const calls = data?.calls.filter((call) => call.args && call.args.length > 0); return ( <> @@ -78,34 +79,42 @@ export function TransactionTableRow({ row }: { row: Row }) - -
    -

    Inputs

    + {calls.length > 0 ? ( + <> + +
    +

    Inputs

    - {data.calls.length > 0 ? ( -
    - {data.calls.map((call, idx) => ( -
    - {call.functionName}: - {call.args?.map((arg, argIdx) => ( -
    - arg {argIdx + 1}: - - {typeof arg === "object" && arg !== null ? JSON.stringify(arg, null, 2) : String(arg)} - -
    - ))} +
    + {calls.map((call, idx) => { + if (!call.args || call.args.length === 0) { + return null; + } - {call.value && call.value > 0n ? ( -
    value: {formatEther(call.value)} ETH
    - ) : null} -
    - ))} + return ( +
    + {call.functionName}: + {call.args?.map((arg, argIdx) => ( +
    + arg {argIdx + 1}: + + {typeof arg === "object" && arg !== null + ? JSON.stringify(arg, null, 2) + : String(arg)} + +
    + ))} + + {call.value && call.value > 0n ? ( +
    value: {formatEther(call.value)} ETH
    + ) : null} +
    + ); + })} +
    - ) : ( -

    No inputs

    - )} -
    + + ) : null} {data.error ? ( <> From f2c69f4d1600c9dd2fc79bd0002ba7c43250b98c Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Tue, 7 Jan 2025 09:10:59 -0800 Subject: [PATCH 5/9] refactor(cli): move deploy utils to common (#3421) --- packages/cli/src/deploy/common.ts | 7 +---- .../cli/src/deploy/createPrepareDeploy.ts | 17 +++++++---- packages/cli/src/deploy/deploy.ts | 4 +-- packages/cli/src/deploy/deployCustomWorld.ts | 12 ++++---- packages/cli/src/deploy/ensureModules.ts | 2 +- packages/cli/src/deploy/ensureResourceTags.ts | 2 +- packages/cli/src/deploy/ensureSystems.ts | 2 +- packages/cli/src/deploy/ensureWorldFactory.ts | 2 +- packages/cli/src/deploy/getWorldContracts.ts | 23 +++++++-------- .../src/deploy/getWorldFactoryContracts.ts | 6 ++-- .../deploy/getWorldProxyFactoryContracts.ts | 6 ++-- packages/cli/src/verify.ts | 16 +++++------ packages/common/package.json | 4 +++ packages/common/src/deploy/common.ts | 7 +++++ .../src/deploy/create2/README.md | 0 .../src/deploy/create2/deployment.json | 0 packages/common/src/deploy/debug.ts | 10 +++++++ .../src/deploy/ensureContract.ts | 28 +++++++------------ .../src/deploy/ensureContractsDeployed.ts | 4 +-- .../src/deploy/ensureDeployer.ts | 0 .../common/src/deploy/getContractAddress.ts | 12 ++++++++ .../{cli => common}/src/deploy/getDeployer.ts | 6 ++-- packages/common/src/exports/internal.ts | 6 ++++ .../src}/waitForTransactions.ts | 1 + packages/common/tsup.config.ts | 1 + 25 files changed, 101 insertions(+), 77 deletions(-) create mode 100644 packages/common/src/deploy/common.ts rename packages/{cli => common}/src/deploy/create2/README.md (100%) rename packages/{cli => common}/src/deploy/create2/deployment.json (100%) create mode 100644 packages/common/src/deploy/debug.ts rename packages/{cli => common}/src/deploy/ensureContract.ts (71%) rename packages/{cli => common}/src/deploy/ensureContractsDeployed.ts (87%) rename packages/{cli => common}/src/deploy/ensureDeployer.ts (100%) create mode 100644 packages/common/src/deploy/getContractAddress.ts rename packages/{cli => common}/src/deploy/getDeployer.ts (81%) create mode 100644 packages/common/src/exports/internal.ts rename packages/{cli/src/deploy => common/src}/waitForTransactions.ts (95%) diff --git a/packages/cli/src/deploy/common.ts b/packages/cli/src/deploy/common.ts index 3cef84101b..e66902133b 100644 --- a/packages/cli/src/deploy/common.ts +++ b/packages/cli/src/deploy/common.ts @@ -1,14 +1,9 @@ -import { Abi, Account, Address, Chain, Client, Hex, Transport, padHex } from "viem"; +import { Abi, Account, Address, Chain, Client, Hex, Transport } from "viem"; import IBaseWorldAbi from "@latticexyz/world/out/IBaseWorld.sol/IBaseWorld.abi.json" assert { type: "json" }; import { helloStoreEvent } from "@latticexyz/store"; import { helloWorldEvent } from "@latticexyz/world"; import { LibraryMap } from "./getLibraryMap"; -export const salt = padHex("0x", { size: 32 }); - -// https://eips.ethereum.org/EIPS/eip-170 -export const contractSizeLimit = parseInt("6000", 16); - export const worldDeployEvents = [helloStoreEvent, helloWorldEvent] as const; export const worldAbi = IBaseWorldAbi; diff --git a/packages/cli/src/deploy/createPrepareDeploy.ts b/packages/cli/src/deploy/createPrepareDeploy.ts index 065ade5e0b..00f2d1c621 100644 --- a/packages/cli/src/deploy/createPrepareDeploy.ts +++ b/packages/cli/src/deploy/createPrepareDeploy.ts @@ -1,17 +1,18 @@ -import { DeterministicContract, LibraryPlaceholder, salt } from "./common"; +import { DeterministicContract, LibraryPlaceholder } from "./common"; import { spliceHex } from "@latticexyz/common"; -import { Hex, getCreate2Address, Address } from "viem"; +import { Hex, Address } from "viem"; import { LibraryMap } from "./getLibraryMap"; +import { getContractAddress } from "@latticexyz/common/internal"; export function createPrepareDeploy( bytecodeWithPlaceholders: Hex, placeholders: readonly LibraryPlaceholder[], ): DeterministicContract["prepareDeploy"] { - return function prepareDeploy(deployer: Address, libraryMap?: LibraryMap) { + return function prepareDeploy(deployerAddress: Address, libraryMap?: LibraryMap) { let bytecode = bytecodeWithPlaceholders; if (placeholders.length === 0) { - return { bytecode, address: getCreate2Address({ from: deployer, bytecode, salt }) }; + return { bytecode, address: getContractAddress({ deployerAddress, bytecode }) }; } if (!libraryMap) { @@ -19,12 +20,16 @@ export function createPrepareDeploy( } for (const placeholder of placeholders) { - const address = libraryMap.getAddress({ name: placeholder.name, path: placeholder.path, deployer }); + const address = libraryMap.getAddress({ + name: placeholder.name, + path: placeholder.path, + deployer: deployerAddress, + }); bytecode = spliceHex(bytecode, placeholder.start, placeholder.length, address); } return { bytecode, - address: getCreate2Address({ from: deployer, bytecode, salt }), + address: getContractAddress({ deployerAddress, bytecode }), }; }; } diff --git a/packages/cli/src/deploy/deploy.ts b/packages/cli/src/deploy/deploy.ts index d2d00f3906..853300632b 100644 --- a/packages/cli/src/deploy/deploy.ts +++ b/packages/cli/src/deploy/deploy.ts @@ -1,5 +1,4 @@ import { Address, Hex, stringToHex } from "viem"; -import { ensureDeployer } from "./ensureDeployer"; import { deployWorld } from "./deployWorld"; import { ensureTables } from "./ensureTables"; import { @@ -18,16 +17,15 @@ import { ensureModules } from "./ensureModules"; import { ensureNamespaceOwner } from "./ensureNamespaceOwner"; import { debug } from "./debug"; import { resourceToHex, resourceToLabel } from "@latticexyz/common"; -import { ensureContractsDeployed } from "./ensureContractsDeployed"; import { randomBytes } from "crypto"; import { Table } from "@latticexyz/config"; import { ensureResourceTags } from "./ensureResourceTags"; -import { waitForTransactions } from "./waitForTransactions"; import { ContractArtifact } from "@latticexyz/world/node"; import { World } from "@latticexyz/world"; import { deployCustomWorld } from "./deployCustomWorld"; import { uniqueBy } from "@latticexyz/common/utils"; import { getLibraryMap } from "./getLibraryMap"; +import { ensureContractsDeployed, ensureDeployer, waitForTransactions } from "@latticexyz/common/internal"; type DeployOptions = { config: World; diff --git a/packages/cli/src/deploy/deployCustomWorld.ts b/packages/cli/src/deploy/deployCustomWorld.ts index 54016b47c9..ad6b2f4226 100644 --- a/packages/cli/src/deploy/deployCustomWorld.ts +++ b/packages/cli/src/deploy/deployCustomWorld.ts @@ -1,14 +1,13 @@ -import { Account, Chain, Client, Hex, Transport, concatHex, encodeDeployData, getCreate2Address, isHex } from "viem"; +import { Account, Chain, Client, Hex, Transport, concatHex, encodeDeployData, isHex } from "viem"; import { waitForTransactionReceipt } from "viem/actions"; import { resourceToHex, sendTransaction, writeContract } from "@latticexyz/common"; import { debug } from "./debug"; import { logsToWorldDeploy } from "./logsToWorldDeploy"; -import { WorldDeploy, salt, worldAbi } from "./common"; +import { WorldDeploy, worldAbi } from "./common"; import { getWorldContracts } from "./getWorldContracts"; -import { ensureContractsDeployed } from "./ensureContractsDeployed"; +import { ensureContractsDeployed, getContractAddress, waitForTransactions } from "@latticexyz/common/internal"; import { ContractArtifact, ReferenceIdentifier } from "@latticexyz/world/node"; import { World } from "@latticexyz/world"; -import { waitForTransactions } from "./waitForTransactions"; function findArtifact(ref: ReferenceIdentifier, artifacts: readonly ContractArtifact[]): ContractArtifact { const artifact = artifacts.find((a) => a.sourcePath === ref.sourcePath && a.name === ref.name); @@ -31,9 +30,8 @@ function getDeployable(deployerAddress: Hex, artifact: ContractArtifact, artifac return concatHex( artifact.bytecode.map((ref): Hex => { if (isHex(ref)) return ref; - return getCreate2Address({ - from: deployerAddress, - salt, + return getContractAddress({ + deployerAddress, bytecode: getDeployable(deployerAddress, findArtifact(ref, artifacts), artifacts), }); }), diff --git a/packages/cli/src/deploy/ensureModules.ts b/packages/cli/src/deploy/ensureModules.ts index 5dc2128514..ea68626b81 100644 --- a/packages/cli/src/deploy/ensureModules.ts +++ b/packages/cli/src/deploy/ensureModules.ts @@ -4,8 +4,8 @@ import { Module, WorldDeploy, worldAbi } from "./common"; import { debug } from "./debug"; import { isDefined } from "@latticexyz/common/utils"; import pRetry from "p-retry"; -import { ensureContractsDeployed } from "./ensureContractsDeployed"; import { LibraryMap } from "./getLibraryMap"; +import { ensureContractsDeployed } from "@latticexyz/common/internal"; export async function ensureModules({ client, diff --git a/packages/cli/src/deploy/ensureResourceTags.ts b/packages/cli/src/deploy/ensureResourceTags.ts index 0b286e954a..4bd855f347 100644 --- a/packages/cli/src/deploy/ensureResourceTags.ts +++ b/packages/cli/src/deploy/ensureResourceTags.ts @@ -8,11 +8,11 @@ import { ensureModules } from "./ensureModules"; import metadataModule from "@latticexyz/world-module-metadata/out/MetadataModule.sol/MetadataModule.json" assert { type: "json" }; import { getContractArtifact } from "../utils/getContractArtifact"; import { createPrepareDeploy } from "./createPrepareDeploy"; -import { waitForTransactions } from "./waitForTransactions"; import { LibraryMap } from "./getLibraryMap"; import { getKeyTuple, getSchemaPrimitives } from "@latticexyz/protocol-parser/internal"; import { getRecords } from "@latticexyz/store-sync"; import { CommonDeployOptions } from "./common"; +import { waitForTransactions } from "@latticexyz/common/internal"; const metadataModuleArtifact = getContractArtifact(metadataModule); diff --git a/packages/cli/src/deploy/ensureSystems.ts b/packages/cli/src/deploy/ensureSystems.ts index 0db527850d..1cabbf2423 100644 --- a/packages/cli/src/deploy/ensureSystems.ts +++ b/packages/cli/src/deploy/ensureSystems.ts @@ -5,8 +5,8 @@ import { debug } from "./debug"; import { getSystems } from "./getSystems"; import { getResourceAccess } from "./getResourceAccess"; import pRetry from "p-retry"; -import { ensureContractsDeployed } from "./ensureContractsDeployed"; import { LibraryMap } from "./getLibraryMap"; +import { ensureContractsDeployed } from "@latticexyz/common/internal"; // TODO: move each system registration+access to batch call to be atomic diff --git a/packages/cli/src/deploy/ensureWorldFactory.ts b/packages/cli/src/deploy/ensureWorldFactory.ts index 3343755def..8f00e6cbdc 100644 --- a/packages/cli/src/deploy/ensureWorldFactory.ts +++ b/packages/cli/src/deploy/ensureWorldFactory.ts @@ -1,7 +1,7 @@ import { Client, Transport, Chain, Account, Hex, Address } from "viem"; -import { ensureContractsDeployed } from "./ensureContractsDeployed"; import { getWorldFactoryContracts } from "./getWorldFactoryContracts"; import { getWorldProxyFactoryContracts } from "./getWorldProxyFactoryContracts"; +import { ensureContractsDeployed } from "@latticexyz/common/internal"; export async function ensureWorldFactory( client: Client, diff --git a/packages/cli/src/deploy/getWorldContracts.ts b/packages/cli/src/deploy/getWorldContracts.ts index c20a01fad4..f0de3388be 100644 --- a/packages/cli/src/deploy/getWorldContracts.ts +++ b/packages/cli/src/deploy/getWorldContracts.ts @@ -4,36 +4,33 @@ import batchCallSystemBuild from "@latticexyz/world/out/BatchCallSystem.sol/Batc import registrationSystemBuild from "@latticexyz/world/out/RegistrationSystem.sol/RegistrationSystem.json" assert { type: "json" }; import initModuleBuild from "@latticexyz/world/out/InitModule.sol/InitModule.json" assert { type: "json" }; import initModuleAbi from "@latticexyz/world/out/InitModule.sol/InitModule.abi.json" assert { type: "json" }; -import { Hex, getCreate2Address, encodeDeployData, size } from "viem"; -import { salt } from "./common"; +import { Hex, encodeDeployData, size } from "viem"; +import { getContractAddress } from "@latticexyz/common/internal"; export function getWorldContracts(deployerAddress: Hex) { const accessManagementSystemDeployedBytecodeSize = size(accessManagementSystemBuild.deployedBytecode.object as Hex); const accessManagementSystemBytecode = accessManagementSystemBuild.bytecode.object as Hex; - const accessManagementSystem = getCreate2Address({ - from: deployerAddress, + const accessManagementSystem = getContractAddress({ + deployerAddress, bytecode: accessManagementSystemBytecode, - salt, }); const balanceTransferSystemDeployedBytecodeSize = size(balanceTransferSystemBuild.deployedBytecode.object as Hex); const balanceTransferSystemBytecode = balanceTransferSystemBuild.bytecode.object as Hex; - const balanceTransferSystem = getCreate2Address({ - from: deployerAddress, + const balanceTransferSystem = getContractAddress({ + deployerAddress, bytecode: balanceTransferSystemBytecode, - salt, }); const batchCallSystemDeployedBytecodeSize = size(batchCallSystemBuild.deployedBytecode.object as Hex); const batchCallSystemBytecode = batchCallSystemBuild.bytecode.object as Hex; - const batchCallSystem = getCreate2Address({ from: deployerAddress, bytecode: batchCallSystemBytecode, salt }); + const batchCallSystem = getContractAddress({ deployerAddress, bytecode: batchCallSystemBytecode }); const registrationDeployedBytecodeSize = size(registrationSystemBuild.deployedBytecode.object as Hex); const registrationBytecode = registrationSystemBuild.bytecode.object as Hex; - const registration = getCreate2Address({ - from: deployerAddress, + const registration = getContractAddress({ + deployerAddress, bytecode: registrationBytecode, - salt, }); const initModuleDeployedBytecodeSize = size(initModuleBuild.deployedBytecode.object as Hex); @@ -42,7 +39,7 @@ export function getWorldContracts(deployerAddress: Hex) { abi: initModuleAbi, args: [accessManagementSystem, balanceTransferSystem, batchCallSystem, registration], }); - const initModule = getCreate2Address({ from: deployerAddress, bytecode: initModuleBytecode, salt }); + const initModule = getContractAddress({ deployerAddress, bytecode: initModuleBytecode }); return { AccessManagementSystem: { diff --git a/packages/cli/src/deploy/getWorldFactoryContracts.ts b/packages/cli/src/deploy/getWorldFactoryContracts.ts index f98acbc380..572fabfe55 100644 --- a/packages/cli/src/deploy/getWorldFactoryContracts.ts +++ b/packages/cli/src/deploy/getWorldFactoryContracts.ts @@ -1,8 +1,8 @@ import worldFactoryBuild from "@latticexyz/world/out/WorldFactory.sol/WorldFactory.json" assert { type: "json" }; import worldFactoryAbi from "@latticexyz/world/out/WorldFactory.sol/WorldFactory.abi.json" assert { type: "json" }; -import { Hex, getCreate2Address, encodeDeployData, size } from "viem"; -import { salt } from "./common"; +import { Hex, encodeDeployData, size } from "viem"; import { getWorldContracts } from "./getWorldContracts"; +import { getContractAddress } from "@latticexyz/common/internal"; export function getWorldFactoryContracts(deployerAddress: Hex) { const worldContracts = getWorldContracts(deployerAddress); @@ -13,7 +13,7 @@ export function getWorldFactoryContracts(deployerAddress: Hex) { abi: worldFactoryAbi, args: [worldContracts.InitModule.address], }); - const worldFactory = getCreate2Address({ from: deployerAddress, bytecode: worldFactoryBytecode, salt }); + const worldFactory = getContractAddress({ deployerAddress, bytecode: worldFactoryBytecode }); return { ...worldContracts, diff --git a/packages/cli/src/deploy/getWorldProxyFactoryContracts.ts b/packages/cli/src/deploy/getWorldProxyFactoryContracts.ts index 5662df94a5..d291dbf2ab 100644 --- a/packages/cli/src/deploy/getWorldProxyFactoryContracts.ts +++ b/packages/cli/src/deploy/getWorldProxyFactoryContracts.ts @@ -1,8 +1,8 @@ import worldProxyFactoryBuild from "@latticexyz/world/out/WorldProxyFactory.sol/WorldProxyFactory.json" assert { type: "json" }; import worldProxyFactoryAbi from "@latticexyz/world/out/WorldProxyFactory.sol/WorldProxyFactory.abi.json" assert { type: "json" }; -import { Hex, getCreate2Address, encodeDeployData, size } from "viem"; -import { salt } from "./common"; +import { Hex, encodeDeployData, size } from "viem"; import { getWorldContracts } from "./getWorldContracts"; +import { getContractAddress } from "@latticexyz/common/internal"; export function getWorldProxyFactoryContracts(deployerAddress: Hex) { const worldContracts = getWorldContracts(deployerAddress); @@ -13,7 +13,7 @@ export function getWorldProxyFactoryContracts(deployerAddress: Hex) { abi: worldProxyFactoryAbi, args: [worldContracts.InitModule.address], }); - const worldProxyFactory = getCreate2Address({ from: deployerAddress, bytecode: worldProxyFactoryBytecode, salt }); + const worldProxyFactory = getContractAddress({ deployerAddress, bytecode: worldProxyFactoryBytecode }); return { ...worldContracts, diff --git a/packages/cli/src/verify.ts b/packages/cli/src/verify.ts index c6e20663a0..9a711a043d 100644 --- a/packages/cli/src/verify.ts +++ b/packages/cli/src/verify.ts @@ -1,13 +1,13 @@ -import { Chain, Client, Hex, Transport, getCreate2Address, sliceHex, zeroHash } from "viem"; +import { Chain, Client, Hex, Transport, sliceHex, zeroHash } from "viem"; import { getWorldFactoryContracts } from "./deploy/getWorldFactoryContracts"; import { verifyContract } from "./verify/verifyContract"; import PQueue from "p-queue"; import { getWorldProxyFactoryContracts } from "./deploy/getWorldProxyFactoryContracts"; -import { getDeployer } from "./deploy/getDeployer"; import { MUDError } from "@latticexyz/common/errors"; -import { Module, salt } from "./deploy/common"; +import { Module } from "./deploy/common"; import { getStorageAt } from "viem/actions"; import { execa } from "execa"; +import { getContractAddress, getDeployer } from "@latticexyz/common/internal"; type VerifyOptions = { client: Client; @@ -58,10 +58,9 @@ export async function verify({ rpc, verifier, verifierUrl, - address: getCreate2Address({ - from: deployerAddress, + address: getContractAddress({ + deployerAddress, bytecode: bytecode, - salt, }), }).catch((error) => { console.error(`Error verifying system contract ${name}:`, error); @@ -93,10 +92,9 @@ export async function verify({ rpc, verifier, verifierUrl, - address: getCreate2Address({ - from: deployerAddress, + address: getContractAddress({ + deployerAddress, bytecode: bytecode, - salt, }), }).catch((error) => { console.error(`Error verifying world factory contract ${name}:`, error); diff --git a/packages/common/package.json b/packages/common/package.json index ab8e05b62a..25427de799 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -19,6 +19,7 @@ "./type-utils": "./dist/type-utils.js", "./utils": "./dist/utils.js", "./kms": "./dist/kms.js", + "./internal": "./dist/internal.js", "./tsconfig.base.json": "./tsconfig.base.json" }, "typesVersions": { @@ -49,6 +50,9 @@ ], "kms": [ "./dist/kms.d.ts" + ], + "internal": [ + "./dist/internal.d.ts" ] } }, diff --git a/packages/common/src/deploy/common.ts b/packages/common/src/deploy/common.ts new file mode 100644 index 0000000000..633a7eba80 --- /dev/null +++ b/packages/common/src/deploy/common.ts @@ -0,0 +1,7 @@ +import { stringToHex } from "viem"; + +// salt for deterministic deploys of singleton contracts +export const singletonSalt = stringToHex("", { size: 32 }); + +// https://eips.ethereum.org/EIPS/eip-170 +export const contractSizeLimit = parseInt("6000", 16); diff --git a/packages/cli/src/deploy/create2/README.md b/packages/common/src/deploy/create2/README.md similarity index 100% rename from packages/cli/src/deploy/create2/README.md rename to packages/common/src/deploy/create2/README.md diff --git a/packages/cli/src/deploy/create2/deployment.json b/packages/common/src/deploy/create2/deployment.json similarity index 100% rename from packages/cli/src/deploy/create2/deployment.json rename to packages/common/src/deploy/create2/deployment.json diff --git a/packages/common/src/deploy/debug.ts b/packages/common/src/deploy/debug.ts new file mode 100644 index 0000000000..2642d0b81a --- /dev/null +++ b/packages/common/src/deploy/debug.ts @@ -0,0 +1,10 @@ +import { debug as parentDebug } from "../debug"; + +export const debug = parentDebug.extend("deploy"); +export const error = parentDebug.extend("deploy"); + +// Pipe debug output to stdout instead of stderr +debug.log = console.debug.bind(console); + +// Pipe error output to stderr +error.log = console.error.bind(console); diff --git a/packages/cli/src/deploy/ensureContract.ts b/packages/common/src/deploy/ensureContract.ts similarity index 71% rename from packages/cli/src/deploy/ensureContract.ts rename to packages/common/src/deploy/ensureContract.ts index c02f93965a..cadc5688ef 100644 --- a/packages/cli/src/deploy/ensureContract.ts +++ b/packages/common/src/deploy/ensureContract.ts @@ -1,9 +1,8 @@ import { Client, Transport, Chain, Account, concatHex, getCreate2Address, Hex } from "viem"; -import { getBytecode } from "viem/actions"; -import { contractSizeLimit, salt } from "./common"; -import { sendTransaction } from "@latticexyz/common"; +import { getCode } from "viem/actions"; +import { contractSizeLimit, singletonSalt } from "./common"; import { debug } from "./debug"; -import pRetry from "p-retry"; +import { sendTransaction } from "../sendTransaction"; export type Contract = { bytecode: Hex; @@ -25,9 +24,9 @@ export async function ensureContract({ throw new Error(`Found unlinked public library in ${debugLabel} bytecode`); } - const address = getCreate2Address({ from: deployerAddress, salt, bytecode }); + const address = getCreate2Address({ from: deployerAddress, salt: singletonSalt, bytecode }); - const contractCode = await getBytecode(client, { address, blockTag: "pending" }); + const contractCode = await getCode(client, { address, blockTag: "pending" }); if (contractCode) { debug("found", debugLabel, "at", address); return []; @@ -48,17 +47,10 @@ export async function ensureContract({ debug("deploying", debugLabel, "at", address); return [ - await pRetry( - () => - sendTransaction(client, { - chain: client.chain ?? null, - to: deployerAddress, - data: concatHex([salt, bytecode]), - }), - { - retries: 3, - onFailedAttempt: () => debug(`failed to deploy ${debugLabel}, retrying...`), - }, - ), + await sendTransaction(client, { + chain: client.chain ?? null, + to: deployerAddress, + data: concatHex([singletonSalt, bytecode]), + }), ]; } diff --git a/packages/cli/src/deploy/ensureContractsDeployed.ts b/packages/common/src/deploy/ensureContractsDeployed.ts similarity index 87% rename from packages/cli/src/deploy/ensureContractsDeployed.ts rename to packages/common/src/deploy/ensureContractsDeployed.ts index 6724641799..739d6ce2bd 100644 --- a/packages/cli/src/deploy/ensureContractsDeployed.ts +++ b/packages/common/src/deploy/ensureContractsDeployed.ts @@ -1,7 +1,7 @@ import { Client, Transport, Chain, Account, Hex } from "viem"; import { Contract, ensureContract } from "./ensureContract"; -import { uniqueBy } from "@latticexyz/common/utils"; -import { waitForTransactions } from "./waitForTransactions"; +import { waitForTransactions } from "../waitForTransactions"; +import { uniqueBy } from "../utils/uniqueBy"; export async function ensureContractsDeployed({ client, diff --git a/packages/cli/src/deploy/ensureDeployer.ts b/packages/common/src/deploy/ensureDeployer.ts similarity index 100% rename from packages/cli/src/deploy/ensureDeployer.ts rename to packages/common/src/deploy/ensureDeployer.ts diff --git a/packages/common/src/deploy/getContractAddress.ts b/packages/common/src/deploy/getContractAddress.ts new file mode 100644 index 0000000000..8da9441699 --- /dev/null +++ b/packages/common/src/deploy/getContractAddress.ts @@ -0,0 +1,12 @@ +import { Hex, getCreate2Address } from "viem"; +import { singletonSalt } from "./common"; + +export function getContractAddress({ + deployerAddress, + bytecode, +}: { + readonly deployerAddress: Hex; + readonly bytecode: Hex; +}): Hex { + return getCreate2Address({ from: deployerAddress, bytecode, salt: singletonSalt }); +} diff --git a/packages/cli/src/deploy/getDeployer.ts b/packages/common/src/deploy/getDeployer.ts similarity index 81% rename from packages/cli/src/deploy/getDeployer.ts rename to packages/common/src/deploy/getDeployer.ts index 03f22eda3a..dea14ab7d9 100644 --- a/packages/cli/src/deploy/getDeployer.ts +++ b/packages/common/src/deploy/getDeployer.ts @@ -1,14 +1,14 @@ import { Address, Chain, Client, Transport, sliceHex } from "viem"; -import { getBytecode } from "viem/actions"; +import { getCode } from "viem/actions"; import deployment from "./create2/deployment.json"; import { debug } from "./debug"; const deployer = `0x${deployment.address}` as const; export async function getDeployer(client: Client): Promise
    { - const bytecode = await getBytecode(client, { address: deployer }); + const bytecode = await getCode(client, { address: deployer }); if (bytecode) { - debug("found CREATE2 deployer at", deployer); + debug("found deployer bytecode at", deployer); // check if deployed bytecode is the same as the expected bytecode (minus 14-bytes creation code prefix) if (bytecode !== sliceHex(`0x${deployment.creationCode}`, 14)) { console.warn( diff --git a/packages/common/src/exports/internal.ts b/packages/common/src/exports/internal.ts new file mode 100644 index 0000000000..a25edda57a --- /dev/null +++ b/packages/common/src/exports/internal.ts @@ -0,0 +1,6 @@ +export * from "../waitForTransactions"; +export * from "../deploy/ensureContract"; +export * from "../deploy/ensureContractsDeployed"; +export * from "../deploy/ensureDeployer"; +export * from "../deploy/getContractAddress"; +export * from "../deploy/getDeployer"; diff --git a/packages/cli/src/deploy/waitForTransactions.ts b/packages/common/src/waitForTransactions.ts similarity index 95% rename from packages/cli/src/deploy/waitForTransactions.ts rename to packages/common/src/waitForTransactions.ts index 934233de22..ad3c26127e 100644 --- a/packages/cli/src/deploy/waitForTransactions.ts +++ b/packages/common/src/waitForTransactions.ts @@ -17,6 +17,7 @@ export async function waitForTransactions({ // wait for each tx separately/serially, because parallelizing results in RPC errors for (const hash of hashes) { const receipt = await waitForTransactionReceipt(client, { hash }); + // TODO: handle user op failures? if (receipt.status === "reverted") { throw new Error(`Transaction reverted: ${hash}`); } diff --git a/packages/common/tsup.config.ts b/packages/common/tsup.config.ts index 2ea1df785a..851bbf70b4 100644 --- a/packages/common/tsup.config.ts +++ b/packages/common/tsup.config.ts @@ -11,6 +11,7 @@ export default defineConfig((opts) => ({ "type-utils": "src/type-utils/index.ts", utils: "src/utils/index.ts", kms: "src/exports/kms.ts", + internal: "src/exports/internal.ts", }, target: "esnext", format: ["esm"], From 68d9d7095d13037cf2d952a328d5ce37545b925f Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Tue, 7 Jan 2025 09:16:01 -0800 Subject: [PATCH 6/9] chore: add ts path for common internal exports (#3425) --- tsconfig.paths.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.paths.json b/tsconfig.paths.json index b1b1135d47..6dc4a98795 100644 --- a/tsconfig.paths.json +++ b/tsconfig.paths.json @@ -13,6 +13,7 @@ "@latticexyz/common/type-utils": ["./packages/common/src/type-utils/index.ts"], "@latticexyz/common/utils": ["./packages/common/src/utils/index.ts"], "@latticexyz/common/kms": ["./packages/common/src/exports/kms.ts"], + "@latticexyz/common/internal": ["./packages/common/src/exports/internal.ts"], "@latticexyz/config": ["./packages/config/src/exports/index.ts"], "@latticexyz/config/*": ["./packages/config/src/exports/*.ts"], "@latticexyz/config/node": ["./packages/config/src/deprecated/node"], From a7625b97410346b1187e66803dde5194084312fd Mon Sep 17 00:00:00 2001 From: alvarius Date: Tue, 7 Jan 2025 18:19:13 +0100 Subject: [PATCH 7/9] feat(paymaster): add simple GenerousPaymaster for local development (#3422) Co-authored-by: Kevin Ingersoll --- .changeset/young-seals-travel.md | 5 + packages/paymaster/.gitignore | 2 + packages/paymaster/CHANGELOG.md | 1 + packages/paymaster/README.md | 3 + packages/paymaster/foundry.toml | 15 +++ packages/paymaster/package.json | 44 +++++++ packages/paymaster/remappings.txt | 2 + .../src/experimental/GenerousPaymaster.sol | 61 ++++++++++ .../paymaster/test/GenerousPaymaster.t.sol | 108 ++++++++++++++++++ packages/paymaster/test/utils/TestCounter.sol | 34 ++++++ pnpm-lock.yaml | 81 +++++++++++-- 11 files changed, 348 insertions(+), 8 deletions(-) create mode 100644 .changeset/young-seals-travel.md create mode 100644 packages/paymaster/.gitignore create mode 100644 packages/paymaster/CHANGELOG.md create mode 100644 packages/paymaster/README.md create mode 100644 packages/paymaster/foundry.toml create mode 100644 packages/paymaster/package.json create mode 100644 packages/paymaster/remappings.txt create mode 100644 packages/paymaster/src/experimental/GenerousPaymaster.sol create mode 100644 packages/paymaster/test/GenerousPaymaster.t.sol create mode 100644 packages/paymaster/test/utils/TestCounter.sol diff --git a/.changeset/young-seals-travel.md b/.changeset/young-seals-travel.md new file mode 100644 index 0000000000..484aec348d --- /dev/null +++ b/.changeset/young-seals-travel.md @@ -0,0 +1,5 @@ +--- +"@latticexyz/paymaster": patch +--- + +Added `GenerousPaymaster`, a simple paymaster that sponsors all user operations for local development purposes. diff --git a/packages/paymaster/.gitignore b/packages/paymaster/.gitignore new file mode 100644 index 0000000000..1e4ded714a --- /dev/null +++ b/packages/paymaster/.gitignore @@ -0,0 +1,2 @@ +cache +out diff --git a/packages/paymaster/CHANGELOG.md b/packages/paymaster/CHANGELOG.md new file mode 100644 index 0000000000..e62969c775 --- /dev/null +++ b/packages/paymaster/CHANGELOG.md @@ -0,0 +1 @@ +# @latticexyz/paymaster diff --git a/packages/paymaster/README.md b/packages/paymaster/README.md new file mode 100644 index 0000000000..8aff618ff4 --- /dev/null +++ b/packages/paymaster/README.md @@ -0,0 +1,3 @@ +# Paymaster contracts + +> :warning: **Important note: these contracts have not been audited yet, so any production use is discouraged for now.** diff --git a/packages/paymaster/foundry.toml b/packages/paymaster/foundry.toml new file mode 100644 index 0000000000..f0e017f5a0 --- /dev/null +++ b/packages/paymaster/foundry.toml @@ -0,0 +1,15 @@ +[profile.default] +solc = "0.8.24" +ffi = false +fuzz_runs = 256 +optimizer = true +optimizer_runs = 3000 +verbosity = 2 +allow_paths = ["../../node_modules", "../"] +src = "src" +out = "out" +bytecode_hash = "none" +extra_output_files = [ + "abi", + "evm.bytecode" +] diff --git a/packages/paymaster/package.json b/packages/paymaster/package.json new file mode 100644 index 0000000000..304d04779f --- /dev/null +++ b/packages/paymaster/package.json @@ -0,0 +1,44 @@ +{ + "name": "@latticexyz/paymaster", + "version": "2.2.14", + "description": "Paymaster contracts", + "repository": { + "type": "git", + "url": "https://github.com/latticexyz/mud.git", + "directory": "packages/paymaster" + }, + "license": "MIT", + "type": "module", + "exports": { + "./out/*": "./out/*" + }, + "typesVersions": { + "*": {} + }, + "files": [ + "out/GenerousPaymaster.sol", + "src" + ], + "scripts": { + "build": "pnpm run build:abi && pnpm run build:abi-ts", + "build:abi": "forge build", + "build:abi-ts": "abi-ts", + "clean": "pnpm run clean:abi", + "clean:abi": "forge clean", + "dev": "echo 'nothing to watch'", + "lint": "solhint --config ./.solhint.json 'src/**/*.sol'", + "test": "forge test", + "test:ci": "pnpm run test" + }, + "dependencies": {}, + "devDependencies": { + "@account-abstraction/contracts": "0.7.0", + "@latticexyz/abi-ts": "workspace:*", + "@openzeppelin/contracts": "5.1.0", + "forge-std": "https://github.com/foundry-rs/forge-std.git#1eea5bae12ae557d589f9f0f0edae2faa47cb262", + "solhint": "^3.3.7" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/paymaster/remappings.txt b/packages/paymaster/remappings.txt new file mode 100644 index 0000000000..af2dbf2991 --- /dev/null +++ b/packages/paymaster/remappings.txt @@ -0,0 +1,2 @@ +forge-std/=node_modules/forge-std/src/ +@account-abstraction/=node_modules/@account-abstraction/ \ No newline at end of file diff --git a/packages/paymaster/src/experimental/GenerousPaymaster.sol b/packages/paymaster/src/experimental/GenerousPaymaster.sol new file mode 100644 index 0000000000..2bae2e2f13 --- /dev/null +++ b/packages/paymaster/src/experimental/GenerousPaymaster.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +import { IPaymaster } from "@account-abstraction/contracts/interfaces/IPaymaster.sol"; +import { IEntryPoint } from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; +import { PackedUserOperation } from "@account-abstraction/contracts/interfaces/PackedUserOperation.sol"; +import { BasePaymaster } from "@account-abstraction/contracts/core/BasePaymaster.sol"; + +/** + * @title Generous Paymaster + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @dev This contract is a simple paymaster that sponsors all user operations. + * It is intended for local development purposes. + */ +contract GenerousPaymaster is BasePaymaster { + constructor(IEntryPoint _entryPoint) BasePaymaster(_entryPoint) {} + + /** + * Payment validation: check if paymaster agrees to pay. + * Revert to reject this request. + * Note that bundlers will reject this method if it changes the state, unless the paymaster is trusted (whitelisted). + * The paymaster pre-pays using its deposit, and receive back a refund after the postOp method returns. + * @param userOp - The user operation. + * @param userOpHash - Hash of the user's request data. + * @param maxCost - The maximum cost of this transaction (based on maximum gas and gas price from userOp). + * @return context - Value to send to a postOp. Zero length to signify postOp is not required. + * @return validationData - Signature and time-range of this operation, encoded the same as the return + * value of validateUserOperation. + * <20-byte> sigAuthorizer - 0 for valid signature, 1 to mark signature failure, + * other values are invalid for paymaster. + * <6-byte> validUntil - last timestamp this operation is valid. 0 for "indefinite" + * <6-byte> validAfter - first timestamp this operation is valid + * Note that the validation code cannot use block.timestamp (or block.number) directly. + */ + function _validatePaymasterUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 maxCost + ) internal override returns (bytes memory context, uint256 validationData) { + // No validation required, since this paymaster sponsors all user operations. + } + + /** + * Post-operation handler. + * @param mode - Enum with the following options: + * opSucceeded - User operation succeeded. + * opReverted - User op reverted. The paymaster still has to pay for gas. + * postOpReverted - never passed in a call to postOp(). + * @param context - The context value returned by validatePaymasterUserOp + * @param actualGasCost - Actual gas used so far (without this postOp call). + * @param actualUserOpFeePerGas - the gas price this UserOp pays. This value is based on the UserOp's maxFeePerGas + * and maxPriorityFee (and basefee) + * It is not the same as tx.gasprice, which is what the bundler pays. + */ + function _postOp( + IPaymaster.PostOpMode mode, + bytes calldata context, + uint256 actualGasCost, + uint256 actualUserOpFeePerGas + ) internal override {} +} diff --git a/packages/paymaster/test/GenerousPaymaster.t.sol b/packages/paymaster/test/GenerousPaymaster.t.sol new file mode 100644 index 0000000000..1f86e5124f --- /dev/null +++ b/packages/paymaster/test/GenerousPaymaster.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +import "forge-std/Test.sol"; +import { EntryPoint, IEntryPoint } from "@account-abstraction/contracts/core/EntryPoint.sol"; +import { PackedUserOperation } from "@account-abstraction/contracts/interfaces/PackedUserOperation.sol"; +import { SimpleAccountFactory, SimpleAccount } from "@account-abstraction/contracts/samples/SimpleAccountFactory.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import { TestCounter } from "./utils/TestCounter.sol"; +import { GenerousPaymaster } from "../src/experimental/GenerousPaymaster.sol"; + +contract GenerousPaymasterTest is Test { + EntryPoint entryPoint; + SimpleAccountFactory accountFactory; + GenerousPaymaster paymaster; + TestCounter counter; + + address payable beneficiary; + address user; + uint256 userKey; + SimpleAccount account; + + uint256 grantAllowance = 10 ether; + uint256 paymasterDeposit = 10 ether; + + function setUp() public { + entryPoint = new EntryPoint(); + accountFactory = new SimpleAccountFactory(entryPoint); + paymaster = new GenerousPaymaster(entryPoint); + counter = new TestCounter(); + + beneficiary = payable(makeAddr("beneficiary")); + (user, userKey) = makeAddrAndKey("user"); + account = accountFactory.createAccount(user, 0); + + entryPoint.depositTo{ value: paymasterDeposit }(address(paymaster)); + } + + // sanity check for everything works without paymaster + function testCall() external { + vm.deal(address(account), 1e18); + PackedUserOperation memory op = fillUserOp( + account, + userKey, + address(counter), + 0, + abi.encodeWithSelector(TestCounter.count.selector) + ); + op.signature = signUserOp(op, userKey); + submitUserOp(op); + assertEq(counter.counters(address(account)), 1); + } + + function testCallWithPaymaster() external { + PackedUserOperation memory op = fillUserOp( + account, + userKey, + address(counter), + 0, + abi.encodeWithSelector(TestCounter.count.selector) + ); + + op.paymasterAndData = abi.encodePacked(address(paymaster), uint128(100000), uint128(100000)); + op.signature = signUserOp(op, userKey); + + assertEq(beneficiary.balance, 0); + submitUserOp(op); + assertEq(counter.counters(address(account)), 1); + assertLt(entryPoint.balanceOf(address(paymaster)), paymasterDeposit); + } + + function fillUserOp( + SimpleAccount _sender, + uint256 _key, + address _to, + uint256 _value, + bytes memory _data + ) internal view returns (PackedUserOperation memory op) { + op.sender = address(_sender); + op.nonce = entryPoint.getNonce(address(_sender), 0); + op.callData = abi.encodeWithSelector(SimpleAccount.execute.selector, _to, _value, _data); + op.accountGasLimits = bytes32(abi.encodePacked(bytes16(uint128(80000)), bytes16(uint128(50000)))); + op.preVerificationGas = 50000; + op.gasFees = bytes32(abi.encodePacked(bytes16(uint128(100)), bytes16(uint128(1000000000)))); + // NOTE: gas fees are set to 0 on purpose to not require paymaster to have a deposit + // op.gasFees = bytes32(abi.encodePacked(bytes16(uint128(0)), bytes16(uint128(0)))); + op.signature = signUserOp(op, _key); + return op; + } + + function signUserOp(PackedUserOperation memory op, uint256 _key) internal view returns (bytes memory signature) { + bytes32 hash = entryPoint.getUserOpHash(op); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_key, MessageHashUtils.toEthSignedMessageHash(hash)); + signature = abi.encodePacked(r, s, v); + } + + function submitUserOp(PackedUserOperation memory op) internal { + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + ops[0] = op; + entryPoint.handleOps(ops, beneficiary); + } + + function expectUserOpRevert(bytes memory message) internal { + vm.expectRevert( + abi.encodeWithSelector(IEntryPoint.FailedOpWithRevert.selector, uint256(0), "AA33 reverted", message) + ); + } +} diff --git a/packages/paymaster/test/utils/TestCounter.sol b/packages/paymaster/test/utils/TestCounter.sol new file mode 100644 index 0000000000..efcfc784ed --- /dev/null +++ b/packages/paymaster/test/utils/TestCounter.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +//sample "receiver" contract, for testing "exec" from account. +contract TestCounter { + mapping(address => uint256) public counters; + + function count() public { + counters[msg.sender] = counters[msg.sender] + 1; + } + + function countFail() public pure { + revert("count failed"); + } + + function justemit() public { + emit CalledFrom(msg.sender); + } + + event CalledFrom(address sender); + + //helper method to waste gas + // repeat - waste gas on writing storage in a loop + // junk - dynamic buffer to stress the function size. + mapping(uint256 => uint256) public xxx; + uint256 public offset; + + function gasWaster(uint256 repeat, string calldata /*junk*/) external { + for (uint256 i = 1; i <= repeat; i++) { + offset++; + xxx[offset] = i; + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index acc0acfa0b..1641c26e8f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -718,6 +718,24 @@ importers: specifier: 0.34.6 version: 0.34.6(jsdom@22.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.33.0) + packages/paymaster: + devDependencies: + '@account-abstraction/contracts': + specifier: 0.7.0 + version: 0.7.0 + '@latticexyz/abi-ts': + specifier: workspace:* + version: link:../abi-ts + '@openzeppelin/contracts': + specifier: 5.1.0 + version: 5.1.0 + forge-std: + specifier: https://github.com/foundry-rs/forge-std.git#1eea5bae12ae557d589f9f0f0edae2faa47cb262 + version: https://codeload.github.com/foundry-rs/forge-std/tar.gz/1eea5bae12ae557d589f9f0f0edae2faa47cb262 + solhint: + specifier: ^3.3.7 + version: 3.3.7 + packages/protocol-parser: dependencies: '@latticexyz/common': @@ -1555,6 +1573,9 @@ packages: resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} engines: {node: '>=0.10.0'} + '@account-abstraction/contracts@0.7.0': + resolution: {integrity: sha512-Bt/66ilu3u8I9+vFZ9fTd+cWs55fdb9J5YKfrhsrFafH1drkzwuCSL/xEot1GGyXXNJLQuXbMRztQPyelNbY1A==} + '@adraffy/ens-normalize@1.11.0': resolution: {integrity: sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==} @@ -3755,6 +3776,12 @@ packages: resolution: {integrity: sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==} engines: {node: '>=8.0.0'} + '@openzeppelin/contracts@3.4.2-solc-0.7': + resolution: {integrity: sha512-W6QmqgkADuFcTLzHL8vVoNBtkwjvQRpYIAom7KiUNoLKghyx3FgH0GBjt8NRvigV1ZmMOBllvE1By1C+bi8WpA==} + + '@openzeppelin/contracts@5.1.0': + resolution: {integrity: sha512-p1ULhl7BXzjjbha5aqst+QMLY+4/LCWADXOCsmLHRM77AqiPjnd9vvUN9sosUfhL9JGKpZ0TjEGxgvnizmWGSA==} + '@parcel/watcher-android-arm64@2.4.1': resolution: {integrity: sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==} engines: {node: '>= 10.0.0'} @@ -5595,6 +5622,22 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + '@uniswap/lib@4.0.1-alpha': + resolution: {integrity: sha512-f6UIliwBbRsgVLxIaBANF6w09tYqc6Y/qXdsrbEmXHyFA7ILiKrIwRFXe1yOg8M3cksgVsO9N7yuL2DdCGQKBA==} + engines: {node: '>=10'} + + '@uniswap/v2-core@1.0.1': + resolution: {integrity: sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q==} + engines: {node: '>=10'} + + '@uniswap/v3-core@1.0.1': + resolution: {integrity: sha512-7pVk4hEm00j9tc71Y9+ssYpO6ytkeI0y7WE9P6UcmNzhxPePwyAxImuhVsTqWK9YFvzgtvzJHi64pBl4jUzKMQ==} + engines: {node: '>=10'} + + '@uniswap/v3-periphery@1.4.4': + resolution: {integrity: sha512-S4+m+wh8HbWSO3DKk4LwUCPZJTpCugIsHrWR86m/OrUyvSqGDTXKFfc2sMuGXCZrD1ZqO3rhQsKgdWg3Hbb2Kw==} + engines: {node: '>=10'} + '@vanilla-extract/css@1.15.5': resolution: {integrity: sha512-N1nQebRWnXvlcmu9fXKVUs145EVwmWtMD95bpiEKtvehHDpUhmO1l2bauS7FGYKbi3dU1IurJbGpQhBclTr1ng==} @@ -6137,6 +6180,9 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + base64-sol@1.0.1: + resolution: {integrity: sha512-ld3cCNMeXt4uJXmLZBHFGMvVpK9KsLVEhPpFRXnvSVAqABKbuNZg/+dsq3NuM+wxFLb/UrVkz7m1ciWmkMfTbg==} + better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} @@ -10356,10 +10402,6 @@ packages: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true - semver@6.3.0: - resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} - hasBin: true - semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -11852,6 +11894,11 @@ snapshots: '@aashutoshrathi/word-wrap@1.2.6': {} + '@account-abstraction/contracts@0.7.0': + dependencies: + '@openzeppelin/contracts': 5.1.0 + '@uniswap/v3-periphery': 1.4.4 + '@adraffy/ens-normalize@1.11.0': {} '@alloc/quick-lru@5.2.0': {} @@ -14660,6 +14707,10 @@ snapshots: '@opentelemetry/api@1.8.0': {} + '@openzeppelin/contracts@3.4.2-solc-0.7': {} + + '@openzeppelin/contracts@5.1.0': {} + '@parcel/watcher-android-arm64@2.4.1': optional: true @@ -16937,6 +16988,20 @@ snapshots: '@ungap/structured-clone@1.2.0': {} + '@uniswap/lib@4.0.1-alpha': {} + + '@uniswap/v2-core@1.0.1': {} + + '@uniswap/v3-core@1.0.1': {} + + '@uniswap/v3-periphery@1.4.4': + dependencies: + '@openzeppelin/contracts': 3.4.2-solc-0.7 + '@uniswap/lib': 4.0.1-alpha + '@uniswap/v2-core': 1.0.1 + '@uniswap/v3-core': 1.0.1 + base64-sol: 1.0.1 + '@vanilla-extract/css@1.15.5': dependencies: '@emotion/hash': 0.9.2 @@ -17875,6 +17940,8 @@ snapshots: base64-js@1.5.1: {} + base64-sol@1.0.1: {} + better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 @@ -19195,7 +19262,7 @@ snapshots: eslint@5.16.0: dependencies: - '@babel/code-frame': 7.21.4 + '@babel/code-frame': 7.24.7 ajv: 6.12.6 chalk: 2.4.2 cross-spawn: 6.0.5 @@ -22930,8 +22997,6 @@ snapshots: semver@5.7.2: {} - semver@6.3.0: {} - semver@6.3.1: {} semver@7.5.0: @@ -23128,7 +23193,7 @@ snapshots: ignore: 4.0.6 js-yaml: 3.14.1 lodash: 4.17.21 - semver: 6.3.0 + semver: 6.3.1 optionalDependencies: prettier: 1.19.1 transitivePeerDependencies: From 2d2aa0867580dd910cc772b5cdd42c802e8652e3 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Wed, 8 Jan 2025 07:17:39 -0800 Subject: [PATCH 8/9] fix(world): switch to TS for ABIs in utils (#3429) --- .changeset/little-tables-wait.md | 5 ++ packages/world/ts/actions/callFrom.ts | 38 +++++++++++++- packages/world/ts/encodeSystemCall.ts | 6 +-- packages/world/ts/encodeSystemCallFrom.ts | 4 +- packages/world/ts/encodeSystemCalls.ts | 4 +- packages/world/ts/encodeSystemCallsFrom.ts | 4 +- packages/world/ts/worldCallAbi.ts | 60 ++++++++++++++++++++++ 7 files changed, 109 insertions(+), 12 deletions(-) create mode 100644 .changeset/little-tables-wait.md create mode 100644 packages/world/ts/worldCallAbi.ts diff --git a/.changeset/little-tables-wait.md b/.changeset/little-tables-wait.md new file mode 100644 index 0000000000..9c82f5bdde --- /dev/null +++ b/.changeset/little-tables-wait.md @@ -0,0 +1,5 @@ +--- +"@latticexyz/world": patch +--- + +Moved TS utils over to using hardcoded ABIs instead of ones imported from `.abi.json` files to fix some internal type resolution issues. diff --git a/packages/world/ts/actions/callFrom.ts b/packages/world/ts/actions/callFrom.ts index b095f69ccf..e3217e51e3 100644 --- a/packages/world/ts/actions/callFrom.ts +++ b/packages/world/ts/actions/callFrom.ts @@ -21,7 +21,6 @@ import { encodeKey, } from "@latticexyz/protocol-parser/internal"; import worldConfig from "../../mud.config"; -import IStoreReadAbi from "../../out/IStoreRead.sol/IStoreRead.abi.json"; type CallFromParameters = { worldAddress: Hex; @@ -134,7 +133,42 @@ async function retrieveSystemFunctionFromContract( const [staticData, encodedLengths, dynamicData] = await _readContract({ address: worldAddress, - abi: IStoreReadAbi, + abi: [ + { + type: "function", + name: "getRecord", + inputs: [ + { + name: "tableId", + type: "bytes32", + internalType: "ResourceId", + }, + { + name: "keyTuple", + type: "bytes32[]", + internalType: "bytes32[]", + }, + ], + outputs: [ + { + name: "staticData", + type: "bytes", + internalType: "bytes", + }, + { + name: "encodedLengths", + type: "bytes32", + internalType: "EncodedLengths", + }, + { + name: "dynamicData", + type: "bytes", + internalType: "bytes", + }, + ], + stateMutability: "view", + }, + ], functionName: "getRecord", args: [table.tableId, encodeKey(keySchema, { worldFunctionSelector })], }); diff --git a/packages/world/ts/encodeSystemCall.ts b/packages/world/ts/encodeSystemCall.ts index a6a36e735b..8de1bb4ea1 100644 --- a/packages/world/ts/encodeSystemCall.ts +++ b/packages/world/ts/encodeSystemCall.ts @@ -1,6 +1,6 @@ import { Abi, EncodeFunctionDataParameters, Hex, encodeFunctionData, type ContractFunctionName } from "viem"; import type { AbiParametersToPrimitiveTypes, ExtractAbiFunction } from "abitype"; -import IWorldCallAbi from "../out/IWorldKernel.sol/IWorldCall.abi.json"; +import { worldCallAbi } from "./worldCallAbi"; export type SystemCall> = EncodeFunctionDataParameters< abi, @@ -15,9 +15,7 @@ export function encodeSystemCall): AbiParametersToPrimitiveTypes< - ExtractAbiFunction["inputs"] -> { +}: SystemCall): AbiParametersToPrimitiveTypes["inputs"]> { return [ systemId, encodeFunctionData({ diff --git a/packages/world/ts/encodeSystemCallFrom.ts b/packages/world/ts/encodeSystemCallFrom.ts index 691675684e..39393dee21 100644 --- a/packages/world/ts/encodeSystemCallFrom.ts +++ b/packages/world/ts/encodeSystemCallFrom.ts @@ -1,7 +1,7 @@ import { Abi, EncodeFunctionDataParameters, encodeFunctionData, Address, type ContractFunctionName } from "viem"; import type { AbiParametersToPrimitiveTypes, ExtractAbiFunction } from "abitype"; -import IWorldCallAbi from "../out/IWorldKernel.sol/IWorldCall.abi.json"; import { SystemCall } from "./encodeSystemCall"; +import { worldCallAbi } from "./worldCallAbi"; export type SystemCallFrom> = SystemCall< abi, @@ -18,7 +18,7 @@ export function encodeSystemCallFrom): AbiParametersToPrimitiveTypes< - ExtractAbiFunction["inputs"] + ExtractAbiFunction["inputs"] > { return [ from, diff --git a/packages/world/ts/encodeSystemCalls.ts b/packages/world/ts/encodeSystemCalls.ts index f93301da43..cf8bb74a5f 100644 --- a/packages/world/ts/encodeSystemCalls.ts +++ b/packages/world/ts/encodeSystemCalls.ts @@ -1,12 +1,12 @@ import { Abi, type ContractFunctionName } from "viem"; -import IWorldCallAbi from "../out/IWorldKernel.sol/IWorldCall.abi.json"; import { SystemCall, encodeSystemCall } from "./encodeSystemCall"; import type { AbiParametersToPrimitiveTypes, ExtractAbiFunction } from "abitype"; +import { worldCallAbi } from "./worldCallAbi"; /** Encode system calls to be passed as arguments into `World.batchCall` */ export function encodeSystemCalls>( abi: abi, systemCalls: readonly Omit, "abi">[], -): AbiParametersToPrimitiveTypes["inputs"]>[] { +): AbiParametersToPrimitiveTypes["inputs"]>[] { return systemCalls.map((systemCall) => encodeSystemCall({ ...systemCall, abi } as SystemCall)); } diff --git a/packages/world/ts/encodeSystemCallsFrom.ts b/packages/world/ts/encodeSystemCallsFrom.ts index 0e0b40d0da..cc8e265d1f 100644 --- a/packages/world/ts/encodeSystemCallsFrom.ts +++ b/packages/world/ts/encodeSystemCallsFrom.ts @@ -1,14 +1,14 @@ import { Abi, Address, type ContractFunctionName } from "viem"; -import IWorldCallAbi from "../out/IWorldKernel.sol/IWorldCall.abi.json"; import { SystemCallFrom, encodeSystemCallFrom } from "./encodeSystemCallFrom"; import type { AbiParametersToPrimitiveTypes, ExtractAbiFunction } from "abitype"; +import { worldCallAbi } from "./worldCallAbi"; /** Encode system calls to be passed as arguments into `World.batchCallFrom` */ export function encodeSystemCallsFrom>( abi: abi, from: Address, systemCalls: readonly Omit, "abi" | "from">[], -): AbiParametersToPrimitiveTypes["inputs"]>[] { +): AbiParametersToPrimitiveTypes["inputs"]>[] { return systemCalls.map((systemCall) => encodeSystemCallFrom({ ...systemCall, abi, from } as SystemCallFrom), ); diff --git a/packages/world/ts/worldCallAbi.ts b/packages/world/ts/worldCallAbi.ts new file mode 100644 index 0000000000..7fa386b2f4 --- /dev/null +++ b/packages/world/ts/worldCallAbi.ts @@ -0,0 +1,60 @@ +// TODO: replace this with abi-ts generated files once we move to +// generating full TS files rather than DTS + +export const worldCallAbi = [ + { + type: "function", + name: "call", + inputs: [ + { + name: "systemId", + type: "bytes32", + internalType: "ResourceId", + }, + { + name: "callData", + type: "bytes", + internalType: "bytes", + }, + ], + outputs: [ + { + name: "", + type: "bytes", + internalType: "bytes", + }, + ], + stateMutability: "payable", + }, + { + type: "function", + name: "callFrom", + inputs: [ + { + name: "delegator", + type: "address", + internalType: "address", + }, + { + name: "systemId", + type: "bytes32", + internalType: "ResourceId", + }, + { + name: "callData", + type: "bytes", + internalType: "bytes", + }, + ], + outputs: [ + { + name: "", + type: "bytes", + internalType: "bytes", + }, + ], + stateMutability: "payable", + }, +] as const; + +export type worldCallAbi = typeof worldCallAbi; From 1b477d476a666ccffafc6eb266d1732b90bc28f9 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Wed, 8 Jan 2025 07:40:34 -0800 Subject: [PATCH 9/9] feat(store): add getRecord and getStaticDataLocation helpers (#3430) --- .changeset/sharp-lions-cover.md | 5 ++ packages/protocol-parser/src/getKeyTuple.ts | 2 +- packages/store/ts/exports/internal.ts | 2 + packages/store/ts/getRecord.ts | 78 +++++++++++++++++++++ packages/store/ts/getStaticDataLocation.ts | 10 +++ 5 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 .changeset/sharp-lions-cover.md create mode 100644 packages/store/ts/getRecord.ts create mode 100644 packages/store/ts/getStaticDataLocation.ts diff --git a/.changeset/sharp-lions-cover.md b/.changeset/sharp-lions-cover.md new file mode 100644 index 0000000000..e94ead112b --- /dev/null +++ b/.changeset/sharp-lions-cover.md @@ -0,0 +1,5 @@ +--- +"@latticexyz/store": patch +--- + +Added internal `getRecord` and `getStaticDataLocation` helpers. diff --git a/packages/protocol-parser/src/getKeyTuple.ts b/packages/protocol-parser/src/getKeyTuple.ts index 17c6ab0237..b368ae6330 100644 --- a/packages/protocol-parser/src/getKeyTuple.ts +++ b/packages/protocol-parser/src/getKeyTuple.ts @@ -7,7 +7,7 @@ type PartialTable = Pick; export type getKeyTuple = { [i in keyof key]: Hex; -}; +} & unknown; export function getKeyTuple( table: table, diff --git a/packages/store/ts/exports/internal.ts b/packages/store/ts/exports/internal.ts index 0765dc8d68..0ae3f3cc9a 100644 --- a/packages/store/ts/exports/internal.ts +++ b/packages/store/ts/exports/internal.ts @@ -2,6 +2,8 @@ export * from "../common"; export * from "../getStoreLogs"; export * from "../flattenStoreLogs"; export * from "../logToRecord"; +export * from "../getRecord"; +export * from "../getStaticDataLocation"; export * from "../config/v2/codegen"; export * from "../config/v2/defaults"; diff --git a/packages/store/ts/getRecord.ts b/packages/store/ts/getRecord.ts new file mode 100644 index 0000000000..56b17ba3d9 --- /dev/null +++ b/packages/store/ts/getRecord.ts @@ -0,0 +1,78 @@ +import { Address, Client, Hex } from "viem"; +import { Table } from "@latticexyz/config"; +import { + decodeValueArgs, + getKeySchema, + getKeyTuple, + getSchemaPrimitives, + getSchemaTypes, + getValueSchema, +} from "@latticexyz/protocol-parser/internal"; +import { readContract } from "viem/actions"; +import { getAction } from "viem/utils"; + +export type GetRecordOptions
    = { + address: Address; + table: table; + key: getSchemaPrimitives>; + blockTag?: "latest" | "pending"; +}; + +export async function getRecord
    ( + client: Client, + { address, table, key, blockTag }: GetRecordOptions
    , +): Promise> { + const [staticData, encodedLengths, dynamicData] = await getAction( + client, + readContract, + "readContract", + )({ + address, + abi, + functionName: "getRecord", + args: [table.tableId, getKeyTuple(table, key) as readonly Hex[]], + blockTag, + }); + + return { + ...key, + ...decodeValueArgs(getSchemaTypes(getValueSchema(table)), { staticData, encodedLengths, dynamicData }), + }; +} + +const abi = [ + { + type: "function", + name: "getRecord", + inputs: [ + { + name: "tableId", + type: "bytes32", + internalType: "ResourceId", + }, + { + name: "keyTuple", + type: "bytes32[]", + internalType: "bytes32[]", + }, + ], + outputs: [ + { + name: "staticData", + type: "bytes", + internalType: "bytes", + }, + { + name: "encodedLengths", + type: "bytes32", + internalType: "EncodedLengths", + }, + { + name: "dynamicData", + type: "bytes", + internalType: "bytes", + }, + ], + stateMutability: "view", + }, +] as const; diff --git a/packages/store/ts/getStaticDataLocation.ts b/packages/store/ts/getStaticDataLocation.ts new file mode 100644 index 0000000000..ad0f14771a --- /dev/null +++ b/packages/store/ts/getStaticDataLocation.ts @@ -0,0 +1,10 @@ +import { Hex, concatHex, hexToBigInt, keccak256, numberToHex, toBytes } from "viem"; + +// TODO: move to protocol-parser? +// equivalent of StoreCore._getStaticDataLocation + +const SLOT = hexToBigInt(keccak256(toBytes("mud.store"))); + +export function getStaticDataLocation(tableId: Hex, keyTuple: readonly Hex[]): Hex { + return numberToHex(SLOT ^ hexToBigInt(keccak256(concatHex([tableId, ...keyTuple]))), { size: 32 }); +}