diff --git a/CLAUDE.md b/CLAUDE.md index baa8f74745..bf55630ce2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,6 +15,80 @@ Tevm is an in-browser & Node.js-compatible Ethereum Virtual Machine (EVM) enviro - **In the Browser** for advanced user experiences (offline simulation, real-time testing) - **In Deno, Bun**, or any modern JavaScript runtime +### Key Features + +- **Forking:** Fork from any EVM-compatible network (mainnet, testnet) with efficient caching +- **Transaction Pool:** Track and manage pending transactions locally +- **Flexible Mining:** Choose between automatic, interval-based, manual, or gas-limit-based mining +- **Zero Native Dependencies:** Works seamlessly in browsers and JavaScript runtimes +- **Highly Extensible:** Customize the VM, add precompiles, handle receipts, and more + +### Why JavaScript for Ethereum? + +- **Advanced Gas Estimation & Local Execution**: Remove round-trip latency to remote nodes +- **User Experience Enhancements**: Offline capabilities, optimistic UI updates with local simulation +- **Testing & Debugging**: Fine-grained EVM introspection, deterministic environment +- **Ecosystem & Portability**: Works across Node.js, browsers, and serverless environments + +## Core Architecture + +Tevm Node is built on a modular architecture with several key components: + +1. **EVM (Ethereum Virtual Machine)** - Core execution engine that runs EVM bytecode, handles state transitions and gas metering +2. **Blockchain** - Block and chain state management, handles block production (mining) and chain reorganizations +3. **StateManager** - Manages account balances, contract code, and storage state with support for forking from live networks +4. **TxPool** - Manages pending transactions in the mempool, orders transactions by gas price, validates transaction requirements +5. **ReceiptsManager** - Handles transaction receipts, event logs, and filters for implementing optimistic updates + +These components work together to provide a complete Ethereum node implementation that can be used for local development, testing, transaction simulation, and more. + +### API Conventions: Ethereumjs vs Viem + +Tevm has two distinct API styles due to its underlying implementation: + +1. **Low-level Ethereumjs API** + This is currently being replaced by a new zig api + +2. **High-level Viem API**: + - Uses hex strings (e.g., `0x123abc`) for binary data and addresses + - More familiar to web3 developers + - Used in most client-facing interfaces and the JSON-RPC implementation + +### Package Structure and Dependencies + +The new zig and javascript code will live in src/\*_/_. Most of the existing released code lives in packages/_ and bundler-packages/_ + +Tevm wraps its main dependencies (viem and ethereumjs) in dedicated packages: + +- **Viem utilities** are wrapped in the `packages/utils` package +- **Ethereumjs packages** are wrapped in corresponding Tevm packages: + - `packages/evm` - Wraps the EVM implementation + - `packages/state` - Wraps state management + - `packages/blockchain` - Wraps blockchain functionality + - `packages/block` - Wraps block handling + - `packages/address` - Wraps address utilities + - `packages/vm` - Wraps the main VM implementation + - `packages/common` - Wraps chain configuration + - And more... + +This structure provides a unified API and allows Tevm to extend or modify functionality when needed. + +When working with both APIs, you may need to convert between formats: + +```typescript +// Converting from Viem hex string to Ethereumjs bytes +import { hexToBytes } from "viem"; +import { createAddress } from "tevm/address"; + +// Address conversion +const viem_address = "0x1234567890123456789012345678901234567890"; +const ethereumjs_address = createAddress(viem_address); + +// Bytes conversion +const viem_data = "0xabcdef1234"; +const ethereumjs_data = hexToBytes(viem_data); +``` + ### Forking Implementation Most of Tevm's forking logic is implemented in the `StateManager`. When a fork configuration is provided: @@ -38,6 +112,233 @@ The entire JSON-RPC API is implemented in the `tevm/actions` package, which prov These action handlers translate between Viem-style parameters and the internal Ethereumjs API, handling format conversions automatically. +#### Common JSON-RPC Methods + +```typescript +// Using with Viem client interface +const client = createMemoryClient(); + +// eth_ methods +await client.getBalance({ address: "0x123..." }); +await client.getBlockNumber(); +await client.getCode({ address: "0x123..." }); +await client.getTransactionCount({ address: "0x123..." }); +await client.call({ to: "0x123...", data: "0xabcdef..." }); + +// anvil_ methods +await client.impersonateAccount({ address: "0x123..." }); +await client.stopImpersonatingAccount({ address: "0x123..." }); +await client.mine({ blocks: 1 }); +await client.setBalance({ address: "0x123...", value: 1000000000000000000n }); +await client.reset({ forking: { jsonRpcUrl: "https://..." } }); + +// tevm_ methods +await client.tevmSetAccount({ + address: "0x123...", + balance: 1000000000000000000n, +}); +await client.tevmDumpState(); +``` + +#### Raw JSON-RPC Interface + +You can also use the raw JSON-RPC interface: + +```typescript +import { createTevmNode } from "tevm"; +import { requestEip1193 } from "tevm/decorators"; + +const node = createTevmNode().extend(requestEip1193()); + +// Make JSON-RPC requests +const result = await node.request({ + method: "eth_getBalance", + params: ["0x1234567890123456789012345678901234567890", "latest"], +}); +``` + +### Quick Start Example (Viem) + +```typescript +import { createMemoryClient, http } from "tevm"; +import { optimism } from "tevm/common"; +import { + encodeFunctionData, + parseAbi, + decodeFunctionResult, + parseEther, +} from "viem"; + +// Create a client as a fork of the Optimism mainnet +const client = createMemoryClient({ + fork: { + transport: http("https://mainnet.optimism.io")({}), + common: optimism, + }, +}); + +await client.tevmReady(); + +// Mint 1 ETH for our address +const account = "0x" + "baD60A7".padStart(40, "0"); +await client.setBalance({ + address: account, + value: parseEther("1"), +}); + +// Interact with a smart contract +const greeterContractAddress = "0x10ed0b176048c34d69ffc0712de06CbE95730748"; +const greeterAbi = parseAbi([ + "function greet() view returns (string)", + "function setGreeting(string memory _greeting) public", +]); + +// Send a transaction +const txHash = await client.sendTransaction({ + account, + to: greeterContractAddress, + data: encodeFunctionData({ + abi: greeterAbi, + functionName: "setGreeting", + args: ["Hello from Tevm!"], + }), +}); + +// Mine the transaction +await client.mine({ blocks: 1 }); +``` + +### Using with EthersJS + +```typescript +import { createMemoryClient, http, parseAbi } from "tevm"; +import { optimism } from "tevm/common"; +import { requestEip1193 } from "tevm/decorators"; +import { BrowserProvider, Contract, Wallet } from "ethers"; +import { parseUnits } from "ethers/utils"; + +const client = createMemoryClient({ + fork: { + transport: http("https://mainnet.optimism.io")({}), + common: optimism, + }, +}); + +client.transport.tevm.extend(requestEip1193()); +await client.tevmReady(); + +const provider = new BrowserProvider(client.transport.tevm); +const signer = Wallet.createRandom(provider); + +// Mint ETH for our wallet +await client.setBalance({ + address: signer.address, + value: parseUnits("1.0", "ether"), +}); + +// Create contract instance +const greeterContractAddress = "0x10ed0b176048c34d69ffc0712de06CbE95730748"; +const greeterAbi = parseAbi([ + "function greet() view returns (string)", + "function setGreeting(string memory _greeting) public", +]); +const greeter = new Contract(greeterContractAddress, greeterAbi, signer); + +// Call contract functions +const originalGreeting = await greeter.greet(); +const tx = await greeter.setGreeting("Hello from Ethers!"); +await client.mine({ blocks: 1 }); +``` + +### Bundler Integration: Direct Solidity Imports + +One of Tevm's most powerful features is its bundler integration, allowing direct imports of Solidity contracts: + +```typescript +// Import a Solidity contract directly +import { Counter } from "./Counter.sol"; +import { createMemoryClient } from "tevm"; + +const client = createMemoryClient(); + +// Deploy the contract +const deployed = await client.deployContract(Counter); + +// Call contract methods with type safety +const count = await deployed.read.count(); +const tx = await deployed.write.increment(); +await client.mine({ blocks: 1 }); +const newCount = await deployed.read.count(); +``` + +Tevm provides bundler plugins for various build tools: + +- Vite: `@tevm/vite` +- Webpack: `@tevm/webpack` +- ESBuild: `@tevm/esbuild` +- Rollup: `@tevm/rollup` +- Bun: `@tevm/bun` + +These plugins enable: + +- Direct importing of `.sol` files +- Automatic compilation with solc +- Type generation for full TypeScript safety +- Hot module reloading for Solidity contracts + +### Low-Level VM Access and State Management + +```typescript +import { createTevmNode } from "tevm"; +import { createAddress } from "tevm/address"; +import { hexToBytes } from "viem"; + +const node = createTevmNode(); +await node.ready(); + +// Get VM and its components +const vm = await node.getVm(); +const evm = vm.evm; +const stateManager = vm.stateManager; +const blockchain = vm.blockchain; +const txPool = await node.getTxPool(); + +// Listen to EVM execution steps +vm.evm.events.on("step", (data, next) => { + console.log( + data.pc.toString().padStart(5, " "), // program counter + data.opcode.name.padEnd(9, " "), // opcode name + data.stack.length.toString().padStart(3, " "), // stack length + data.stack.length < 5 ? data.stack : data.stack.slice(-5), // stack items + ); + next?.(); +}); + +// Manipulate state directly +await stateManager.putAccount("0x1234567890123456789012345678901234567890", { + nonce: 0n, + balance: 10_000_000n, + storageRoot: + "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + codeHash: + "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", +}); + +// Call EVM directly for contract execution +await evm.runCall({ + to: createAddress("0x1234567890123456789012345678901234567890"), + caller: createAddress("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + data: hexToBytes("0xabcdef12"), // call data +}); + +// Use transaction pool +await txPool.add({ + from: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + to: "0x1234567890123456789012345678901234567890", + value: 1000000000000000000n, +}); +``` + ## Commands - Build: `bun build` or `nx run-many --targets=build:dist,build:app,build:types` diff --git a/CONSTANTS_MODULE_SUMMARY.md b/CONSTANTS_MODULE_SUMMARY.md new file mode 100644 index 0000000000..38fc69bb38 --- /dev/null +++ b/CONSTANTS_MODULE_SUMMARY.md @@ -0,0 +1,99 @@ +# EVM Constants Module Implementation Summary + +## Overview +Created a centralized constants and utilities module for the EVM implementation to eliminate circular dependencies and provide a single source of truth for all EVM specifications. + +## Files Created/Modified + +### 1. `src/evm/constants.zig` (New) +The main constants module containing: +- **Opcode Constants**: All EVM opcode byte values (ADD, MUL, PUSH, etc.) +- **EVM Limits**: Stack limits, call depth, code sizes, memory sizes +- **Gas Constants**: Base costs, memory costs, storage costs, transaction costs +- **System Constants**: Empty hashes, system addresses, chain IDs +- **Utility Functions**: + - Opcode classification (isPush, isDup, isSwap, isLog) + - Size calculations (getPushSize, getDupSize, getSwapSize) + - Gas calculations (memoryGasCost, initCodeGasCost, dataGasCost) + - Validation functions (isPrecompile, isBlockHashAccessible, isTerminal) +- **Hardfork Support**: Gas constants that vary by hardfork + +### 2. `src/evm/evm.zig` (Modified) +- Added import and re-export of constants module +- Exported commonly used types (EvmError, MemorySize, GasResult) +- Made constants easily accessible throughout EVM implementation + +### 3. `src/evm/opcodes.zig` (Modified) +- Replaced local type definitions with imports from constants module +- Updated gas constants to use centralized values +- Delegated utility functions (getOpcodeName, isPush) to constants module +- Removed duplicate code in favor of centralized implementation + +### 4. `test/Evm/constants_test.zig` (New) +Comprehensive test suite covering: +- Opcode constant values +- EVM limits and sizes +- Gas constant values +- All utility functions with edge cases +- Memory gas cost calculations +- Hardfork-specific gas constants + +### 5. `build.zig` (Modified) +- Added constants test configuration +- Integrated constants tests into main test suite + +### 6. `src/evm/constants_usage_example.zig` (New) +Example code demonstrating: +- Memory gas cost calculations +- Hardfork-specific opcode validation +- Dynamic gas calculations (SSTORE example) +- Opcode stack effect classification + +## Key Benefits + +1. **No Circular Dependencies**: Constants module has zero dependencies, can be imported anywhere +2. **Single Source of Truth**: All EVM constants defined in one place +3. **Type Safety**: Proper types for all values and results +4. **Hardfork Support**: Easy to manage constants that change between hardforks +5. **Well-Tested**: Comprehensive test coverage for all functionality +6. **Documentation**: Every constant and function is documented with relevant EIP references + +## Usage Pattern + +```zig +const constants = @import("constants.zig"); + +// Use opcode constants +if (opcode == constants.ADD) { + // Handle ADD opcode +} + +// Use gas constants +const gas_cost = constants.G_VERYLOW; + +// Use utility functions +if (constants.isPush(opcode)) { + const push_size = constants.getPushSize(opcode); +} + +// Calculate gas costs +const memory_cost = constants.memoryGasCost(size); + +// Get hardfork-specific values +const gas_constants = constants.getGasConstants(.London); +``` + +## Next Steps + +1. Update remaining EVM modules to use centralized constants +2. Remove any duplicate constant definitions throughout codebase +3. Consider adding more hardfork-specific configurations as needed +4. Potentially add constants for precompile implementations + +## References + +- Ethereum Yellow Paper +- EVM.codes opcode reference +- Go-Ethereum: params/protocol_params.go +- Reth: crates/primitives/src/constants.rs +- Evmone: lib/evmone/instructions.hpp \ No newline at end of file diff --git a/examples/mud/.eslintrc b/examples/mud/.eslintrc new file mode 100644 index 0000000000..79bd6ef23f --- /dev/null +++ b/examples/mud/.eslintrc @@ -0,0 +1,10 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ] +} diff --git a/examples/mud/.gitattributes b/examples/mud/.gitattributes new file mode 100644 index 0000000000..9c70dc52f0 --- /dev/null +++ b/examples/mud/.gitattributes @@ -0,0 +1,3 @@ +# suppress diffs for generated files +**/pnpm-lock.yaml linguist-generated=true +**/codegen/**/*.sol linguist-generated=true diff --git a/examples/mud/.gitignore b/examples/mud/.gitignore new file mode 100644 index 0000000000..b05d373f12 --- /dev/null +++ b/examples/mud/.gitignore @@ -0,0 +1,16 @@ +.DS_Store +logs +*.log + +node_modules + +.env.* + +# foundry +cache +broadcast +out/* +!out/IWorld.sol +out/IWorld.sol/* +!out/IWorld.sol/IWorld.abi.json +!out/IWorld.sol/IWorld.abi.d.json.ts diff --git a/examples/mud/.vscode/extensions.json b/examples/mud/.vscode/extensions.json new file mode 100644 index 0000000000..f015de2761 --- /dev/null +++ b/examples/mud/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["NomicFoundation.hardhat-solidity", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] +} diff --git a/examples/mud/.vscode/settings.json b/examples/mud/.vscode/settings.json new file mode 100644 index 0000000000..25fa6215fd --- /dev/null +++ b/examples/mud/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} diff --git a/examples/mud/mprocs.yaml b/examples/mud/mprocs.yaml new file mode 100644 index 0000000000..d3d02c2118 --- /dev/null +++ b/examples/mud/mprocs.yaml @@ -0,0 +1,21 @@ +scrollback: 10000 +procs: + client: + cwd: packages/client + shell: pnpm run dev + contracts: + cwd: packages/contracts + shell: pnpm mud dev-contracts --rpc http://127.0.0.1:8545 + deploy-prereqs: + cwd: packages/contracts + shell: pnpm entrykit-deploy + env: + DEBUG: "mud:*" + # Anvil default account (0x70997970C51812dc3A010C7d01b50e0d17dc79C8) + PRIVATE_KEY: "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" + anvil: + cwd: packages/contracts + shell: anvil --block-time 2 + explorer: + cwd: packages/contracts + shell: pnpm explorer diff --git a/examples/mud/package.json b/examples/mud/package.json new file mode 100644 index 0000000000..a2d36f0ba2 --- /dev/null +++ b/examples/mud/package.json @@ -0,0 +1,27 @@ +{ + "name": "mud-template-react", + "private": true, + "scripts": { + "build": "pnpm recursive run build", + "dev": "mprocs", + "dev:client": "pnpm --filter 'client' run dev", + "dev:contracts": "pnpm --filter 'contracts' dev", + "foundry:up": "curl -L https://foundry.paradigm.xyz | bash && bash $HOME/.foundry/bin/foundryup", + "mud:up": "pnpm mud set-version --tag main && pnpm install", + "prepare": "(forge --version || pnpm foundry:up)", + "test": "pnpm recursive run test" + }, + "devDependencies": { + "@latticexyz/cli": "2.2.21", + "@latticexyz/common": "2.2.21", + "@latticexyz/explorer": "2.2.21", + "@latticexyz/store-indexer": "2.2.21", + "@types/debug": "4.1.7", + "@typescript-eslint/eslint-plugin": "7.1.1", + "@typescript-eslint/parser": "7.1.1", + "eslint": "8.57.0", + "mprocs": "^0.7.1", + "shx": "^0.3.4", + "typescript": "5.4.2" + } +} diff --git a/examples/mud/packages/client/.eslintrc b/examples/mud/packages/client/.eslintrc new file mode 100644 index 0000000000..930af95967 --- /dev/null +++ b/examples/mud/packages/client/.eslintrc @@ -0,0 +1,7 @@ +{ + "extends": ["../../.eslintrc", "plugin:react/recommended", "plugin:react-hooks/recommended"], + "plugins": ["react", "react-hooks"], + "rules": { + "react/react-in-jsx-scope": "off" + } +} diff --git a/examples/mud/packages/client/.gitignore b/examples/mud/packages/client/.gitignore new file mode 100644 index 0000000000..1521c8b765 --- /dev/null +++ b/examples/mud/packages/client/.gitignore @@ -0,0 +1 @@ +dist diff --git a/examples/mud/packages/client/index.html b/examples/mud/packages/client/index.html new file mode 100644 index 0000000000..ccf1b76fb2 --- /dev/null +++ b/examples/mud/packages/client/index.html @@ -0,0 +1,12 @@ + + + + + + a MUD app + + +
+ + + diff --git a/examples/mud/packages/client/package.json b/examples/mud/packages/client/package.json new file mode 100644 index 0000000000..2e95d0fc96 --- /dev/null +++ b/examples/mud/packages/client/package.json @@ -0,0 +1,44 @@ +{ + "name": "client", + "version": "0.0.0", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite", + "preview": "vite preview", + "test": "tsc --noEmit" + }, + "dependencies": { + "@latticexyz/common": "2.2.21", + "@latticexyz/entrykit": "2.2.21", + "@latticexyz/explorer": "2.2.21", + "@latticexyz/react": "2.2.21", + "@latticexyz/schema-type": "2.2.21", + "@latticexyz/stash": "2.2.21", + "@latticexyz/store-sync": "2.2.21", + "@latticexyz/utils": "2.2.21", + "@latticexyz/world": "2.2.21", + "@tanstack/react-query": "^5.63.0", + "contracts": "workspace:*", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-error-boundary": "5.0.0", + "tailwind-merge": "^2.6.0", + "viem": "2.23.2", + "wagmi": "2.12.11" + }, + "devDependencies": { + "@types/react": "18.2.22", + "@types/react-dom": "18.2.7", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "eslint-plugin-react": "7.31.11", + "eslint-plugin-react-hooks": "4.6.0", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "vite": "^6.0.7", + "vite-plugin-mud": "2.2.21" + } +} diff --git a/examples/mud/packages/client/postcss.config.cjs b/examples/mud/packages/client/postcss.config.cjs new file mode 100644 index 0000000000..12a703d900 --- /dev/null +++ b/examples/mud/packages/client/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/examples/mud/packages/client/src/App.tsx b/examples/mud/packages/client/src/App.tsx new file mode 100644 index 0000000000..ddc7bae21c --- /dev/null +++ b/examples/mud/packages/client/src/App.tsx @@ -0,0 +1,46 @@ +import { stash } from "./mud/stash"; +import { useRecords } from "@latticexyz/stash/react"; +import { AccountButton } from "@latticexyz/entrykit/internal"; +import { Direction } from "./common"; +import mudConfig from "contracts/mud.config"; +import { useMemo } from "react"; +import { GameMap } from "./game/GameMap"; +import { useWorldContract } from "./mud/useWorldContract"; +import { Synced } from "./mud/Synced"; +import { useSync } from "@latticexyz/store-sync/react"; + +export function App() { + const players = useRecords({ stash, table: mudConfig.tables.app__Position }); + + const sync = useSync(); + const worldContract = useWorldContract(); + const onMove = useMemo( + () => + sync.data && worldContract + ? async (direction: Direction) => { + const tx = await worldContract.write.app__move([mudConfig.enums.Direction.indexOf(direction)]); + await sync.data.waitForTransaction(tx); + } + : undefined, + [sync.data, worldContract], + ); + + return ( + <> +
+ ( +
+ {message} ({percentage.toFixed(1)}%)… +
+ )} + > + +
+
+
+ +
+ + ); +} diff --git a/examples/mud/packages/client/src/Providers.tsx b/examples/mud/packages/client/src/Providers.tsx new file mode 100644 index 0000000000..ed96171dbb --- /dev/null +++ b/examples/mud/packages/client/src/Providers.tsx @@ -0,0 +1,35 @@ +import { WagmiProvider } from "wagmi"; +import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; +import { ReactNode } from "react"; +import { createSyncAdapter } from "@latticexyz/store-sync/internal"; +import { SyncProvider } from "@latticexyz/store-sync/react"; +import { stash } from "./mud/stash"; +import { defineConfig, EntryKitProvider } from "@latticexyz/entrykit/internal"; +import { wagmiConfig } from "./wagmiConfig"; +import { chainId, getWorldAddress, startBlock } from "./common"; + +const queryClient = new QueryClient(); + +export type Props = { + children: ReactNode; +}; + +export function Providers({ children }: Props) { + const worldAddress = getWorldAddress(); + return ( + + + + + {children} + + + + + ); +} diff --git a/examples/mud/packages/client/src/common.ts b/examples/mud/packages/client/src/common.ts new file mode 100644 index 0000000000..e5b14a917b --- /dev/null +++ b/examples/mud/packages/client/src/common.ts @@ -0,0 +1,26 @@ +import mudConfig from "contracts/mud.config"; +import { chains } from "./wagmiConfig"; +import { Chain } from "viem"; + +export const chainId = import.meta.env.CHAIN_ID; +export const worldAddress = import.meta.env.WORLD_ADDRESS; +export const startBlock = BigInt(import.meta.env.START_BLOCK ?? 0n); + +export const url = new URL(window.location.href); + +export type Direction = (typeof mudConfig.enums.Direction)[number]; + +export function getWorldAddress() { + if (!worldAddress) { + throw new Error("No world address configured. Is the world still deploying?"); + } + return worldAddress; +} + +export function getChain(): Chain { + const chain = chains.find((c) => c.id === chainId); + if (!chain) { + throw new Error(`No chain configured for chain ID ${chainId}.`); + } + return chain; +} diff --git a/examples/mud/packages/client/src/game/GameMap.tsx b/examples/mud/packages/client/src/game/GameMap.tsx new file mode 100644 index 0000000000..04d7d24d4f --- /dev/null +++ b/examples/mud/packages/client/src/game/GameMap.tsx @@ -0,0 +1,102 @@ +import { serialize, useAccount } from "wagmi"; +import { useKeyboardMovement } from "./useKeyboardMovement"; +import { Address, Hex, hexToBigInt, keccak256 } from "viem"; +import { ArrowDownIcon } from "../ui/icons/ArrowDownIcon"; +import { twMerge } from "tailwind-merge"; +import { Direction } from "../common"; +import mudConfig from "contracts/mud.config"; +import { AsyncButton } from "../ui/AsyncButton"; +import { useAccountModal } from "@latticexyz/entrykit/internal"; + +export type Props = { + readonly players?: readonly { + readonly player: Address; + readonly x: number; + readonly y: number; + }[]; + + readonly onMove?: (direction: Direction) => Promise; +}; + +const size = 40; +const scale = 100 / size; + +function getColorAngle(seed: Hex) { + return Number(hexToBigInt(keccak256(seed)) % 360n); +} + +const rotateClassName = { + North: "rotate-0", + East: "rotate-90", + South: "rotate-180", + West: "-rotate-90", +} as const satisfies Record; + +export function GameMap({ players = [], onMove }: Props) { + const { openAccountModal } = useAccountModal(); + const { address: userAddress } = useAccount(); + const currentPlayer = players.find((player) => player.player.toLowerCase() === userAddress?.toLowerCase()); + useKeyboardMovement(onMove); + return ( +
+
+ {onMove + ? mudConfig.enums.Direction.map((direction) => ( + + )) + : null} + + {players.map((player) => ( +
+ {player === currentPlayer ?
: null} +
+ ))} + + {!currentPlayer ? ( + onMove ? ( +
+ onMove("North")} + > + Spawning… + +
+ ) : ( +
+ +
+ ) + ) : null} +
+
+ ); +} diff --git a/examples/mud/packages/client/src/game/useKeyboardMovement.ts b/examples/mud/packages/client/src/game/useKeyboardMovement.ts new file mode 100644 index 0000000000..7e9074290f --- /dev/null +++ b/examples/mud/packages/client/src/game/useKeyboardMovement.ts @@ -0,0 +1,26 @@ +import { useEffect } from "react"; +import { Direction } from "../common"; + +const keys = new Map([ + ["ArrowUp", "North"], + ["ArrowRight", "East"], + ["ArrowDown", "South"], + ["ArrowLeft", "West"], +]); + +export const useKeyboardMovement = (move: undefined | ((direction: Direction) => void)) => { + useEffect(() => { + if (!move) return; + + const listener = (event: KeyboardEvent) => { + const direction = keys.get(event.key); + if (direction == null) return; + + event.preventDefault(); + move(direction); + }; + + window.addEventListener("keydown", listener); + return () => window.removeEventListener("keydown", listener); + }, [move]); +}; diff --git a/examples/mud/packages/client/src/index.tsx b/examples/mud/packages/client/src/index.tsx new file mode 100644 index 0000000000..995e6ec9fd --- /dev/null +++ b/examples/mud/packages/client/src/index.tsx @@ -0,0 +1,19 @@ +import "tailwindcss/tailwind.css"; +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { Providers } from "./Providers"; +import { App } from "./App"; +import { Explorer } from "./mud/Explorer"; +import { ErrorBoundary } from "react-error-boundary"; +import { ErrorFallback } from "./ui/ErrorFallback"; + +createRoot(document.getElementById("react-root")!).render( + + + + + + + + , +); diff --git a/examples/mud/packages/client/src/mud/Explorer.tsx b/examples/mud/packages/client/src/mud/Explorer.tsx new file mode 100644 index 0000000000..fe59d58c57 --- /dev/null +++ b/examples/mud/packages/client/src/mud/Explorer.tsx @@ -0,0 +1,32 @@ +import { useState } from "react"; +import { getChain, getWorldAddress } from "../common"; +import { MUDIcon } from "../ui/icons/MUDIcon"; + +export function Explorer() { + const [open, setOpen] = useState(false); + + const chain = getChain(); + const worldAddress = getWorldAddress(); + + const explorerUrl = chain.blockExplorers?.worldsExplorer?.url; + if (!explorerUrl) return null; + + return ( +
+ + {open ?