Guides
Add a table

Add a table

In this tutorial you add a table of historical counter values and the time in which the counter reached those values. For the sake of simplicity, we will implement this in the increment function rather than use a storage hook.

Setup

Create a new MUD application from the template. Use the vanilla template.

Modify the MUD configuration file

  1. In an editor, open packages/contracts/mud.config.ts and add a table definition for History.

    mud.config.ts
    import { defineWorld } from "@latticexyz/world";
     
    export default defineWorld({
      tables: {
        Counter: {
          schema: {
            value: "uint32",
          },
          key: [],
        },
        History: {
          schema: {
            counterValue: "uint32",
            blockNumber: "uint256",
            time: "uint256",
          },
          key: ["counterValue"],
        },
      },
    });
Explanation

A MUD table is a key-value database (opens in a new tab). The schema includes all the fields in the key and the value, along with their types.

To distinguish between the key fields and value fields the key field includes a list of fields that are part of the key.

FieldTypePart of the
counterValueuint32Key
blockNumberuint256Value
timeuint256Value
  1. Run this command to regenerate the table libraries.

    (cd packages/contracts; pnpm mud tablegen)

Update IncrementSystem

In an editor, open packages/contracts/src/systems/IncrementSystem.sol.

  • Modify the second import line to import History.
  • Modify the increment function to also update History. To see the exact functions that are available, you can look at packages/contracts/src/codegen/tables/History.sol (that is the reason we ran pnpm build to recreate it already).
IncrementSystem.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
 
import { System } from "@latticexyz/world/src/System.sol";
import { Counter, History } from "../codegen/index.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;
  }
}
Explanation
import { Counter, History } from "../codegen/index.sol";

Import the new table.

History.set(newValue, block.number, block.timestamp);

Set the value. All MUD tables have a <table>.set function with the parameters being the key fields in order and then the value schema fields in order.

Update the user interface

You can already run the application and see in the MUD Dev Tools that there is a History table and it gets updated when you click Increment. Click the Components tab and select the History component.

However, you can also add the history to the user interface. The directions here apply to the vanilla client template, if you use anything else you'll need to modify them as appropriate.

  1. Edit packages/client/src/index.ts.

    • Import some additional definitions.
    • Use components.History.update$.subscribe to update the history.
    index.ts
    import { setup } from "./mud/setup";
    import mudConfig from "contracts/mud.config";
    import { encodeEntity, singletonEntity } from "@latticexyz/store-sync/recs";
    import { getComponentValue } from "@latticexyz/recs";
     
    const {
      components,
      systemCalls: { increment },
      network,
    } = await setup();
     
    // Components expose a stream that triggers when the component is updated.
    components.Counter.update$.subscribe((update) => {
      const [nextValue, prevValue] = update.value;
      console.log("Counter updated", update, { nextValue, prevValue });
      document.getElementById("counter")!.innerHTML = String(nextValue?.value ?? "unset");
    });
     
    components.History.update$.subscribe((update) => {
      const History = components.History;
      var table = "<tr><th>Counter</th><th>Block</th><th>Time</th></tr>";
      for (var i = 0; i <= getComponentValue(components.Counter, singletonEntity).value; i++) {
        const key = encodeEntity(History.metadata.keySchema, { counterValue: i });
        const value = getComponentValue(History, key);
        if (value)
          table +=
            `<tr><td>${i}</td><td>${value.blockNumber}</td>` + `<td>${new Date(Number(value.time) * 1000)}</td></tr>\n`;
      }
      document.getElementById("history")!.innerHTML = `<table border>${table}</table>`;
    });
     
    // Just for demonstration purposes: we create a global function that can be
    // called to invoke the Increment system contract via the world.
    // (See IncrementSystem.sol)
    (window as any).increment = async () => {
      console.log("new counter value:", await increment());
    };
     
    if (import.meta.env.DEV) {
      const { mount: mountDevTools } = await import("@latticexyz/dev-tools");
      mountDevTools({
        config: mudConfig,
        publicClient: network.publicClient,
        walletClient: network.walletClient,
        latestBlock$: network.latestBlock$,
        storedBlockLogs$: network.storedBlockLogs$,
        worldAddress: network.worldContract.address,
        worldAbi: network.worldContract.abi,
        write$: network.write$,
        recsWorld: network.world,
      });
    }
Explanation
components.History.update$.subscribe((update) => {

Register a function to be called when the History component changes.

  const History = components.History;
  var table = "<tr><th>Counter</th><th>Block</th><th>Time</th></tr>";
  for(var i=0; i<=getComponentValue(components.Counter,singletonEntity).value; i++) {

To get the value of a component we use getComponentValue (or getComponentValueStrict if we want to throw an error if the value is not found). This function gets a component and a key. In the case of a singleton there is no key, so we use singletonEntity. The returned value includes multiple fields, but here we only care about the value.

const key = encodeEntity(History.metadata.keySchema, { counterValue: i });
const value = getComponentValue(History, key);

Reading a value from a table that has keys is a two-step process:

  1. Use encodeEntity to get the key.
  2. Use getComponentValue to get the value tied to that key.
if (value)
  table += `<tr><td>${i}</td><td>${value.blockNumber}</td>` + `<td>${new Date(Number(value.time) * 1000)}</td></tr>\n`;

If there is a value, add a line to the table.

Solidity uses Unix time (opens in a new tab). JavaScript uses a similar system, but it measures times in milliseconds. So to get a readable date, we take the time (which is a BigInt (opens in a new tab)), multiply it by a thousand, and then convert it to a Date (opens in a new tab) object.

  }
  document.getElementById("history")!.innerHTML = `<table border>${table}</table>`
});

Set the internal HTML of the history HTML tag. Notice the exclamation mark (!). document.getElementById may return either a tag that can be changed, or an empty value (if the parameter is not an id of any of the HTML tags). We know that history exists in the HTML, but the TypeScript compiler does not. This exclamation point tells the compiler that it's OK, there will be a real value there. See here for additional information (opens in a new tab).

  1. Edit packages/clients/index.html to add a text area for the history.

    index.html
    <!doctype html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>a minimal MUD client</title>
      </head>
      <body>
        <script type="module" src="/src/index.ts"></script>
        <div>Counter: <span id="counter">0</span></div>
        <button onclick="window.increment()">Increment</button>
        <hr />
        <h2>History</h2>
        <div id="history"></div>
      </body>
    </html>
  2. Run pnpm dev in the application's root directory (unless it is already running), browse to the app URL, and click Increment a few times. See that the history table gets populated.