From cca553775e0061a1437be2ad92050f872dec11be Mon Sep 17 00:00:00 2001 From: Ori Pomerantz Date: Tue, 24 Oct 2023 04:57:04 -0500 Subject: [PATCH] docs(guides): add the extending world explanation to the new docs (#1800) Co-authored-by: alvarius --- next-docs/pages/guides/extending-world.mdx | 485 +++++++++++++++++++++ 1 file changed, 485 insertions(+) diff --git a/next-docs/pages/guides/extending-world.mdx b/next-docs/pages/guides/extending-world.mdx index 2c25c1c3de..3d4741873e 100644 --- a/next-docs/pages/guides/extending-world.mdx +++ b/next-docs/pages/guides/extending-world.mdx @@ -1 +1,486 @@ # Extending World + +On this page you learn how to modify a `World` that is already deployed to the blockchain. +If you want to learn how to modify a `World` before it is deployed, [see the hello world page](./hello-world). + +## The sample program + +To learn how to extend a `World`, we will extend the `Counter` example to allow users to leave a message while incrementing the counter. +The steps to create the example `World` that we'll extend [are here](/templates/typescript/getting-started). +You can either do it now, or wait until after you've created the extension resources. + +To extend the `World` with the message functionality requires adding several resources: + +- **Namespace**. + A [namescape](/world/namespaces-access-control) can contain tables and systems. + In most cases, the only way you would be able to extend a `World` that somebody else owns is to create your own namespace within that world. + +- **Table**. + A table to store the messages that have been sent. + +- **System**. + A system that updates the messages table and then calls `increment` to update the counter. + +## Create the Solidity code + +The easiest way to create the Solidity code is to use the MUD template: + +1. Create a new MUD application. + It does not matter which user interface option you choose. + + ```sh copy + pnpm create mud@next extension + cd extension/packages/contracts + ``` + +1. Edit `mud.config.ts` to include the definitions we need. + + ```ts filename="mud.config.ts" copy + import { mudConfig } from "@latticexyz/world/register"; + + export default mudConfig({ + namespace: "messaging", + tables: { + Messages: { + keySchema: { + counterValue: "uint32", + }, + valueSchema: { + message: "string", + }, + }, + }, + systems: { + MessageSystem: { + name: "MessageSystem", + openAccess: true, + }, + }, + }); + ``` + +1. Create `src/systems/MessageSystem.sol`. + + ```solidity filename="MessageSystem.sol" copy + // SPDX-License-Identifier: MIT + pragma solidity >=0.8.21; + + import { System } from "@latticexyz/world/src/System.sol"; + import { Messages } from "../codegen/index.sol"; + + interface WorldWithIncrement { + function increment() external returns (uint32); + } + + contract MessageSystem is System { + function incrementMessage(string memory message) public returns (uint32) { + uint32 newVal = WorldWithIncrement(_world()).increment(); + Messages.set(newVal, message); + return newVal; + } + } + ``` + +
+ + Explanation + + ```solidity + import { System } from "@latticexyz/world/src/System.sol"; + import { Messages } from "../codegen/index.sol"; + ``` + + These are the two main things the `System` needs to know: how to be a `System` and how to access the `Messages` table. + + ```solidity + interface WorldWithIncrement { + function increment() external returns (uint32); + } + ``` + + This `System` needs to call `increment` on the `World` where it is implemented. + However, as an extension author you might not have access to the source code of any `System` that isn't part of your extension. + + If you define your own interface for `World` you can add whatever function signatures are supported. + Note that in Ethereum [a function signature](https://docs.soliditylang.org/en/latest/abi-spec.html#function-selector) is the function name and its parameter types, it does not include the return type. + So if you are unsure of the return type that is not a huge problem. + + ```solidity + contract MessageSystem is System { + function incrementMessage(string memory message) public returns (uint32) { + uint32 newVal = WorldWithIncrement(_world()).increment(); + ``` + + This is how we use the `WorldWithIncrement` interface we created. + The [`_world()`](https://github.com/latticexyz/mud/blob/main/packages/world/src/WorldContext.sol#L38-L44) call gives us the address of the `World` that called us. + When we specify `WorldWithIncrement(
)`, we are telling Solidity that there is already a `WorldWithIncrement` at that address, and therefore we can use functions that are supported by `WorldWithIncrement`, such as `increment()`. + + ```solidity + Messages.set(newVal, message); + ``` + + This is one way to create a record with the key `newVal` and the value `message`. + + ```solidity + return newVal; + } + } + ``` + + When we are called by a user, an externally owned account, the return value is meaningless. + However, just as we call `increment()` from a contract and use the return value (instead of having to read `Counter` ourselves), some future onchain code might call `incrementMessage` and use the returned value. + +
+ +1. Remove files that are part of the template but don't make sense when our extension doesn't have its own counter. + + ```sh copy + rm src/systems/IncrementSystem.sol test/*.t.sol script/PostDeploy.s.sol + ``` + +1. Build and compile the Solidity code. + + ```sh copy + pnpm build + ``` + +## Deploy to the blockchain + +1. To have a `Counter` example `World` to modify, go to a separate command line window and create a blockchain with a `World` using [the TypeScript template](/template/typescript/getting-started) and start the execution. + Choose either the **react** user interface or the **vanilla** one. + + ```sh copy + pnpm create mud@next extendMe + cd extendMe + pnpm dev + ``` + +1. Back in the extension's `packages/contracts` directory, create a `.env` file with: + + - `PRIVATE_KEY` - the private key of an account that has ETH on the blockchain. + - `WORLD_ADDRESS` - the address of the `World` to which you add the namespace. + + If you are using the template with a fresh `pnpm dev`, then you can use this `.env`: + + ```sh filename=".env" copy + # Anvil default private key for the second account + # (NOT the account that deployed the World) + PRIVATE_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d + + # Address for the world we are extending + WORLD_ADDRESS=0x6e9474e9c83676b9a71133ff96db43e7aa0a4342 + ``` + +1. Create this script in `script/MessagingExtension.s.sol`. + + ```solidity filename="MessagingExtension.s.sol" copy + // 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"; + + import { WorldRegistrationSystem } from "@latticexyz/world/src/modules/core/implementations/WorldRegistrationSystem.sol"; + + // Create resource identifiers (for the namespace and system) + import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; + import { WorldResourceIdLib } from "@latticexyz/world/src/WorldResourceId.sol"; + import { RESOURCE_SYSTEM } from "@latticexyz/world/src/worldResourceTypes.sol"; + + // For registering the table + import { Messages, MessagesTableId } from "../src/codegen/index.sol"; + import { IStore } from "@latticexyz/store/src/IStore.sol"; + import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; + + // For deploying MessageSystem + import { MessageSystem } from "../src/systems/MessageSystem.sol"; + + contract MessagingExtension is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address worldAddress = vm.envAddress("WORLD_ADDRESS"); + + WorldRegistrationSystem world = WorldRegistrationSystem(worldAddress); + ResourceId namespaceResource = WorldResourceIdLib.encodeNamespace(bytes14("messaging")); + ResourceId systemResource = WorldResourceIdLib.encode(RESOURCE_SYSTEM, "messaging", "message"); + + vm.startBroadcast(deployerPrivateKey); + world.registerNamespace(namespaceResource); + + StoreSwitch.setStoreAddress(worldAddress); + Messages.register(); + + MessageSystem messageSystem = new MessageSystem(); + console.log("MessageSystem address: ", address(messageSystem)); + + world.registerSystem(systemResource, messageSystem, true); + world.registerFunctionSelector(systemResource, "incrementMessage(string)"); + + vm.stopBroadcast(); + } + } + ``` + +
+ + Explanation + + ```solidity + // SPDX-License-Identifier: MIT + pragma solidity >=0.8.21; + ``` + + Standard Solidity boilerplate. + + ```solidity + import { Script } from "forge-std/Script.sol"; + import { console } from "forge-std/console.sol"; + ``` + + The definitions for [forge scripts](https://book.getfoundry.sh/reference/forge/forge-script) and [the console](https://book.getfoundry.sh/reference/forge-std/console-log). + + ```solidity + import { IBaseWorld } from "@latticexyz/world-modules/src/interfaces/IBaseWorld.sol"; + ``` + + Use [IBaseWorld.sol](https://github.com/latticexyz/mud/blob/main/packages/world-modules/src/interfaces/IBaseWorld.sol) to get definitions that are common to all `World` contracts. + + ```solidity + import { WorldRegistrationSystem } from "@latticexyz/world/src/modules/core/implementations/WorldRegistrationSystem.sol"; + ``` + + [WorldRegistartionSystem.sol](https://github.com/latticexyz/mud/blob/main/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol) contains the function definitions necessary to register new namespaces and systems with an existing `World`. + + ```solidity + // Create resource identifiers (for the namespace and system) + import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; + import { WorldResourceIdLib } from "@latticexyz/world/src/WorldResourceId.sol"; + import { RESOURCE_SYSTEM } from "@latticexyz/world/src/worldResourceTypes.sol"; + ``` + + These definitions make it easy to manage resource identifiers. + We need them for the resource IDs we need to create: the namespace and the system. + + ```solidity + // For registering the table + import { Messages, MessagesTableId } from "../src/codegen/index.sol"; + import { IStore } from "@latticexyz/store/src/IStore.sol"; + import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; + ``` + + These are the definitions we need to register the `Messages` table. + + ```solidity + // For deploying MessageSystem + import { MessageSystem } from "../src/systems/MessageSystem.sol"; + ``` + + These are the definitions we need to deploy the `MessageSystem` contract so we'll then be able to register it as a `System` in the `World`. + + ```solidity + contract MessagingExtension is Script { + function run() external { + ``` + + This is the function that implements the script. + + ```solidity + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address worldAddress = vm.envAddress("WORLD_ADDRESS"); + ``` + + Read the private key and the address of the `World` from the environment (which includes the content of the `.env` file). + + ```solidity + WorldRegistrationSystem world = WorldRegistrationSystem(worldAddress); + ResourceId namespaceResource = WorldResourceIdLib.encodeNamespace(bytes14("messaging")); + ResourceId systemResource = WorldResourceIdLib.encode(RESOURCE_SYSTEM, "messaging", "message"); + ``` + + Among other things, a MUD `World` is a `WorldRegistrationSystem`, so it has the appropriate functions. + A `ResourceId` is a 32 byte value that uniquely identifies a resource in a MUD `World`. + It is two bytes of resource type followed by 14 bytes of namespace and then 16 bytes of the name of the actual resource. + + Here we create two `ResourceId` values: + + | Name | Type | Namespace | Resource name | + | ----------------- | ---------------- | --------- | ------------- | + | namespaceResource | `ns` (namespace) | messaging | Empty | + | systemResource | `sy` (system) | messaging | message | + + If you want to see these values, add these two lines to the script: + + ```solidity + console.log("Namespace ID: %x", uint256(ResourceId.unwrap(namespaceResource))); + console.log("System ID: %x", uint256(ResourceId.unwrap(systemResource))); + ``` + + Note that `console.log` requires a `uint256` value, and we can't get that directly from a `ResourceId`. + Instead, we have to [unwrap](https://docs.soliditylang.org/en/v0.8.20/types.html#user-defined-value-types) our `ResourceId` to get the original type (`bytes32`) and then [cast](https://docs.soliditylang.org/en/v0.8.20/types.html#explicit-conversions) it to `uint256`. + + The expected values are: + + | Name | Expected value | + | ------------ | ------------------------------------------------------------------ | + | Namespace ID | 0x6e736d6573736167696e67000000000000000000000000000000000000000000 | + | System ID | 0x73796d6573736167696e6700000000006d657373616765000000000000000000 | + + You can use [an online calculator](https://www.duplichecker.com/hex-to-text.php) to verify the values are correct. + + | Hex value | ASCII | + | ---------------------: | ----------- | + | 6e736d6573736167696e67 | nsmessaging | + | 73796d6573736167696e67 | symessaging | + | 6d657373616765 | message | + + ```solidity + vm.startBroadcast(deployerPrivateKey); + ``` + + Use the private key to submit transactions. + + ```solidity + world.registerNamespace(namespaceResource); + ``` + + [Register the namespace](https://github.com/latticexyz/mud/blob/main/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol#L39-L63). + + ```solidity + StoreSwitch.setStoreAddress(worldAddress); + Messages.register(); + ``` + + Register the `Messages` table to `worldAddress`. + + ```solidity + MessageSystem messageSystem = new MessageSystem(); + world.registerSystem(systemResource, messageSystem, true); + ``` + + Deploy the new system and then register it with the `World` we are extending. + The last parameter is whether or not we allow everybody to access this `System`. + + ```solidity + world.registerFunctionSelector(systemResource, "incrementMessage(string)"); + ``` + + Register `MessageSystem.incrementMessage(string)`. + This step is necessary to make the function accessible through the `World`. + The function's name when accessed through the world is `__`, so this function will be available as `messaging_message_incrementMessage(string)`. + + **Note:** This is the case for all namespaces except for the root namespace, where we just use the name of the function (such as `increment()`). + + ```solidity + vm.stopBroadcast(); + } + } + ``` + + Stop using the private key. + Here this call is not necessary because we immediately leave the script, but it is a good idea to include it in case `MessagingExtension.run()` ever becomes part of a larger script. + +
+ +1. Run the script. + Note that you need to provide the URL in the command line, you can't rely on the `ETH_RPC_URL` environment variable. + + ```sh copy + forge script script/MessagingExtension.s.sol --rpc-url http://localhost:8545 --broadcast + ``` + +1. Increment and write a message. + + ```sh copy + source .env + cast send $WORLD_ADDRESS --private-key $PRIVATE_KEY "messaging_message_incrementMessage(string)" "hello" + ``` + + When a function is _not_ in the root namespace, it is accessible as `__` (as long as it is [registered](https://github.com/latticexyz/mud/blob/main/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol#L164-L201)). + + Here we call our `incrementMessage(string)` with the parameter `hello`. + +1. You can see in the user interface of `extendMe` that the counter has been incremented. + To see the message we sent, use these commands: + + ```sh copy + TABLE_ID=0x74626d6573736167696e6700000000004d657373616765730000000000000000 + KEY=[`cast to-int256 2`] + RAW_RESULT=`cast call $WORLD_ADDRESS "getRecord(bytes32,bytes32[])" $TABLE_ID $KEY` + cast --to-ascii ${RAW_RESULT:322:-2} + ``` + +
+ + Explanation + + ```sh + TABLE_ID=0x74626d6573736167696e6700000000004d657373616765730000000000000000 + ``` + + `TABLE_ID` is the `ResourceId` for the table, taken from `src/codegen/tables/Messages.sol` + You can verify the interpretation [with the online calculator](https://www.duplichecker.com/hex-to-text.php). + + | Type | Namespace | Resource name | + | ------------ | --------- | ------------- | + | `tb` (table) | messaging | Messages | + + ```sh + KEY=[`cast to-int256 2`] + ``` + + The key to a MUD table is always an array of `byte32` values. + To create that value, we convert our key (`2`, because that's the first user call to `increment()`, the 0->1 increment is done by the post deploy script) to a 256 bit value using [`cast`](https://book.getfoundry.sh/reference/cast/cast-to-int256) and then envelop it in an array. + + ```sh + RAW_RESULT=`cast call $WORLD_ADDRESS "getRecord(bytes32,bytes32[],bytes32)" $TABLE_ID $KEY $FIELD_LAYOUT` + ``` + + To call [`getRecord`](https://github.com/latticexyz/mud/blob/main/packages/store/src/StoreRead.sol#L44-L57) we need the tableID, the key array, and the field layout. + The call result is the entire value, which may have multiple static (fixed length) and dynamic (variable length) fields. + + Here is the raw result divided into 32 byte words. + + | Word | Value | + | ---: | ------------------------------------------------------------------ | + | 0 | `0000000000000000000000000000000000000000000000000000000000000060` | + | 1 | `0000000000000000000000000000000000000000000000000500000000000005` | + | 2 | `0000000000000000000000000000000000000000000000000000000000000080` | + | 3 | `0000000000000000000000000000000000000000000000000000000000000000` | + | 4 | `0000000000000000000000000000000000000000000000000000000000000005` | + | 5 | `68656c6c6f000000000000000000000000000000000000000000000000000000` | + + When there is a variable-length field (a field whose length is not known at compile-time) in a Solidity function's return value, it is represented by a word that tells us at what offset into the return data that field starts. + This function is used for different tables with different lengths of static fields, so the static fields are a variable length field as far as Solidity is concerned. + + Word 0 shows us that this field's value starts at `0x60`, which is word 3. + Because there are no static fields, word 3 is all zeros. + + Word 1 is a [`PackedCounter`](https://github.com/latticexyz/mud/blob/main/packages/store/src/PackedCounter.sol) with the lengths of the dynamic fields. + Here is the interpretation. + + | Bytes | Value | Meaning | + | ----: | ---------------: | --------------------------------------------------------------------------- | + | 6-0 | `00000000000005` | The total length of all dynamic fields is five bytes. | + | 11-7 | `0000000005` | The length of the first dynamic field is five bytes. | + | 16-12 | `0000000000` | The length of the (non existent in `Messages`) second dynamic field is zero | + | 21-17 | `0000000000` | The length of the (non existent in `Messages`) third dynamic field is zero | + | 26-22 | `0000000000` | The length of the (non existent in `Messages`) fourth dynamic field is zero | + | 31-27 | `0000000000` | The length of the (non existent in `Messages`) fifth dynamic field is zero | + + MUD tables can only have up to five dynamic fields because `PackedCounter` needs to fit in a 32 byte word. + + Word 2 shows us that the field with the dynamic lengths starts at byte 0x80, which is word 4. + Word 4 gives us the length of the string. + + Finally, word 5 gives us the actual message, "hello". + + ```sh + cast --to-ascii ${RAW_RESULT:322:-2} + ``` + + Based on the above we don't need the first 322 characters of `$RAW_RESULT`. + The first two characters are the `0x` that tells us this is a hexadecimal number. + The next 320 characters are words 0-4, which are not the actual message (each word is 32 bytes, which is 64 hexadecimal digits). We also don't need the trailing newline. `${RAW_RESULT:322:-2}` removes those characters so we can use [`cast`](https://book.getfoundry.sh/reference/cast/cast-to-ascii) to get the ASCII. + +