Skip to content

Commit

Permalink
docs: add table tutorial (#1273)
Browse files Browse the repository at this point in the history
Co-authored-by: alvarius <alvarius@lattice.xyz>
  • Loading branch information
qbzzt and alvrs committed Aug 23, 2023
1 parent 57ee7f9 commit 293f2f7
Show file tree
Hide file tree
Showing 4 changed files with 345 additions and 3 deletions.
4 changes: 3 additions & 1 deletion docs/pages/tutorials.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ These tutorials teach you how to do various things with MUD.
[These tutorials](tutorials/minimal) teach you how to make various changes to [the getting started example](https://github.com/latticexyz/mud/tree/main/examples/minimal).
This is an easy way to learn how to modify MUD functionality in various ways.

- [Add a system](tutorials/minimal/add-system):
- [Add a table](tutorials/minimal/add-table):
In this tutorial you add a table to preserve counter history, and learn how to lookup information using a key.
- [Add a system](tutorials/minimal/add_system):
In this tutorial you add a system to decrement the counter and update the application to use it.

## Walkthrough
Expand Down
3 changes: 2 additions & 1 deletion docs/pages/tutorials/minimal.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ To create the basic version to modify, follow these steps:

## The tutorials

- [Add a system](minimal/add-system)
- [Add a table](minimal/add-table)
- [Add a system](minimal/add_system)
3 changes: 2 additions & 1 deletion docs/pages/tutorials/minimal/_meta.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export default {
"add-system": "Add a system",
"add-table": "Add a table",
"add_system": "Add a system",
};
338 changes: 338 additions & 0 deletions docs/pages/tutorials/minimal/add-table.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,338 @@
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.
For the sake of simplicity, we will implement this in the `increment` function rather than use a [storage hook](/store/advanced-features#storage-hooks).

## Setup

[Create a new MUD application from the template](../minimal).

## Modify the MUD configuration file

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

<CollapseCode>

```ts filename="mud.config.ts" showLineNumbers copy {9-17}
import { mudConfig } from "@latticexyz/world/register";

export default mudConfig({
tables: {
Counter: {
keySchema: {},
schema: "uint32",
},
History: {
keySchema: {
counterValue: "uint32",
},
schema: {
blockNumber: "uint256",
time: "uint256",
},
},
},
});
```

</CollapseCode>

<details>
<summary>Explanation</summary>

A MUD table has two schemas:

- `keySchema`, the key used to find entries
- `schema`, the value in the entry (soon to be renamed to `valueSchema`)

Each schema is represented as a structure with field names as keys, and the appropriate [Solidity data types](https://docs.soliditylang.org/en/latest/types.html) as their values.

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>

2. Run this command in `packages/contracts` to regenerate the table libraries.

```sh copy
pnpm build:mud
```

## Update `IncrementSystem`

1. 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:mud` to recreate it already).

<CollapseCode>

```solidity filename="IncrementSystem.sol.sol" copy showLineNumbers {5, 12}
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
import { System } from "@latticexyz/world/src/System.sol";
import { Counter, History, HistoryData } from "../codegen/Tables.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;
}
}
```

</CollapseCode>

<details>
<summary>Explanation</summary>

```solidity
import { Counter, History, HistoryData } from "../codegen/Tables.sol";
```

When a table has multiple fields in the value schema, MUD generates a [Struct](https://www.tutorialspoint.com/solidity/solidity_structs.htm) to hold a full value.
Here is `HistoryData`, copied from `packages/contract/src/codegen/History.sol`.

```solidity
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.

```solidity
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.

</details>

2. Run this command in `packages/contracts` to rebuild everything this package produces.

```sh copy
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.

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

- Import two more functions we need.
- In `components.Counter.update$.subscribe`, add code that updates the selection of history values.
- Add a function that is called when the user selects a different option.

<CollapseCode>

```ts filename="index.ts" copy showLineNumbers {3-4,18-22,31-37}
import { setup } from "./mud/setup";
import mudConfig from "contracts/mud.config";
import { encodeEntity } from "@latticexyz/store-sync/recs";
import { getComponentValueStrict } 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");

let options: String = "";
for (let i = 1; i <= nextValue?.value; i++) {
options += `<option value="${i}">${i}</option>`;
}
document.getElementById("historyValue")!.innerHTML = options;
});

// 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());
};

(window as any).readHistory = async (counterValue) => {
const History = components.History;
const entity = encodeEntity(History.metadata.keySchema, { counterValue });
const { blockNumber, time } = getComponentValueStrict(History, entity);
document.getElementById("blockNumber")!.innerHTML = blockNumber;
document.getElementById("timeStamp")!.innerHTML = new Date(parseInt(Number(time) * 1000));
};

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$,
blockStorageOperations$: network.blockStorageOperations$,
worldAddress: network.worldContract.address,
worldAbi: network.worldContract.abi,
write$: network.write$,
recsWorld: network.world,
});
}
```

</CollapseCode>

<details>
<summary>Explanation</summary>

```typescript
let options: String = "";
```

Create `options` as an empty string.
This is the way you define a variable in TypeScript: `let <variable name>: <type>`.
Here we initialize it to the empty string.

```typescript
for (let i = 1; i <= nextValue?.value; i++) {
options += `<option value="${i}">${i}</option>`;
}
```

Create the list of options.

```typescript
document.getElementById("historyValue")!.innerHTML = options;
```

Set the internal HTML of the `historyValue` HTML tag to `options`.
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 `historyValue` 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](https://blog.logrocket.com/understanding-exclamation-mark-typescript/).

```typescript
(window as any).readHistory = async counterValue => {
const History = components.History
const entity = encodeEntity(History.metadata.keySchema, { counterValue });
```
`encodeEntity` creates a key in the format that MUD uses, which is based on [ABI argument encoding](https://docs.soliditylang.org/en/latest/abi-spec.html#argument-encoding).
```typescript
const { blockNumber, time } = getComponentValueStrict(History, entity);
```
Read the actual data.
```typescript
document.getElementById("blockNumber")!.innerHTML = blockNumber;
```
Update the value in the HTML table.
```typescript
document.getElementById("timeStamp")!.innerHTML =
new Date(parseInt(Number(time) * 1000))
}
```

Solidity uses [Unix time](https://en.wikipedia.org/wiki/Unix_time).
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`](https://www.w3schools.com/js/js_bigint.asp)), multiply it by a thousand, and then convert it to a [`Date`](https://www.w3schools.com/jsref/jsref_obj_date.asp) object.

</details>

2. Edit `packages/clients/index.html`.

<CollapseCode>

```html filename="index.html" copy showLineNumbers {12-26}
<!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 for value:
<select id="historyValue" onInput="window.readHistory(value)"></select>
</h2>
<table border>
<tr>
<th>Block number</th>
<th>Time</th>
</tr>
<tr>
<td id="blockNumber"></td>
<td id="timeStamp"></td>
</tr>
</table>
</body>
</html>
```

</CollapseCode>

<details>
<summary>Explanation</summary>

```html
<hr />
<h2>
History for value:
<select id="historyValue" onInput="window.readHistory(value)"></select>
</h2>
```

This is the [input field](https://www.w3schools.com/tags/tag_select.asp) that lets the user select which counter value they'd like to get information about.
The `id` attribute is used by `packages/client/src/index.ts` to set the options.
The `onInput` attribute is the JavaScript code to execute when the value changes.

```html
<table border></table>
```

A standard [HTML table](https://www.w3schools.com/html/html_tables.asp).

```html
<tr>
<th>Block number</th>
<th>Time</th>
</tr>
<tr>
<td id="blockNumber"></td>
<td id="timeStamp"></td>
</tr>
```

A location for the values, which is set by `window.readHistory` in `index.ts`.

</details>

3. Run `pnpm dev` in the application's root directory, browse to the app URL, and click **Increment** a few times.
Then select a counter value and see that the block number and correct time are written to the HTML table.

0 comments on commit 293f2f7

Please sign in to comment.