Skip to content

Latest commit

 

History

History
368 lines (282 loc) · 11.6 KB

add-table.mdx

File metadata and controls

368 lines (282 loc) · 11.6 KB

import { CollapseCode } from "../../../components/CollapseCode";

Add a table

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.

Setup

  1. Create a new MUD application from the template. Select vanilla or react-ecs.

  2. Run pnpm dev to start the blockchain and the World.

Create the new table code

  1. Create a new package. This step is necessary because if we modify the contracts package the pnpm dev process detects the changes and if needed redeploys the World, which is not the behavior we want here.

    cd packages
    mkdir history
    cd history
  2. Install the necessary modules in the new package.

    cp ../contracts/package.json .
    pnpm install
  3. 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 entries
    • valueSchema, 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 to uint256, so we'll use this type for these fields.

     </details>
    
  4. Run this command to generate the table library.

    pnpm mud tablegen --configPath history.config.ts

Deploy the new table

  1. Copy configuration files from the contracts package.

    cp ../contracts/.env ../contracts/remappings.txt ../contracts/foundry.toml .
  2. Add a WORLD_ADDRESS parameter to .env. If you are using the template with a fresh pnpm 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
  3. Create a directory for scripts.

    mkdir script
  4. 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();
      }
    }
  5. Run the script.

    forge script script/DeployHistoryTable.s.sol --rpc-url http://127.0.0.1:8545 --broadcast

Verify the new table is deployed

  1. Browse to the application, which is typically at http://localhost:3000.

  2. Open MUD Dev Tools.

  3. 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 ASCII History, the name of the table. This means that there is a table called History in the root namespace (because there is nothing in the namespace part of the tableId).

Add :History to the synchronization

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.

  1. Change to the client package and copy the configuration file history.config.ts.

    cd ../client
    cp ../history/history.config.ts src/mud
  2. Add the Lattice store package.

    pnpm install @latticexyz/store@next
  3. Edit src/mud/setupNetwork.ts to add a tables parameter to the syncToRecs 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()),
      };
    }
  4. Browse to the application, which is typically at http://localhost:3000.

  5. Open MUD Dev Tools.

  6. Click Components > :History to see the history table. At this point it is empty.

Put changes into :History

Finally, we need to update :History whenever :Counter changes. The easiest way to do this is to modify IncrementSystem.

  1. Copy History.sol into the contracts package.

    cd ../contracts
    mkdir src/other_tables
    cp ../history/src/codegen/tables/History.sol src/other_tables/History.sol
  2. 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;
      }
    }
  3. Click Increment a few times to see that :History is updated.