import { CollapseCode } from "../../../components/CollapseCode";
In this tutorial you add a table of historical counter values and the time in which the counter reached those values.
In contrast to the Hello, World tutorial, here we do it after the World
is already deployed, without losing the existing data.
-
Create a new MUD application from the template. Select vanilla or react-ecs.
-
Run
pnpm dev
to start the blockchain and theWorld
.
-
Create a new package. This step is necessary because if we modify the
contracts
package thepnpm dev
process detects the changes and if needed redeploys theWorld
, which is not the behavior we want here.cd packages mkdir history cd history
-
Install the necessary modules in the new package.
cp ../contracts/package.json . pnpm install
-
Create a MUD configuration file,
history.config.ts
, with the new table's definition.import { mudConfig } from "@latticexyz/world/register"; export default mudConfig({ tables: { History: { keySchema: { counterValue: "uint32", }, valueSchema: { blockNumber: "uint256", time: "uint256", }, }, }, });
<details>
Explanation A MUD table has two schemas:
keySchema
, the key used to find entriesvalueSchema
, the value in the entry
Each schema is represented as a structure with field names as keys, and the appropriate Solidity data types as their values. Note that the data types in the key schema are limited to those that are fixed length such at
bytes<n>
. You cannot use strings, arrays, etc.In this case, the counter value is represented as a 32 bit unsigned integer, because that is what
Counter
uses. Block numbers and timestamps can be values up touint256
, so we'll use this type for these fields.</details>
-
Run this command to generate the table library.
pnpm mud tablegen --configPath history.config.ts
-
Copy configuration files from the
contracts
package.cp ../contracts/.env ../contracts/remappings.txt ../contracts/foundry.toml .
-
Add a
WORLD_ADDRESS
parameter to.env
. If you are using the template with a freshpnpm dev
, then you can use this.env
:# This .env file is for demonstration purposes only. # # This should usually be excluded via .gitignore and the env vars attached to # your deployment environment, but we're including this here for ease of local # development. Please do not commit changes to this file! # # Enable debug logs for MUD CLI DEBUG=mud:* # Anvil default private key PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 # Address for the world we are extending WORLD_ADDRESS=0xf599e8f01a5ca469cda10711c3f2ffa4eeed755e
-
Create a directory for scripts.
mkdir script
-
Create this script in
script/DeployHistoryTable.s.sol
.// SPDX-License-Identifier: MIT pragma solidity >=0.8.21; import { Script } from "forge-std/Script.sol"; import { console } from "forge-std/console.sol"; import { IBaseWorld } from "@latticexyz/world-modules/src/interfaces/IBaseWorld.sol"; // For registering the table import { History, HistoryTableId } from "../src/codegen/index.sol"; import { IStore } from "@latticexyz/store/src/IStore.sol"; import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; contract DeployHistoryTable is Script { function run() external { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); address worldAddress = vm.envAddress("WORLD_ADDRESS"); vm.startBroadcast(deployerPrivateKey); StoreSwitch.setStoreAddress(worldAddress); History.register(); vm.stopBroadcast(); } }
-
Run the script.
forge script script/DeployHistoryTable.s.sol --rpc-url http://127.0.0.1:8545 --broadcast
-
Browse to the application, which is typically at http://localhost:3000.
-
Open MUD Dev Tools.
-
Click Components > store:Tables to see the list of tables. See that one of the tables has this tableId:
0x74620000000000000000000000000000486973746f7279000000000000000000
.Use an online converter to verify that
486973746f7279
is the Hex for the ASCIIHistory
, the name of the table. This means that there is a table calledHistory
in the root namespace (because there is nothing in the namespace part of the tableId).
To be able to see the history table's content in the MUD DevTools, we need the client to synchronize it.
Normally this happens automatically because the table is in mud.config.ts
, but this new table is not in that configuration file, so we need to add it manually.
-
Change to the client package and copy the configuration file
history.config.ts
.cd ../client cp ../history/history.config.ts src/mud
-
Add the Lattice store package.
pnpm install @latticexyz/store@next
-
Edit
src/mud/setupNetwork.ts
to add atables
parameter to thesyncToRecs
call./* * The MUD client code is built on top of viem * (https://viem.sh/docs/getting-started.html). * This line imports the functions we need from it. */ import { createPublicClient, fallback, webSocket, http, createWalletClient, Hex, parseEther, ClientConfig, } from "viem"; import { createFaucetService } from "@latticexyz/services/faucet"; import { encodeEntity, syncToRecs } from "@latticexyz/store-sync/recs"; import { resolveConfig } from "@latticexyz/store"; import { getNetworkConfig } from "./getNetworkConfig"; import { world } from "./world"; import IWorldAbi from "contracts/out/IWorld.sol/IWorld.abi.json"; import { createBurnerAccount, getContract, transportObserver, ContractWrite } from "@latticexyz/common"; import { Subject, share } from "rxjs"; /* * Import our MUD config, which includes strong types for * our tables and other config options. We use this to generate * things like RECS components and get back strong types for them. * * See https://mud.dev/templates/typescript/contracts#mudconfigts * for the source of this information. */ import mudConfig from "contracts/mud.config"; import historyConfig from "./history.config"; export type SetupNetworkResult = Awaited<ReturnType<typeof setupNetwork>>; export async function setupNetwork() { const networkConfig = await getNetworkConfig(); /* * Create a viem public (read only) client * (https://viem.sh/docs/clients/public.html) */ const clientOptions = { chain: networkConfig.chain, transport: transportObserver(fallback([webSocket(), http()])), pollingInterval: 1000, } as const satisfies ClientConfig; const publicClient = createPublicClient(clientOptions); /* * Create a temporary wallet and a viem client for it * (see https://viem.sh/docs/clients/wallet.html). */ const burnerAccount = createBurnerAccount(networkConfig.privateKey as Hex); const burnerWalletClient = createWalletClient({ ...clientOptions, account: burnerAccount, }); /* * Create an observable for contract writes that we can * pass into MUD dev tools for transaction observability. */ const write$ = new Subject<ContractWrite>(); /* * Create an object for communicating with the deployed World. */ const worldContract = getContract({ address: networkConfig.worldAddress as Hex, abi: IWorldAbi, publicClient, walletClient: burnerWalletClient, onWrite: (write) => write$.next(write), }); /* * Sync on-chain state into RECS and keeps our client in sync. * Uses the MUD indexer if available, otherwise falls back * to the viem publicClient to make RPC calls to fetch MUD * events from the chain. */ const { components, latestBlock$, storedBlockLogs$, waitForTransaction } = await syncToRecs({ world, config: mudConfig, tables: resolveConfig(historyConfig).tables, address: networkConfig.worldAddress as Hex, publicClient, startBlock: BigInt(networkConfig.initialBlockNumber), }); /* * If there is a faucet, request (test) ETH if you have * less than 1 ETH. Repeat every 20 seconds to ensure you don't * run out. */ if (networkConfig.faucetServiceUrl) { const address = burnerAccount.address; console.info("[Dev Faucet]: Player address -> ", address); const faucet = createFaucetService(networkConfig.faucetServiceUrl); const requestDrip = async () => { const balance = await publicClient.getBalance({ address }); console.info(`[Dev Faucet]: Player balance -> ${balance}`); const lowBalance = balance < parseEther("1"); if (lowBalance) { console.info("[Dev Faucet]: Balance is low, dripping funds to player"); // Double drip await faucet.dripDev({ address }); await faucet.dripDev({ address }); } }; requestDrip(); // Request a drip every 20 seconds setInterval(requestDrip, 20000); } return { world, components, playerEntity: encodeEntity({ address: "address" }, { address: burnerWalletClient.account.address }), publicClient, walletClient: burnerWalletClient, latestBlock$, storedBlockLogs$, waitForTransaction, worldContract, write$: write$.asObservable().pipe(share()), }; }
-
Browse to the application, which is typically at http://localhost:3000.
-
Open MUD Dev Tools.
-
Click Components > :History to see the history table. At this point it is empty.
Finally, we need to update :History
whenever :Counter
changes.
The easiest way to do this is to modify IncrementSystem
.
-
Copy
History.sol
into thecontracts
package.cd ../contracts mkdir src/other_tables cp ../history/src/codegen/tables/History.sol src/other_tables/History.sol
-
Edit
src/systems/IncrementSystem.sol
.// SPDX-License-Identifier: MIT pragma solidity >=0.8.0; import { System } from "@latticexyz/world/src/System.sol"; import { Counter } from "../codegen/index.sol"; import { History } from "../other_tables/History.sol"; contract IncrementSystem is System { function increment() public returns (uint32) { uint32 counter = Counter.get(); uint32 newValue = counter + 1; Counter.set(newValue); History.set(newValue, block.number, block.timestamp); return newValue; } }
-
Click Increment a few times to see that
:History
is updated.