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.
Modify the MUD configuration file
-
In an editor, open
packages/contracts/mud.config.ts
and add a table definition forHistory
.mud.config.tsimport { mudConfig } from "@latticexyz/world/register"; export default mudConfig({ tables: { Counter: { keySchema: {}, valueSchema: "uint32", }, History: { keySchema: { counterValue: "uint32", }, valueSchema: { blockNumber: "uint256", time: "uint256", }, }, }, });
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 (opens in a new tab) 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.
-
Run this command in
packages/contracts
to regenerate the table libraries.pnpm build:mud
Update IncrementSystem
-
In an editor, open
packages/contracts/src/systems/IncrementSystem.sol
.- Modify the second
import
line to importHistory
. - Modify the
increment
function to also updateHistory
. To see the exact functions that are available, you can look atpackages/contracts/src/codegen/tables/History.sol
(that is the reason we ranpnpm build:mud
to recreate it already).
- Modify the second
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
import { System } from "@latticexyz/world/src/System.sol";
import { Counter, History, HistoryData } 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, HistoryData } from "../codegen/index.sol";
When a table has multiple fields in the value schema, MUD generates a Struct (opens in a new tab) to hold a full value.
Here is HistoryData
, copied from packages/contract/src/codegen/History.sol
.
struct HistoryData {
uint256 blockNumber;
uint256 time;
}
Note that IncrementSystem
doesn't need to use HistoryData
, because it only writes to history, it doesn't read from it.
However, this is part of manipulating the schema and therefore included in this tutorial.
History.set(newValue, block.number, block.timestamp);
Set the value.
All MUD tables have a <table>.set
function with the parameters being the key schema fields in order and then the value schema fields in order.
-
Run this command in
packages/contracts
to rebuild everything this package produces.pnpm build
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 updates when you click Increment.
Click the Store data tab and select the table :History.
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.
-
Edit
packages/client/src/index.ts
.- Import some additional definitions.
- Use
components.History.update$.subscribe
to update the history.
index.tsimport { 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:
- Use
encodeEntity
to get the key. - 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).
-
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>
-
Run
pnpm dev
in the application's root directory (or restart it if already running), browse to the app URL, and click Increment a few times. See that the history table gets populated.