Version 2.0.0-next.9
Major changes
feat(world): prevent the World
from calling itself (#1563) (opens in a new tab) (@latticexyz/world)
All World
methods now revert if the World
calls itself.
The World
should never need to externally call itself, since all internal table operations happen via library calls, and all root system operations happen via delegate call.
It should not be possible to make the World
call itself as an external actor.
If it were possible to make the World
call itself, it would be possible to write to internal tables that only the World
should have access to.
As this is a very important invariance, we made it explicit in a requirement check in every World
method, rather than just relying on making it impossible to trigger the World
to call itself.
This is a breaking change for modules that previously used external calls to the World
in the installRoot
method.
In the installRoot
method, the World
can only be called via delegatecall
, and table operations should be performed via the internal table methods (e.g. _set
instead of set
).
Example for how to replace external calls to world
in root systems / root modules (installRoot
) with delegatecall
:
+ import { revertWithBytes } from "@latticexyz/world/src/revertWithBytes.sol";
- world.grantAccess(tableId, address(hook));
+ (bool success, bytes memory returnData) = address(world).delegatecall(
+ abi.encodeCall(world.grantAccess, (tableId, address(hook)))
+ );
+ if (!success) revertWithBytes(returnData);
feat: rename schema to valueSchema (#1482) (opens in a new tab) (@latticexyz/cli, @latticexyz/protocol-parser, @latticexyz/store-sync, @latticexyz/store, create-mud)
Renamed all occurrences of schema
where it is used as "value schema" to valueSchema
to clearly distinguish it from "key schema".
The only breaking change for users is the change from schema
to valueSchema
in mud.config.ts
.
// mud.config.ts
export default mudConfig({
tables: {
CounterTable: {
keySchema: {},
- schema: {
+ valueSchema: {
value: "uint32",
},
},
}
}
refactor(store): always render field methods with suffix and conditionally without (#1550) (opens in a new tab) (@latticexyz/common)
- Add
renderWithFieldSuffix
helper method to always render a field function with a suffix, and optionally render the same function without a suffix. - Remove
methodNameSuffix
fromRenderField
interface, because the suffix is now computed as part ofrenderWithFieldSuffix
.
feat(store,): add splice events (#1354) (opens in a new tab) (@latticexyz/store, @latticexyz/world)
We've updated Store events to be "schemaless", meaning there is enough information in each event to only need to operate on the bytes of each record to make an update to that record without having to first decode the record by its schema. This enables new kinds of indexers and sync strategies.
If you've written your own sync logic or are interacting with Store calls directly, this is a breaking change. We have a few more breaking protocol changes upcoming, so you may hold off on upgrading until those land.
If you are using MUD's built-in tooling (table codegen, indexer, store sync, etc.), you don't have to make any changes except upgrading to the latest versions and deploying a fresh World.
-
The
data
field in eachStoreSetRecord
andStoreEphemeralRecord
has been replaced with three new fields:staticData
,encodedLengths
, anddynamicData
. This better reflects the on-chain state and makes it easier to perform modifications to the raw bytes. We recommend storing each of these fields individually in your off-chain storage of choice (indexer, client, etc.).- event StoreSetRecord(bytes32 tableId, bytes32[] keyTuple, bytes data); + event StoreSetRecord(bytes32 tableId, bytes32[] keyTuple, bytes staticData, bytes32 encodedLengths, bytes dynamicData); - event StoreEphemeralRecord(bytes32 tableId, bytes32[] keyTuple, bytes data); + event StoreEphemeralRecord(bytes32 tableId, bytes32[] keyTuple, bytes staticData, bytes32 encodedLengths, bytes dynamicData);
-
The
StoreSetField
event is now replaced by two new events:StoreSpliceStaticData
andStoreSpliceDynamicData
. Splicing allows us to perform efficient operations like push and pop, in addition to replacing a field value. We use two events because updating a dynamic-length field also requires updating the record'sencodedLengths
(aka PackedCounter).- event StoreSetField(bytes32 tableId, bytes32[] keyTuple, uint8 fieldIndex, bytes data); + event StoreSpliceStaticData(bytes32 tableId, bytes32[] keyTuple, uint48 start, uint40 deleteCount, bytes data); + event StoreSpliceDynamicData(bytes32 tableId, bytes32[] keyTuple, uint48 start, uint40 deleteCount, bytes data, bytes32 encodedLengths);
Similarly, Store setter methods (e.g. setRecord
) have been updated to reflect the data
to staticData
, encodedLengths
, and dynamicData
changes. We'll be following up shortly with Store getter method changes for more gas efficient storage reads.
refactor(store): change argument order on Store_SpliceDynamicData
and hooks for consistency (#1589) (opens in a new tab) (@latticexyz/store)
The argument order on Store_SpliceDynamicData
, onBeforeSpliceDynamicData
and onAfterSpliceDynamicData
has been changed to match the argument order on Store_SetRecord
,
where the PackedCounter encodedLength
field comes before the bytes dynamicData
field.
IStore {
event Store_SpliceDynamicData(
ResourceId indexed tableId,
bytes32[] keyTuple,
uint48 start,
uint40 deleteCount,
+ PackedCounter encodedLengths,
bytes data,
- PackedCounter encodedLengths
);
}
IStoreHook {
function onBeforeSpliceDynamicData(
ResourceId tableId,
bytes32[] memory keyTuple,
uint8 dynamicFieldIndex,
uint40 startWithinField,
uint40 deleteCount,
+ PackedCounter encodedLengths,
bytes memory data,
- PackedCounter encodedLengths
) external;
function onAfterSpliceDynamicData(
ResourceId tableId,
bytes32[] memory keyTuple,
uint8 dynamicFieldIndex,
uint40 startWithinField,
uint40 deleteCount,
+ PackedCounter encodedLengths,
bytes memory data,
- PackedCounter encodedLengths
) external;
}
feat(store,): add splice events (#1354) (opens in a new tab) (@latticexyz/common, @latticexyz/protocol-parser)
readHex
was moved from @latticexyz/protocol-parser
to @latticexyz/common
feat(store,world): move hooks to bit flags (#1527) (opens in a new tab) (@latticexyz/store, @latticexyz/world)
Moved the registration of store hooks and systems hooks to bitmaps with bitwise operator instead of a struct.
- import { StoreHookLib } from "@latticexyz/src/StoreHook.sol";
+ import {
+ BEFORE_SET_RECORD,
+ BEFORE_SET_FIELD,
+ BEFORE_DELETE_RECORD
+ } from "@latticexyz/store/storeHookTypes.sol";
StoreCore.registerStoreHook(
tableId,
subscriber,
- StoreHookLib.encodeBitmap({
- onBeforeSetRecord: true,
- onAfterSetRecord: false,
- onBeforeSetField: true,
- onAfterSetField: false,
- onBeforeDeleteRecord: true,
- onAfterDeleteRecord: false
- })
+ BEFORE_SET_RECORD | BEFORE_SET_FIELD | BEFORE_DELETE_RECORD
);
- import { SystemHookLib } from "../src/SystemHook.sol";
+ import { BEFORE_CALL_SYSTEM, AFTER_CALL_SYSTEM } from "../src/systemHookTypes.sol";
world.registerSystemHook(
systemId,
subscriber,
- SystemHookLib.encodeBitmap({ onBeforeCallSystem: true, onAfterCallSystem: true })
+ BEFORE_CALL_SYSTEM | AFTER_CALL_SYSTEM
);
feat(store,world): add splice hooks, expose spliceStaticData, spliceDynamicData (#1531) (opens in a new tab) (@latticexyz/store, @latticexyz/world)
-
The
IStoreHook
interface was changed to replaceonBeforeSetField
andonAfterSetField
withonBeforeSpliceStaticData
,onAfterSpliceStaticData
,onBeforeSpliceDynamicData
andonAfterSpliceDynamicData
.This new interface matches the new
StoreSpliceStaticData
andStoreSpliceDynamicData
events, and avoids having to read the entire field from storage when only a subset of the field was updated (e.g. when pushing elements to a field).interface IStoreHook { - function onBeforeSetField( - bytes32 tableId, - bytes32[] memory keyTuple, - uint8 fieldIndex, - bytes memory data, - FieldLayout fieldLayout - ) external; - function onAfterSetField( - bytes32 tableId, - bytes32[] memory keyTuple, - uint8 fieldIndex, - bytes memory data, - FieldLayout fieldLayout - ) external; + function onBeforeSpliceStaticData( + bytes32 tableId, + bytes32[] memory keyTuple, + uint48 start, + uint40 deleteCount, + bytes memory data + ) external; + function onAfterSpliceStaticData( + bytes32 tableId, + bytes32[] memory keyTuple, + uint48 start, + uint40 deleteCount, + bytes memory data + ) external; + function onBeforeSpliceDynamicData( + bytes32 tableId, + bytes32[] memory keyTuple, + uint8 dynamicFieldIndex, + uint40 startWithinField, + uint40 deleteCount, + bytes memory data, + PackedCounter encodedLengths + ) external; + function onAfterSpliceDynamicData( + bytes32 tableId, + bytes32[] memory keyTuple, + uint8 dynamicFieldIndex, + uint40 startWithinField, + uint40 deleteCount, + bytes memory data, + PackedCounter encodedLengths + ) external; }
-
All
calldata
parameters on theIStoreHook
interface were changed tomemory
, since the functions are called withmemory
from theWorld
. -
IStore
exposes two new functions:spliceStaticData
andspliceDynamicData
.These functions provide lower level access to the operations happening under the hood in
setField
,pushToField
,popFromField
andupdateInField
and simplify handling the new splice hooks.StoreCore
's internal logic was simplified to use thespliceStaticData
andspliceDynamicData
functions instead of duplicating similar logic in different functions.interface IStore { // Splice data in the static part of the record function spliceStaticData( bytes32 tableId, bytes32[] calldata keyTuple, uint48 start, uint40 deleteCount, bytes calldata data ) external; // Splice data in the dynamic part of the record function spliceDynamicData( bytes32 tableId, bytes32[] calldata keyTuple, uint8 dynamicFieldIndex, uint40 startWithinField, uint40 deleteCount, bytes calldata data ) external; }
feat: replace Schema with FieldLayout for contract internals (#1336) (opens in a new tab) (@latticexyz/cli, @latticexyz/protocol-parser, @latticexyz/store, @latticexyz/world)
-
Add
FieldLayout
, which is abytes32
user-type similar toSchema
.Both
FieldLayout
andSchema
have the same kind of data in the first 4 bytes.- 2 bytes for total length of all static fields
- 1 byte for number of static size fields
- 1 byte for number of dynamic size fields
But whereas
Schema
hasSchemaType
enum in each of the other 28 bytes,FieldLayout
has static byte lengths in each of the other 28 bytes. -
Replace
Schema valueSchema
withFieldLayout fieldLayout
in Store and World contracts.FieldLayout
is more gas-efficient because it already has lengths, andSchema
has types which need to be converted to lengths. -
Add
getFieldLayout
toIStore
interface.There is no
FieldLayout
for keys, only for values, because key byte lengths aren't usually relevant on-chain. You can still usegetKeySchema
if you need key types. -
Add
fieldLayoutToHex
utility toprotocol-parser
package. -
Add
constants.sol
for constants shared betweenFieldLayout
,Schema
andPackedCounter
.
refactor: separate data into staticData, encodedLengths, dynamicData in getRecord (#1532) (opens in a new tab) (@latticexyz/store, @latticexyz/world)
Store's getRecord
has been updated to return staticData
, encodedLengths
, and dynamicData
instead of a single data
blob, to match the new behaviour of Store setter methods.
If you use codegenerated libraries, you will only need to update encode
calls.
- bytes memory data = Position.encode(x, y);
+ (bytes memory staticData, PackedCounter encodedLengths, bytes memory dynamicData) = Position.encode(x, y);
feat(world): add FunctionSignatures
offchain table (#1575) (opens in a new tab) (@latticexyz/cli, @latticexyz/world)
The registerRootFunctionSelector
function's signature was changed to accept a string functionSignature
parameter instead of a bytes4 functionSelector
parameter.
This change enables the World
to store the function signatures of all registered functions in a FunctionSignatures
offchain table, which will allow for the automatic generation of interfaces for a given World
address in the future.
IBaseWorld {
function registerRootFunctionSelector(
ResourceId systemId,
- bytes4 worldFunctionSelector,
+ string memory worldFunctionSignature,
bytes4 systemFunctionSelector
) external returns (bytes4 worldFunctionSelector);
}
feat: move forge build + abi + abi-ts to out (#1483) (opens in a new tab) (@latticexyz/store, @latticexyz/world)
Store and World contract ABIs are now exported from the out
directory. You'll need to update your imports like:
- import IBaseWorldAbi from "@latticexyz/world/abi/IBaseWorld.sol/IBaseWorldAbi.json";
+ import IBaseWorldAbi from "@latticexyz/world/out/IBaseWorld.sol/IBaseWorldAbi.json";
MudTest.sol
was also moved to the World package. You can update your import like:
- import { MudTest } from "@latticexyz/store/src/MudTest.sol";
+ import { MudTest } from "@latticexyz/world/test/MudTest.t.sol";
feat(common,store): add support for user-defined types (#1566) (opens in a new tab) (@latticexyz/store)
These breaking changes only affect store utilities, you aren't affected if you use @latticexyz/cli
codegen scripts.
- Add
remappings
argument to thetablegen
codegen function, so that it can read user-provided files. - In
RenderTableOptions
change the type ofimports
fromRelativeImportDatum
toImportDatum
, to allow passing absolute imports to the table renderer. - Add
solidityUserTypes
argument to several functions that need to resolve user or abi types:resolveAbiOrUserType
,importForAbiOrUserType
,getUserTypeInfo
. - Add
userTypes
config option to MUD config, which takes user types mapped to file paths from which to import them.
refactor(store): always render field methods with suffix and conditionally without (#1550) (opens in a new tab) (@latticexyz/store)
- Always render field methods with a suffix in tablegen (they used to not be rendered if field methods without a suffix were rendered).
- Add
withSuffixlessFieldMethods
toRenderTableOptions
, which indicates that field methods without a suffix should be rendered.
feat(world): change registerFunctionSelector signature to accept system signature as a single string (#1574) (opens in a new tab) (@latticexyz/cli, @latticexyz/world)
The registerFunctionSelector
function now accepts a single functionSignature
string paramemer instead of separating function name and function arguments into separate parameters.
IBaseWorld {
function registerFunctionSelector(
ResourceId systemId,
- string memory systemFunctionName,
- string memory systemFunctionArguments
+ string memory systemFunctionSignature
) external returns (bytes4 worldFunctionSelector);
}
This is a breaking change if you were manually registering function selectors, e.g. in a PostDeploy.s.sol
script or a module.
To upgrade, simply replace the separate systemFunctionName
and systemFunctionArguments
parameters with a single systemFunctionSignature
parameter.
world.registerFunctionSelector(
systemId,
- systemFunctionName,
- systemFunctionArguments,
+ string(abi.encodePacked(systemFunctionName, systemFunctionArguments))
);
feat(store,world): replace ResourceSelector
with ResourceId
and WorldResourceId
(#1544) (opens in a new tab) (@latticexyz/world)
All World
methods acting on namespaces as resources have been updated to use ResourceId namespaceId
as parameter instead of bytes14 namespace
.
The reason for this change is to make it clearer when a namespace is used as resource, as opposed to being part of another resource's ID.
+ import { ResourceId } from "@latticexyz/store/src/ResourceId.sol";
IBaseWorld {
- function registerNamespace(bytes14 namespace) external;
+ function registerNamespace(ResourceId namespaceId) external;
- function transferOwnership(bytes14 namespace, address newOwner) external;
+ function transferOwnership(ResourceId namespaceId, address newOwner) external;
- function transferBalanceToNamespace(bytes14 fromNamespace, bytes14 toNamespace, uint256 amount) external;
+ function transferBalanceToNamespace(ResourceId fromNamespaceId, ResourceId toNamespaceId, uint256 amount) external;
- function transferBalanceToAddress(bytes14 fromNamespace, address toAddress, uint256 amount) external;
+ function transferBalanceToAddress(ResourceId fromNamespaceId, address toAddress, uint256 amount) external;
}
feat: bump solidity to 0.8.21 (#1473) (opens in a new tab) (@latticexyz/cli, @latticexyz/common, @latticexyz/gas-report, @latticexyz/noise, @latticexyz/schema-type, @latticexyz/store, @latticexyz/world, create-mud)
Bump Solidity version to 0.8.21
feat(store): codegen index and common files (#1318) (opens in a new tab) (@latticexyz/cli, @latticexyz/store, create-mud)
Renamed the default filename of generated user types from Types.sol
to common.sol
and the default filename of the generated table index file from Tables.sol
to index.sol
.
Both can be overridden via the MUD config:
export default mudConfig({
/** Filename where common user types will be generated and imported from. */
userTypesFilename: "common.sol",
/** Filename where codegen index will be generated. */
codegenIndexFilename: "index.sol",
});
Note: userTypesFilename
was renamed from userTypesPath
and .sol
is not appended automatically anymore but needs to be part of the provided filename.
To update your existing project, update all imports from Tables.sol
to index.sol
and all imports from Types.sol
to common.sol
, or override the defaults in your MUD config to the previous values.
- import { Counter } from "../src/codegen/Tables.sol";
+ import { Counter } from "../src/codegen/index.sol";
- import { ExampleEnum } from "../src/codegen/Types.sol";
+ import { ExampleEnum } from "../src/codegen/common.sol";
feat(store,): add splice events (#1354) (opens in a new tab) (@latticexyz/dev-tools, @latticexyz/store-sync, create-mud)
We've updated Store events to be "schemaless", meaning there is enough information in each event to only need to operate on the bytes of each record to make an update to that record without having to first decode the record by its schema. This enables new kinds of indexers and sync strategies.
As such, we've replaced blockStorageOperations$
with storedBlockLogs$
, a stream of simplified Store event logs after they've been synced to the configured storage adapter. These logs may not reflect exactly the events that are on chain when e.g. hydrating from an indexer, but they will still allow the client to "catch up" to the on-chain state of your tables.
feat(store,world): replace ephemeral
tables with offchain
tables (#1558) (opens in a new tab) (@latticexyz/block-logs-stream, @latticexyz/cli, @latticexyz/common, @latticexyz/dev-tools, @latticexyz/store-sync, @latticexyz/store, create-mud)
What used to be known as ephemeral
table is now called offchain
table.
The previous ephemeral
tables only supported an emitEphemeral
method, which emitted a StoreSetEphemeralRecord
event.
Now offchain
tables support all regular table methods, except partial operations on dynamic fields (push
, pop
, update
).
Unlike regular tables they don't store data on-chain but emit the same events as regular tables (StoreSetRecord
, StoreSpliceStaticData
, StoreDeleteRecord
), so their data can be indexed by offchain indexers/clients.
- EphemeralTable.emitEphemeral(value);
+ OffchainTable.set(value);
feat(store): rename events for readability and consistency with errors (#1577) (opens in a new tab) (@latticexyz/block-logs-stream, @latticexyz/dev-tools, @latticexyz/store-sync, @latticexyz/store)
Store
events have been renamed for consistency and readability.
If you're parsing Store
events manually, you need to update your ABI.
If you're using the MUD sync stack, the new events are already integrated and no further changes are necessary.
- event StoreSetRecord(
+ event Store_SetRecord(
ResourceId indexed tableId,
bytes32[] keyTuple,
bytes staticData,
bytes32 encodedLengths,
bytes dynamicData
);
- event StoreSpliceStaticData(
+ event Store_SpliceStaticData(
ResourceId indexed tableId,
bytes32[] keyTuple,
uint48 start,
uint40 deleteCount,
bytes data
);
- event StoreSpliceDynamicData(
+ event Store_SpliceDynamicData(
ResourceId indexed tableId,
bytes32[] keyTuple,
uint48 start,
uint40 deleteCount,
bytes data,
bytes32 encodedLengths
);
- event StoreDeleteRecord(
+ event Store_DeleteRecord(
ResourceId indexed tableId,
bytes32[] keyTuple
);
feat(store,world): replace ResourceSelector
with ResourceId
and WorldResourceId
(#1544) (opens in a new tab) (@latticexyz/cli, @latticexyz/common, @latticexyz/config, @latticexyz/store)
-
ResourceSelector
is replaced withResourceId
,ResourceIdLib
,ResourceIdInstance
,WorldResourceIdLib
andWorldResourceIdInstance
.Previously a "resource selector" was a
bytes32
value with the first 16 bytes reserved for the resource's namespace, and the last 16 bytes reserved for the resource's name. Now a "resource ID" is abytes32
value with the first 2 bytes reserved for the resource type, the next 14 bytes reserved for the resource's namespace, and the last 16 bytes reserved for the resource's name.Previously
ResouceSelector
was a library and the resource selector type was a plainbytes32
. NowResourceId
is a user type, and the functionality is implemented in theResourceIdInstance
(for type) andWorldResourceIdInstance
(for namespace and name) libraries. We split the logic into two libraries, becauseStore
now also usesResourceId
and needs to be aware of resource types, but not of namespaces/names.- import { ResourceSelector } from "@latticexyz/world/src/ResourceSelector.sol"; + import { ResourceId, ResourceIdInstance } from "@latticexyz/store/src/ResourceId.sol"; + import { WorldResourceIdLib, WorldResourceIdInstance } from "@latticexyz/world/src/WorldResourceId.sol"; + import { RESOURCE_SYSTEM } from "@latticexyz/world/src/worldResourceTypes.sol"; - bytes32 systemId = ResourceSelector.from("namespace", "name"); + ResourceId systemId = WorldResourceIdLib.encode(RESOURCE_SYSTEM, "namespace", "name"); - using ResourceSelector for bytes32; + using WorldResourceIdInstance for ResourceId; + using ResourceIdInstance for ResourceId; systemId.getName(); systemId.getNamespace(); + systemId.getType();
-
All
Store
andWorld
methods now use theResourceId
type fortableId
,systemId
,moduleId
andnamespaceId
. All mentions ofresourceSelector
were renamed toresourceId
or the more specific type (e.g.tableId
,systemId
)import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; IStore { function setRecord( - bytes32 tableId, + ResourceId tableId, bytes32[] calldata keyTuple, bytes calldata staticData, PackedCounter encodedLengths, bytes calldata dynamicData, FieldLayout fieldLayout ) external; // Same for all other methods }
import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; IBaseWorld { function callFrom( address delegator, - bytes32 resourceSelector, + ResourceId systemId, bytes memory callData ) external payable returns (bytes memory); // Same for all other methods }
feat(store): indexed tableId
in store events (#1520) (opens in a new tab) (@latticexyz/store)
Store events now use an indexed
tableId
. This adds ~100 gas per write, but means we our sync stack can filter events by table.
feat(world,store): add initialize method, initialize core tables in core module (#1472) (opens in a new tab) (@latticexyz/store)
-
StoreCore
'sinitialize
function is split intoinitialize
(to set theStoreSwitch
'sstoreAddress
) andregisterCoreTables
(to register theTables
andStoreHooks
tables). The purpose of this is to give consumers more granular control over the setup flow. -
The
StoreRead
contract no longer callsStoreCore.initialize
in its constructor.StoreCore
consumers are expected to callStoreCore.initialize
andStoreCore.registerCoreTable
in their own setup logic.
feat(store): add internalType
property to user types config for type inference (#1587) (opens in a new tab) (@latticexyz/cli, @latticexyz/common, @latticexyz/store)
Changed the userTypes
property to accept { filePath: string, internalType: SchemaAbiType }
to enable strong type inference from the config.
feat(store,world): polish store methods (#1581) (opens in a new tab) (@latticexyz/block-logs-stream, @latticexyz/cli, @latticexyz/common, @latticexyz/dev-tools, @latticexyz/store-sync, @latticexyz/store, @latticexyz/world, create-mud)
-
The external
setRecord
anddeleteRecord
methods ofIStore
no longer accept aFieldLayout
as input, but load it from storage instead. This is to prevent invalidFieldLayout
values being passed, which could cause the onchain state to diverge from the indexer state. However, the internalStoreCore
library still exposes asetRecord
anddeleteRecord
method that allows aFieldLayout
to be passed. This is becauseStoreCore
can only be used internally, so theFieldLayout
value can be trusted and we can save the gas for accessing storage.interface IStore { function setRecord( ResourceId tableId, bytes32[] calldata keyTuple, bytes calldata staticData, PackedCounter encodedLengths, bytes calldata dynamicData, - FieldLayout fieldLayout ) external; function deleteRecord( ResourceId tableId, bytes32[] memory keyTuple, - FieldLayout fieldLayout ) external; }
-
The
spliceStaticData
method andStore_SpliceStaticData
event ofIStore
andStoreCore
no longer includedeleteCount
in their signature. This is because when splicing static data, the data afterstart
is always overwritten withdata
instead of being shifted, sodeleteCount
is always the length of the data to be written.event Store_SpliceStaticData( ResourceId indexed tableId, bytes32[] keyTuple, uint48 start, - uint40 deleteCount, bytes data ); interface IStore { function spliceStaticData( ResourceId tableId, bytes32[] calldata keyTuple, uint48 start, - uint40 deleteCount, bytes calldata data ) external; }
-
The
updateInField
method has been removed fromIStore
, as it's almost identical to the more generalspliceDynamicData
. If you're manually callingupdateInField
, here is how to upgrade tospliceDynamicData
:- store.updateInField(tableId, keyTuple, fieldIndex, startByteIndex, dataToSet, fieldLayout); + uint8 dynamicFieldIndex = fieldIndex - fieldLayout.numStaticFields(); + store.spliceDynamicData(tableId, keyTuple, dynamicFieldIndex, uint40(startByteIndex), uint40(dataToSet.length), dataToSet);
-
All other methods that are only valid for dynamic fields (
pushToField
,popFromField
,getFieldSlice
) have been renamed to make this more explicit (pushToDynamicField
,popFromDynamicField
,getDynamicFieldSlice
).Their
fieldIndex
parameter has been replaced by adynamicFieldIndex
parameter, which is the index relative to the first dynamic field (i.e.dynamicFieldIndex
=fieldIndex
-numStaticFields
). TheFieldLayout
parameter has been removed, as it was only used to calculate thedynamicFieldIndex
in the method.interface IStore { - function pushToField( + function pushToDynamicField( ResourceId tableId, bytes32[] calldata keyTuple, - uint8 fieldIndex, + uint8 dynamicFieldIndex, bytes calldata dataToPush, - FieldLayout fieldLayout ) external; - function popFromField( + function popFromDynamicField( ResourceId tableId, bytes32[] calldata keyTuple, - uint8 fieldIndex, + uint8 dynamicFieldIndex, uint256 byteLengthToPop, - FieldLayout fieldLayout ) external; - function getFieldSlice( + function getDynamicFieldSlice( ResourceId tableId, bytes32[] memory keyTuple, - uint8 fieldIndex, + uint8 dynamicFieldIndex, - FieldLayout fieldLayout, uint256 start, uint256 end ) external view returns (bytes memory data); }
-
IStore
has a newgetDynamicFieldLength
length method, which returns the byte length of the given dynamic field and doesn't require theFieldLayout
.IStore { + function getDynamicFieldLength( + ResourceId tableId, + bytes32[] memory keyTuple, + uint8 dynamicFieldIndex + ) external view returns (uint256); }
-
IStore
now has additional overloads forgetRecord
,getField
,getFieldLength
andsetField
that don't require aFieldLength
to be passed, but instead load it from storage. -
IStore
now exposessetStaticField
andsetDynamicField
to save gas by avoiding the dynamic inference of whether the field is static or dynamic. -
The
getDynamicFieldSlice
method no longer accepts reading outside the bounds of the dynamic field. This is to avoid returning invalid data, as the data of a dynamic field is not deleted when the record is deleted, but only its length is set to zero.
Minor changes
feat(dev-tools): update actions to display function name instead of callFrom (#1497) (opens in a new tab) (@latticexyz/dev-tools)
Improved rendering of transactions that make calls via World's call
and callFrom
methods
feat(faucet): add faucet service (#1517) (opens in a new tab) (@latticexyz/faucet)
New package to run your own faucet service. We'll use this soon for our testnet in place of @latticexyz/services
.
To run the faucet server:
- Add the package with
pnpm add @latticexyz/faucet
- Add a
.env
file that has aRPC_HTTP_URL
andFAUCET_PRIVATE_KEY
(or pass the environment variables into the next command) - Run
pnpm faucet-server
to start the server
You can also adjust the server's HOST
(defaults to 0.0.0.0
) and PORT
(defaults to 3002
). The tRPC routes are accessible under /trpc
.
To connect a tRPC client, add the package with pnpm add @latticexyz/faucet
and then use createClient
:
import { createClient } from "@latticexyz/faucet";
const faucet = createClient({ url: "http://localhost:3002/trpc" });
await faucet.mutate.drip({ address: burnerAccount.address });
feat(world): add registerNamespaceDelegation
for namespace-bound fallback delegation controls (#1590) (opens in a new tab) (@latticexyz/world)
It is now possible for namespace owners to register a fallback delegation control system for the namespace.
This fallback delegation control system is used to verify a delegation in IBaseWorld.callFrom
, after the user's individual and fallback delegations have been checked.
IBaseWorld {
function registerNamespaceDelegation(
ResourceId namespaceId,
ResourceId delegationControlId,
bytes memory initCallData
) external;
}
feat: move forge build + abi + abi-ts to out (#1483) (opens in a new tab) (create-mud)
Templates now use out
for their forge build
artifacts, including ABIs. If you have a project created from a previous template, you can update your packages/contracts/package.json
with:
- "build:abi": "rimraf abi && forge build --extra-output-files abi --out abi --skip test script MudTest.sol",
- "build:abi-ts": "mud abi-ts --input 'abi/IWorld.sol/IWorld.abi.json' && prettier --write '**/*.abi.json.d.ts'",
+ "build:abi": "forge clean && forge build --skip test script",
+ "build:abi-ts": "mud abi-ts && prettier --write '**/*.abi.json.d.ts'",
And your packages/client/src/mud/setupNetwork
with:
- import IWorldAbi from "contracts/abi/IWorld.sol/IWorld.abi.json";
+ import IWorldAbi from "contracts/out/IWorld.sol/IWorld.abi.json";
feat(common,store): add support for user-defined types (#1566) (opens in a new tab) (@latticexyz/common)
- Add
getRemappings
to get foundry remappings as an array of[to, from]
tuples. - Add
extractUserTypes
solidity parser utility to extract user-defined types. - Add
loadAndExtractUserTypes
helper to load and parse a solidity file, extracting user-defined types.
feat(store-indexer): run indexers with npx (#1526) (opens in a new tab) (@latticexyz/store-indexer)
You can now install and run @latticexyz/store-indexer
from the npm package itself, without having to clone/build the MUD repo:
npm install @latticexyz/store-indexer
npm sqlite-indexer
# or
npm postgres-indexer
or
npx -p @latticexyz/store-indexer sqlite-indexer
# or
npx -p @latticexyz/store-indexer postgres-indexer
The binary will also load the nearby .env
file for easier local configuration.
We've removed the CHAIN_ID
requirement and instead require just a RPC_HTTP_URL
or RPC_WS_URL
or both. You can now also adjust the polling interval with POLLING_INTERVAL
(defaults to 1000ms, which corresponds to MUD's default block time).
feat(store,): add splice events (#1354) (opens in a new tab) (@latticexyz/common)
spliceHex
was added, which has a similar API as JavaScript's Array.prototype.splice
(opens in a new tab), but for Hex
strings.
spliceHex("0x123456", 1, 1, "0x0000"); // "0x12000056"
feat(protoocl-parser): add valueSchemaToFieldLayoutHex (#1476) (opens in a new tab) (@latticexyz/protocol-parser)
Adds valueSchemaToFieldLayoutHex
helper
feat(store,world): emit Store/World versions (#1511) (opens in a new tab) (@latticexyz/store, @latticexyz/world)
Add protocol version with corresponding getter and event on deploy
world.worldVersion();
world.storeVersion(); // a World is also a Store
event HelloWorld(bytes32 indexed worldVersion);
event HelloStore(bytes32 indexed storeVersion);
feat(store): expose getStaticField
and getDynamicField
on IStore
and use it in codegen tables (#1521) (opens in a new tab) (@latticexyz/cli, @latticexyz/store, @latticexyz/world)
StoreCore
and IStore
now expose specific functions for getStaticField
and getDynamicField
in addition to the general getField
.
Using the specific functions reduces gas overhead because more optimized logic can be executed.
interface IStore {
/**
* Get a single static field from the given tableId and key tuple, with the given value field layout.
* Note: the field value is left-aligned in the returned bytes32, the rest of the word is not zeroed out.
* Consumers are expected to truncate the returned value as needed.
*/
function getStaticField(
bytes32 tableId,
bytes32[] calldata keyTuple,
uint8 fieldIndex,
FieldLayout fieldLayout
) external view returns (bytes32);
/**
* Get a single dynamic field from the given tableId and key tuple at the given dynamic field index.
* (Dynamic field index = field index - number of static fields)
*/
function getDynamicField(
bytes32 tableId,
bytes32[] memory keyTuple,
uint8 dynamicFieldIndex
) external view returns (bytes memory);
}
refactor(store): inline logic in codegenned set method which uses struct (#1542) (opens in a new tab) (@latticexyz/store)
Add an optional namePrefix
argument to renderRecordData
, to support inlined logic in codegenned set
method which uses a struct.
feat(store): indexed tableId
in store events (#1520) (opens in a new tab) (@latticexyz/cli, @latticexyz/common, @latticexyz/store, @latticexyz/world)
Generated table libraries now have a set of functions prefixed with _
that always use their own storage for read/write.
This saves gas for use cases where the functionality to dynamically determine which Store
to use for read/write is not needed, e.g. root systems in a World
, or when using Store
without World
.
We decided to continue to always generate a set of functions that dynamically decide which Store
to use, so that the generated table libraries can still be imported by non-root systems.
library Counter {
// Dynamically determine which store to write to based on the context
function set(uint32 value) internal;
// Always write to own storage
function _set(uint32 value) internal;
// ... equivalent functions for all other Store methods
}
feat(world,store): add initialize method, initialize core tables in core module (#1472) (opens in a new tab) (@latticexyz/cli, @latticexyz/world)
-
The
World
contract now has aninitialize
function, which can be called once by the creator of the World to install the core module. This change allows the registration of all core tables to happen in theCoreModule
, so no table metadata has to be included in theWorld
's bytecode.interface IBaseWorld { function initialize(IModule coreModule) public; }
-
The
World
contract now stores the original creator of theWorld
in an immutable state variable. It is used internally to only allow the original creator to initialize theWorld
in a separate transaction.interface IBaseWorld { function creator() external view returns (address); }
-
The deploy script is updated to use the
World
'sinitialize
function to install theCoreModule
instead ofregisterRootModule
as before.
feat(world): add CallBatchSystem to core module (#1500) (opens in a new tab) (@latticexyz/world)
The World
now has a callBatch
method which allows multiple system calls to be batched into a single transaction.
import { SystemCallData } from "@latticexyz/world/modules/core/types.sol";
interface IBaseWorld {
function callBatch(SystemCallData[] calldata systemCalls) external returns (bytes[] memory returnDatas);
}
Patch changes
fix(store-indexer): subscribe postgres indexer to stream (#1514) (opens in a new tab) (@latticexyz/store-indexer)
Fixes postgres indexer stopping sync after it catches up to the latest block.
fix: release bytecode on npm and import abi in cli deploy (#1490) (opens in a new tab) (@latticexyz/cli, @latticexyz/store, @latticexyz/world)
Include bytecode for World
and Store
in npm packages.
fix(common): always import relative sol files from ./ (#1585) (opens in a new tab) (@latticexyz/common)
Minor fix to resolving user types: solc
doesn't like relative imports without ./
, but is fine with relative imports from ./../
, so we always append ./
to the relative path.
feat(store): compute FieldLayout at compile time (#1508) (opens in a new tab) (@latticexyz/cli, @latticexyz/store, @latticexyz/world)
The FieldLayout
in table libraries is now generated at compile time instead of dynamically in a table library function.
This significantly reduces gas cost in all table library functions.
feat(store): add Storage.loadField for optimized loading of 32 bytes or less from storage (#1512) (opens in a new tab) (@latticexyz/store)
Added Storage.loadField
to optimize loading 32 bytes or less from storage (which is always the case when loading data for static fields).
refactor(store,world): prefix errors with library/contract name (#1568) (opens in a new tab) (@latticexyz/store, @latticexyz/world)
Prefixed all errors with their respective library/contract for improved debugging.
refactor(world): remove workaround in mud config (#1501) (opens in a new tab) (@latticexyz/world)
Remove a workaround for the internal InstalledModules
table that is not needed anymore.
feat(world): rename funcSelectorAndArgs
to callData
(#1524) (opens in a new tab) (@latticexyz/world)
Renamed all funcSelectorAndArgs
arguments to callData
for clarity.
fix(faucet,store-indexer): fix invalid env message (#1546) (opens in a new tab) (@latticexyz/faucet, @latticexyz/store-indexer)
Improves error message when parsing env variables
feat(store,world): replace ResourceSelector
with ResourceId
and WorldResourceId
(#1544) (opens in a new tab) (@latticexyz/world, @latticexyz/store)
The ResourceType
table is removed.
It was previously used to store the resource type for each resource ID in a World
. This is no longer necessary as the resource type is now encoded in the resource ID (opens in a new tab).
To still be able to determine whether a given resource ID exists, a ResourceIds
table has been added.
The previous ResourceType
table was part of World
and missed tables that were registered directly via StoreCore.registerTable
instead of via World.registerTable
(e.g. when a table was registered as part of a root module).
This problem is solved by the new table ResourceIds
being part of Store
.
StoreCore
's hasTable
function was removed in favor of using ResourceIds.getExists(tableId)
directly.
- import { ResourceType } from "@latticexyz/world/src/tables/ResourceType.sol";
- import { StoreCore } from "@latticexyz/store/src/StoreCore.sol";
+ import { ResourceIds } from "@latticexyz/store/src/codegen/tables/ResourceIds.sol";
- bool tableExists = StoreCore.hasTable(tableId);
+ bool tableExists = ResourceIds.getExists(tableId);
- bool systemExists = ResourceType.get(systemId) != Resource.NONE;
+ bool systemExists = ResourceIds.getExists(systemId);
feat: rename table to tableId (#1484) (opens in a new tab) (@latticexyz/block-logs-stream, @latticexyz/store-sync, @latticexyz/store)
Renamed all occurrences of table
where it is used as "table ID" to tableId
.
This is only a breaking change for consumers who manually decode Store
events, but not for consumers who use the MUD libraries.
event StoreSetRecord(
- bytes32 table,
+ bytes32 tableId,
bytes32[] key,
bytes data
);
event StoreSetField(
- bytes32 table,
+ bytes32 tableId,
bytes32[] key,
uint8 fieldIndex,
bytes data
);
event StoreDeleteRecord(
- bytes32 table,
+ bytes32 tableId,
bytes32[] key
);
event StoreEphemeralRecord(
- bytes32 table,
+ bytes32 tableId,
bytes32[] key,
bytes data
);
docs: cli changeset after deploy changes (#1503) (opens in a new tab) (@latticexyz/cli)
Refactor deploy
command to break up logic into modules
feat: rename key to keyTuple (#1492) (opens in a new tab) (@latticexyz/block-logs-stream, @latticexyz/store-sync, @latticexyz/store, @latticexyz/world)
Renamed all occurrences of key
where it is used as "key tuple" to keyTuple
.
This is only a breaking change for consumers who manually decode Store
events, but not for consumers who use the MUD libraries.
event StoreSetRecord(
bytes32 tableId,
- bytes32[] key,
+ bytes32[] keyTuple,
bytes data
);
event StoreSetField(
bytes32 tableId,
- bytes32[] key,
+ bytes32[] keyTuple,
uint8 fieldIndex,
bytes data
);
event StoreDeleteRecord(
bytes32 tableId,
- bytes32[] key,
+ bytes32[] keyTuple,
);
event StoreEphemeralRecord(
bytes32 tableId,
- bytes32[] key,
+ bytes32[] keyTuple,
bytes data
);
fix(protocol-parser): export valueSchemaToFieldLayoutHex (#1481) (opens in a new tab) (@latticexyz/protocol-parser)
Export valueSchemaToFieldLayoutHex
helper
fix(world): register Delegations table in CoreModule (#1452) (opens in a new tab) (@latticexyz/world)
Register Delegations
table in the CoreModule
fix(store-indexer): catch errors when parsing logs to tables and storage operations (#1488) (opens in a new tab) (@latticexyz/store-sync)
Catch errors when parsing logs to tables and storage operations, log and skip
feat(store,world): use user-types for ResourceId
, FieldLayout
and Schema
in table libraries (#1586) (opens in a new tab) (@latticexyz/store-sync, @latticexyz/store, @latticexyz/world)
All Store
and World
tables now use the appropriate user-types for ResourceId
, FieldLayout
and Schema
to avoid manual wrap
/unwrap
.
feat(store): optimize storage location hash (#1509) (opens in a new tab) (@latticexyz/store, @latticexyz/world)
Optimized the StoreCore
hash function determining the data location to use less gas.
fix(dev-tools): key -> keyTuple (#1505) (opens in a new tab) (@latticexyz/dev-tools)
Updates store event key
reference to keyTuple
fix(dev-tools): table -> tableId (#1502) (opens in a new tab) (@latticexyz/dev-tools)
Updates table
reference to tableId
feat: move forge build + abi + abi-ts to out (#1483) (opens in a new tab) (@latticexyz/cli)
deploy
and dev-contracts
CLI commands now use forge build --skip test script
before deploying and run mud abi-ts
to generate strong types for ABIs.
refactor(store-indexer): add readme, refactor common env (#1533) (opens in a new tab) (@latticexyz/store-indexer)
Added README and refactored handling of common environment variables
refactor(store,world): simplify constants (#1569) (opens in a new tab) (@latticexyz/store, @latticexyz/world)
Simplified a couple internal constants used for bitshifting.
docs(faucet): add readme (#1534) (opens in a new tab) (@latticexyz/faucet)
Added README
Version 2.0.0-next.8
Major changes
feat(world,store): add ERC165 checks for all registration methods (#1458) (opens in a new tab) (@latticexyz/store, @latticexyz/world)
The World
now performs ERC165
interface checks to ensure that the StoreHook
, SystemHook
, System
, DelegationControl
and Module
contracts to actually implement their respective interfaces before registering them in the World.
The required supportsInterface
methods are implemented on the respective base contracts.
When creating one of these contracts, the recommended approach is to extend the base contract rather than the interface.
- import { IStoreHook } from "@latticexyz/store/src/IStore.sol";
+ import { StoreHook } from "@latticexyz/store/src/StoreHook.sol";
- contract MyStoreHook is IStoreHook {}
+ contract MyStoreHook is StoreHook {}
- import { ISystemHook } from "@latticexyz/world/src/interfaces/ISystemHook.sol";
+ import { SystemHook } from "@latticexyz/world/src/SystemHook.sol";
- contract MySystemHook is ISystemHook {}
+ contract MySystemHook is SystemHook {}
- import { IDelegationControl } from "@latticexyz/world/src/interfaces/IDelegationControl.sol";
+ import { DelegationControl } from "@latticexyz/world/src/DelegationControl.sol";
- contract MyDelegationControl is IDelegationControl {}
+ contract MyDelegationControl is DelegationControl {}
- import { IModule } from "@latticexyz/world/src/interfaces/IModule.sol";
+ import { Module } from "@latticexyz/world/src/Module.sol";
- contract MyModule is IModule {}
+ contract MyModule is Module {}
feat(world): change requireOwnerOrSelf to requireOwner (#1457) (opens in a new tab) (@latticexyz/world)
-
The access control library no longer allows calls by the
World
contract to itself to bypass the ownership check. This is a breaking change for root modules that relied on this mechanism to register root tables, systems or function selectors. To upgrade, root modules must usedelegatecall
instead of a regularcall
to install root tables, systems or function selectors.- world.registerSystem(rootSystemId, rootSystemAddress); + address(world).delegatecall(abi.encodeCall(world.registerSystem, (rootSystemId, rootSystemAddress)));
-
An
installRoot
method was added to theIModule
interface. This method is now called when installing a root module viaworld.installRootModule
. When installing non-root modules viaworld.installModule
, the module'sinstall
function continues to be called.
feat(world): add Balance table and BalanceTransferSystem (#1425) (opens in a new tab) (@latticexyz/world)
The World now maintains a balance per namespace. When a system is called with value, the value stored in the World contract and credited to the system's namespace.
Previously, the World contract did not store value, but passed it on to the system contracts. However, as systems are expected to be stateless (reading/writing state only via the calling World) and can be registered in multiple Worlds, this could have led to exploits.
Any address with access to a namespace can use the balance of that namespace. This allows all systems registered in the same namespace to work with the same balance.
There are two new World methods to transfer balance between namespaces (transferBalanceToNamespace
) or to an address (transferBalanceToAddress
).
interface IBaseWorld {
function transferBalanceToNamespace(bytes16 fromNamespace, bytes16 toNamespace, uint256 amount) external;
function transferBalanceToAddress(bytes16 fromNamespace, address toAddress, uint256 amount) external;
}
Minor changes
feat(store,world): add ability to unregister hooks (#1422) (opens in a new tab) (@latticexyz/store, @latticexyz/world)
It is now possible to unregister Store hooks and System hooks.
interface IStore {
function unregisterStoreHook(bytes32 table, IStoreHook hookAddress) external;
// ...
}
interface IWorld {
function unregisterSystemHook(bytes32 resourceSelector, ISystemHook hookAddress) external;
// ...
}
feat(protocol-parser): add keySchema/valueSchema helpers (#1443) (opens in a new tab) (@latticexyz/store)
Moved KeySchema
, ValueSchema
, SchemaToPrimitives
and TableRecord
types into @latticexyz/protocol-parser
feat(protocol-parser): add keySchema/valueSchema helpers (#1443) (opens in a new tab) (@latticexyz/protocol-parser)
Adds decodeKey
, decodeValue
, encodeKey
, and encodeValue
helpers to decode/encode from key/value schemas. Deprecates previous methods that use a schema object with static/dynamic field arrays, originally attempting to model our on-chain behavior but ended up not very ergonomic when working with table configs.
Version 2.0.0-next.7
Major changes
feat(store,world): more granularity for onchain hooks (#1399) (opens in a new tab) (@latticexyz/store, @latticexyz/world)
-
The
onSetRecord
hook is split intoonBeforeSetRecord
andonAfterSetRecord
and theonDeleteRecord
hook is split intoonBeforeDeleteRecord
andonAfterDeleteRecord
. The purpose of this change is to allow more fine-grained control over the point in the lifecycle at which hooks are executed.The previous hooks were executed before modifying data, so they can be replaced with the respective
onBefore
hooks.- function onSetRecord( + function onBeforeSetRecord( bytes32 table, bytes32[] memory key, bytes memory data, Schema valueSchema ) public; - function onDeleteRecord( + function onBeforeDeleteRecord( bytes32 table, bytes32[] memory key, Schema valueSchema ) public;
-
It is now possible to specify which methods of a hook contract should be called when registering a hook. The purpose of this change is to save gas by avoiding to call no-op hook methods.
function registerStoreHook( bytes32 tableId, - IStoreHook hookAddress + IStoreHook hookAddress, + uint8 enabledHooksBitmap ) public; function registerSystemHook( bytes32 systemId, - ISystemHook hookAddress + ISystemHook hookAddress, + uint8 enabledHooksBitmap ) public;
There are
StoreHookLib
andSystemHookLib
with helper functions to encode the bitmap of enabled hooks.import { StoreHookLib } from "@latticexyz/store/src/StoreHook.sol"; uint8 storeHookBitmap = StoreBookLib.encodeBitmap({ onBeforeSetRecord: true, onAfterSetRecord: true, onBeforeSetField: true, onAfterSetField: true, onBeforeDeleteRecord: true, onAfterDeleteRecord: true });
import { SystemHookLib } from "@latticexyz/world/src/SystemHook.sol"; uint8 systemHookBitmap = SystemHookLib.encodeBitmap({ onBeforeCallSystem: true, onAfterCallSystem: true });
-
The
onSetRecord
hook call foremitEphemeralRecord
has been removed to save gas and to more clearly distinguish ephemeral tables as offchain tables.
Patch changes
fix(abi-ts): remove cwd join (#1418) (opens in a new tab) (@latticexyz/abi-ts)
Let glob
handle resolving the glob against the current working directory.
feat(world): allow callFrom from own address without explicit delegation (#1407) (opens in a new tab) (@latticexyz/world)
Allow callFrom
with the own address as delegator
without requiring an explicit delegation
Version 2.0.0-next.6
Major changes
style(gas-report): rename mud-gas-report to gas-report (#1410) (opens in a new tab) (@latticexyz/gas-report)
Renames mud-gas-report
binary to gas-report
, since it's no longer MUD specific.
Minor changes
docs: rework abi-ts changesets (#1413) (opens in a new tab) (@latticexyz/abi-ts, @latticexyz/cli)
Added a new @latticexyz/abi-ts
package to generate TS type declaration files (.d.ts
) for each ABI JSON file.
This allows you to import your JSON ABI and use it directly with libraries like viem (opens in a new tab) and abitype (opens in a new tab).
pnpm add @latticexyz/abi-ts
pnpm abi-ts
By default, abi-ts
looks for files with the glob **/*.abi.json
, but you can customize this glob with the --input
argument, e.g.
pnpm abi-ts --input 'abi/IWorld.sol/IWorld.abi.json'
docs: rework abi-ts changesets (#1413) (opens in a new tab) (create-mud)
We now use @latticexyz/abi-ts
to generate TS type declaration files (.d.ts
) for each ABI JSON file. This replaces our usage TypeChain everywhere.
If you have a MUD project created from an older template, you can replace TypeChain with abi-ts
by first updating your contracts' package.json
:
-"build": "pnpm run build:mud && pnpm run build:abi && pnpm run build:typechain",
+"build": "pnpm run build:mud && pnpm run build:abi && pnpm run build:abi-ts",
-"build:abi": "forge clean && forge build",
+"build:abi": "rimraf abi && forge build --extra-output-files abi --out abi --skip test script MudTest.sol",
+"build:abi-ts": "mud abi-ts --input 'abi/IWorld.sol/IWorld.abi.json' && prettier --write '**/*.abi.json.d.ts'",
"build:mud": "mud tablegen && mud worldgen",
-"build:typechain": "rimraf types && typechain --target=ethers-v5 out/IWorld.sol/IWorld.json",
And update your client's setupNetwork.ts
with:
-import { IWorld__factory } from "contracts/types/ethers-contracts/factories/IWorld__factory";
+import IWorldAbi from "contracts/abi/IWorld.sol/IWorld.abi.json";
const worldContract = createContract({
address: networkConfig.worldAddress as Hex,
- abi: IWorld__factory.abi,
+ abi: IWorldAbi,
docs: rework abi-ts changesets (#1413) (opens in a new tab) (@latticexyz/store, @latticexyz/world)
We now use @latticexyz/abi-ts
to generate TS type declaration files (.d.ts
) for each ABI JSON file. This replaces our usage TypeChain everywhere.
If you previously relied on TypeChain types from @latticexyz/store
or @latticexyz/world
, you will either need to migrate to viem or abitype using ABI JSON imports or generate TypeChain types from our exported ABI JSON files.
import { getContract } from "viem";
import IStoreAbi from "@latticexyz/store/abi/IStore.sol/IStore.abi.json";
const storeContract = getContract({
abi: IStoreAbi,
...
});
await storeContract.write.setRecord(...);
Version 2.0.0-next.5
Major changes
refactor(world): separate call utils into WorldContextProvider
and SystemCall
(#1370) (opens in a new tab) (@latticexyz/world)
-
The previous
Call.withSender
util is replaced withWorldContextProvider
, since the usecase of appending themsg.sender
to the calldata is tightly coupled withWorldContextConsumer
(which extracts the appended context from the calldata).The previous
Call.withSender
utility reverted if the call failed and only returned the returndata on success. This is replaced withcallWithContextOrRevert
/delegatecallWithContextOrRevert
-import { Call } from "@latticexyz/world/src/Call.sol"; +import { WorldContextProvider } from "@latticexyz/world/src/WorldContext.sol"; -Call.withSender({ - delegate: false, - value: 0, - ... -}); +WorldContextProvider.callWithContextOrRevert({ + value: 0, + ... +}); -Call.withSender({ - delegate: true, - value: 0, - ... -}); +WorldContextProvider.delegatecallWithContextOrRevert({ + ... +});
In addition there are utils that return a
bool success
flag instead of reverting on errors. This mirrors the behavior of Solidity's low levelcall
/delegatecall
functions and is useful in situations where additional logic should be executed in case of a reverting external call.library WorldContextProvider { function callWithContext( address target, // Address to call bytes memory funcSelectorAndArgs, // Abi encoded function selector and arguments to pass to pass to the contract address msgSender, // Address to append to the calldata as context for msgSender uint256 value // Value to pass with the call ) internal returns (bool success, bytes memory data); function delegatecallWithContext( address target, // Address to call bytes memory funcSelectorAndArgs, // Abi encoded function selector and arguments to pass to pass to the contract address msgSender // Address to append to the calldata as context for msgSender ) internal returns (bool success, bytes memory data); }
-
WorldContext
is renamed toWorldContextConsumer
to clarify the relationship betweenWorldContextProvider
(appending context to the calldata) andWorldContextConsumer
(extracting context from the calldata)-import { WorldContext } from "@latticexyz/world/src/WorldContext.sol"; -import { WorldContextConsumer } from "@latticexyz/world/src/WorldContext.sol";
-
The
World
contract previously had a_call
method to handle calling systems via their resource selector, performing accesss control checks and call hooks registered for the system.library SystemCall { /** * Calls a system via its resource selector and perform access control checks. * Does not revert if the call fails, but returns a `success` flag along with the returndata. */ function call( address caller, bytes32 resourceSelector, bytes memory funcSelectorAndArgs, uint256 value ) internal returns (bool success, bytes memory data); /** * Calls a system via its resource selector, perform access control checks and trigger hooks registered for the system. * Does not revert if the call fails, but returns a `success` flag along with the returndata. */ function callWithHooks( address caller, bytes32 resourceSelector, bytes memory funcSelectorAndArgs, uint256 value ) internal returns (bool success, bytes memory data); /** * Calls a system via its resource selector, perform access control checks and trigger hooks registered for the system. * Reverts if the call fails. */ function callWithHooksOrRevert( address caller, bytes32 resourceSelector, bytes memory funcSelectorAndArgs, uint256 value ) internal returns (bytes memory data); }
-
System hooks now are called with the system's resource selector instead of its address. The system's address can still easily obtained within the hook via
Systems.get(resourceSelector)
if necessary.interface ISystemHook { function onBeforeCallSystem( address msgSender, - address systemAddress, + bytes32 resourceSelector, bytes memory funcSelectorAndArgs ) external; function onAfterCallSystem( address msgSender, - address systemAddress, + bytes32 resourceSelector, bytes memory funcSelectorAndArgs ) external; }
Minor changes
feat(world): add support for upgrading systems (#1378) (opens in a new tab) (@latticexyz/world)
It is now possible to upgrade systems by calling registerSystem
again with an existing system id (resource selector).
// Register a system
world.registerSystem(systemId, systemAddress, publicAccess);
// Upgrade the system by calling `registerSystem` with the
// same system id but a new system address or publicAccess flag
world.registerSystem(systemId, newSystemAddress, newPublicAccess);
feat(world): add callFrom entry point (#1364) (opens in a new tab) (@latticexyz/world)
The World
has a new callFrom
entry point which allows systems to be called on behalf of other addresses if those addresses have registered a delegation.
If there is a delegation, the call is forwarded to the system with delegator
as msgSender
.
interface IBaseWorld {
function callFrom(
address delegator,
bytes32 resourceSelector,
bytes memory funcSelectorAndArgs
) external payable virtual returns (bytes memory);
}
A delegation can be registered via the World
's registerDelegation
function.
If delegatee
is address(0)
, the delegation is considered to be a "fallback" delegation and is used in callFrom
if there is no delegation is found for the specific caller.
Otherwise the delegation is registered for the specific delegatee
.
interface IBaseWorld {
function registerDelegation(
address delegatee,
bytes32 delegationControl,
bytes memory initFuncSelectorAndArgs
) external;
}
The delegationControl
refers to the resource selector of a DelegationControl
system that must have been registered beforehand.
As part of registering the delegation, the DelegationControl
system is called with the provided initFuncSelectorAndArgs
.
This can be used to initialize data in the given DelegationControl
system.
The DelegationControl
system must implement the IDelegationControl
interface:
interface IDelegationControl {
function verify(address delegator, bytes32 systemId, bytes calldata funcSelectorAndArgs) external returns (bool);
}
When callFrom
is called, the World
checks if a delegation is registered for the given caller, and if so calls the delegation control's verify
function with the same same arguments as callFrom
.
If the call to verify
is successful and returns true
, the delegation is valid and the call is forwarded to the system with delegator
as msgSender
.
Note: if UNLIMITED_DELEGATION
(from @latticexyz/world/src/constants.sol
) is passed as delegationControl
, the external call to the delegation control contract is skipped and the delegation is considered valid.
For examples of DelegationControl
systems, check out the CallboundDelegationControl
or TimeboundDelegationControl
systems in the std-delegations
module.
See StandardDelegations.t.sol
for usage examples.
feat(world): allow transferring ownership of namespaces (#1274) (opens in a new tab) (@latticexyz/world)
It is now possible to transfer ownership of namespaces!
// Register a new namespace
world.registerNamespace("namespace");
// It's owned by the caller of the function (address(this))
// Transfer ownership of the namespace to address(42)
world.transferOwnership("namespace", address(42));
// It's now owned by address(42)
Patch changes
fix(services): correctly export typescript types (#1377) (opens in a new tab) (@latticexyz/services)
Fixed an issue where the TypeScript types for createFaucetService were not exported correctly from the @latticexyz/services package
feat: docker monorepo build (#1219) (opens in a new tab) (@latticexyz/services)
The build phase of services now works on machines with older protobuf compilers
refactor: remove v1 network package, remove snap sync module, deprecate std-client (#1311) (opens in a new tab) (@latticexyz/common, @latticexyz/store, @latticexyz/world)
- Refactor tightcoder to use typescript functions instead of ejs
- Optimize
TightCoder
library - Add
isLeftAligned
andgetLeftPaddingBits
common codegen helpers
fix(cli): make mud test exit with code 1 on test error (#1371) (opens in a new tab) (@latticexyz/cli)
The mud test
cli now exits with code 1 on test failure. It used to exit with code 0, which meant that CIs didn't notice test failures.
Version 2.0.0-next.4
Major changes
docs: changeset for deleted network package (#1348) (opens in a new tab) (@latticexyz/network)
Removes network
package. Please see the changelog (opens in a new tab) for how to migrate your app to the new store-sync
package. Or create a new project from an up-to-date template with pnpm create mud@next your-app-name
.
chore: delete std-contracts package (#1341) (opens in a new tab) (@latticexyz/cli, @latticexyz/std-contracts)
Removes std-contracts
package. These were v1 contracts, now entirely replaced by our v2 tooling. See the MUD docs (opens in a new tab) for building with v2 or create a new project from our v2 templates with pnpm create mud@next your-app-name
.
chore: delete solecs package (#1340) (opens in a new tab) (@latticexyz/cli, @latticexyz/recs, @latticexyz/solecs, @latticexyz/std-client)
Removes solecs
package. These were v1 contracts, now entirely replaced by our v2 tooling. See the MUD docs (opens in a new tab) for building with v2 or create a new project from our v2 templates with pnpm create mud@next your-app-name
.
feat(recs,std-client): move action system to recs (#1351) (opens in a new tab) (@latticexyz/recs, @latticexyz/std-client)
-
Moved
createActionSystem
fromstd-client
torecs
package and updated it to better support v2 sync stack.If you want to use
createActionSystem
alongsidesyncToRecs
, you'll need to pass in arguments like so:import { syncToRecs } from "@latticexyz/store-sync/recs"; import { createActionSystem } from "@latticexyz/recs/deprecated"; import { from, mergeMap } from "rxjs"; const { blockLogsStorage$, waitForTransaction } = syncToRecs({ world, ... }); const txReduced$ = blockLogsStorage$.pipe( mergeMap(({ operations }) => from(operations.map((op) => op.log?.transactionHash).filter(isDefined))) ); const actionSystem = createActionSystem(world, txReduced$, waitForTransaction);
-
Fixed a bug in
waitForComponentValueIn
that caused the promise to not resolve if the component value was already set when the function was called. -
Fixed a bug in
createActionSystem
that caused optimistic updates to be incorrectly propagated to requirement checks. To fix the bug, you must now pass in the full component object to the action'supdates
instead of just the component name.actions.add({ updates: () => [ { - component: "Resource", + component: Resource, ... } ], ... });
chore: delete std-client package (#1342) (opens in a new tab) (@latticexyz/std-client)
Removes std-client
package. Please see the changelog (opens in a new tab) for how to migrate your app to the new store-sync
package. Or create a new project from an up-to-date template with pnpm create mud@next your-app-name
.
chore: delete ecs-browser package (#1339) (opens in a new tab) (@latticexyz/ecs-browser)
Removes ecs-browser
package. This has now been replaced by dev-tools
, which comes out-of-the-box when creating a new MUD app from the templates (pnpm create mud@next your-app-name
). We'll be adding deeper RECS support (querying for entities) in a future release.
chore: delete store-cache package (#1343) (opens in a new tab) (@latticexyz/store-cache)
Removes store-cache
package. Please see the changelog (opens in a new tab) for how to migrate your app to the new store-sync
package. Or create a new project from an up-to-date template with pnpm create mud@next your-app-name
.
If you need reactivity, we recommend using recs
package and syncToRecs
. We'll be adding reactivity to syncToSqlite
in a future release.
chore: delete store-cache package (#1343) (opens in a new tab) (@latticexyz/react)
Removes useRow
and useRows
hooks, previously powered by store-cache
, which is now deprecated. Please use recs
and the corresponding useEntityQuery
and useComponentValue
hooks. We'll have more hooks soon for SQL.js sync backends.
Version 2.0.0-next.3
Major changes
feat(world, store): stop loading schema from storage, require schema as an argument (#1174) (opens in a new tab) (@latticexyz/cli, @latticexyz/store, @latticexyz/world, create-mud)
All Store
methods now require the table's value schema to be passed in as an argument instead of loading it from storage.
This decreases gas cost and removes circular dependencies of the Schema table (where it was not possible to write to the Schema table before the Schema table was registered).
function setRecord(
bytes32 table,
bytes32[] calldata key,
bytes calldata data,
+ Schema valueSchema
) external;
The same diff applies to getRecord
, getField
, setField
, pushToField
, popFromField
, updateInField
, and deleteRecord
.
This change only requires changes in downstream projects if the Store
methods were accessed directly. In most cases it is fully abstracted in the generated table libraries,
so downstream projects only need to regenerate their table libraries after updating MUD.
refactor(world): combine name and namespace to resource selector in World methods (#1208) (opens in a new tab) (@latticexyz/cli, @latticexyz/world)
-
All
World
function selectors that previously hadbytes16 namespace, bytes16 name
arguments now usebytes32 resourceSelector
instead. This includessetRecord
,setField
,pushToField
,popFromField
,updateInField
,deleteRecord
,call
,grantAccess
,revokeAccess
,registerTable
,registerStoreHook
,registerSystemHook
,registerFunctionSelector
,registerSystem
andregisterRootFunctionSelector
. This change aligns theWorld
function selectors with theStore
function selectors, reduces clutter, reduces gas cost and reduces theWorld
's contract size. -
The
World
'sregisterHook
function is removed. UseregisterStoreHook
orregisterSystemHook
instead. -
The
deploy
script is updated to integrate the World interface changes
refactor: remove v1 network package, remove snap sync module, deprecate std-client (#1311) (opens in a new tab) (@latticexyz/world)
The SnapSyncModule
is removed. The recommended way of loading the initial state of a MUD app is via the new store-indexer
(opens in a new tab). Loading state via contract getter functions is not recommended, as it's computationally heavy on the RPC, can't be cached, and is an easy way to shoot yourself in the foot with exploding RPC costs.
The @latticexyz/network
package was deprecated and is now removed. All consumers should upgrade to the new sync stack from @latticexyz/store-sync
.
refactor(store): optimize PackedCounter (#1231) (opens in a new tab) (@latticexyz/cli, @latticexyz/protocol-parser, @latticexyz/services, @latticexyz/store-sync, @latticexyz/store, @latticexyz/world)
Reverse PackedCounter encoding, to optimize gas for bitshifts. Ints are right-aligned, shifting using an index is straightforward if they are indexed right-to-left.
- Previous encoding: (7 bytes | accumulator),(5 bytes | counter 1),...,(5 bytes | counter 5)
- New encoding: (5 bytes | counter 5),...,(5 bytes | counter 1),(7 bytes | accumulator)
feat(store,world): combine schema and metadata registration, rename getSchema to getValueSchema, change Schema table id (#1182) (opens in a new tab) (@latticexyz/cli, @latticexyz/store, @latticexyz/world, @latticexyz/store-sync, create-mud)
-
Store
's internal schema table is now a normal table instead of using special code paths. It is renamed to Tables, and the table ID changed frommudstore:schema
tomudstore:Tables
-
Store
'sregisterSchema
andsetMetadata
are combined into a singleregisterTable
method. This means metadata (key names, field names) is immutable and indexers can create tables with this metadata when a new table is registered on-chain.- function registerSchema(bytes32 table, Schema schema, Schema keySchema) external; - - function setMetadata(bytes32 table, string calldata tableName, string[] calldata fieldNames) external; + function registerTable( + bytes32 table, + Schema keySchema, + Schema valueSchema, + string[] calldata keyNames, + string[] calldata fieldNames + ) external;
-
World
'sregisterTable
method is updated to match theStore
interface,setMetadata
is removed -
The
getSchema
method is renamed togetValueSchema
on all interfaces- function getSchema(bytes32 table) external view returns (Schema schema); + function getValueSchema(bytes32 table) external view returns (Schema valueSchema);
-
The
store-sync
andcli
packages are updated to integrate the breaking protocol changes. Downstream projects only need to manually integrate these changes if they access low levelStore
orWorld
functions. Otherwise, a fresh deploy with the latest MUD will get you these changes.
refactor: remove v1 network package, remove snap sync module, deprecate std-client (#1311) (opens in a new tab) (@latticexyz/services, create-mud)
Move createFaucetService
from @latticexyz/network
to @latticexyz/services/faucet
.
- import { createFaucetService } from "@latticexyz/network";
+ import { createFaucetService } from "@latticexyz/services/faucet";
refactor: remove v1 network package, remove snap sync module, deprecate std-client (#1311) (opens in a new tab) (@latticexyz/std-client, @latticexyz/common, create-mud)
Deprecate @latticexyz/std-client
and remove v1 network dependencies.
-
getBurnerWallet
is replaced bygetBurnerPrivateKey
from@latticexyz/common
. It now returns aHex
string instead of anrxjs
BehaviorSubject
.- import { getBurnerWallet } from "@latticexyz/std-client"; + import { getBurnerPrivateKey } from "@latticexyz/common"; - const privateKey = getBurnerWallet().value; - const privateKey = getBurnerPrivateKey();
-
All functions from
std-client
that depended on v1 network code are removed (most notablysetupMUDNetwork
andsetupMUDV2Network
). Consumers should upgrade to v2 networking code from@latticexyz/store-sync
. -
The following functions are removed from
std-client
because they are very use-case specific and depend on deprecated code:getCurrentTurn
,getTurnAtTime
,getGameConfig
,isUntraversable
,getPlayerEntity
,resolveRelationshipChain
,findEntityWithComponentInRelationshipChain
,findInRelationshipChain
. Consumers should vendor these functions if they are still needed. -
Remaining exports from
std-client
are moved to/deprecated
. The package will be removed in a future release (once there are replacements for the deprecated exports).- import { ... } from "@latticexyz/std-client"; + import { ... } from "@latticexyz/std-client/deprecated";
Patch changes
feat(common,store-sync): improve initial sync to not block returned promise (#1315) (opens in a new tab) (@latticexyz/common, @latticexyz/store-sync)
Initial sync from indexer no longer blocks the promise returning from createStoreSync
, syncToRecs
, and syncToSqlite
. This should help with rendering loading screens using the SyncProgress
RECS component and avoid the long flashes of no content in templates.
By default, syncToRecs
and syncToSqlite
will start syncing (via observable subscription) immediately after called.
If your app needs to control when syncing starts, you can use the startSync: false
option and then blockStoreOperations$.subscribe()
to start the sync yourself. Just be sure to unsubscribe to avoid memory leaks.
const { blockStorageOperations$ } = syncToRecs({
...
startSync: false,
});
// start sync manually by subscribing to `blockStorageOperation$`
const subcription = blockStorageOperation$.subscribe();
// clean up subscription
subscription.unsubscribe();
refactor(store): optimize table libraries (#1303) (opens in a new tab) (@latticexyz/store)
Optimize autogenerated table libraries
feat(store-sync): add more logging to waitForTransaction (#1317) (opens in a new tab) (@latticexyz/store-sync)
add retry attempts and more logging to waitForTransaction
refactor(store): optimize Schema (#1252) (opens in a new tab) (@latticexyz/store, @latticexyz/world)
Optimize Schema methods.
Return uint256
instead of uint8
in SchemaInstance numFields methods
Version 2.0.0-next.2
Major changes
feat(store-indexer): use fastify, move trpc to /trpc (#1232) (opens in a new tab) (@latticexyz/store-indexer)
Adds a Fastify (opens in a new tab) server in front of tRPC and puts tRPC endpoints under /trpc
to make way for other top-level endpoints (e.g. tRPC panel (opens in a new tab) or other API frontends like REST or gRPC).
If you're using @latticexyz/store-sync
packages with an indexer (either createIndexerClient
or indexerUrl
argument of syncToRecs
), then you'll want to update your indexer URL:
createIndexerClient({
- url: "https://indexer.dev.linfra.xyz",
+ url: "https://indexer.dev.linfra.xyz/trpc",
});
syncToRecs({
...
- indexerUrl: "https://indexer.dev.linfra.xyz",
+ indexerUrl: "https://indexer.dev.linfra.xyz/trpc",
});
refactor(store): remove TableId library (#1279) (opens in a new tab) (@latticexyz/store)
Remove TableId
library to simplify store
package
feat(create-mud): infer recs components from config (#1278) (opens in a new tab) (@latticexyz/cli, @latticexyz/std-client, @latticexyz/store-sync, @latticexyz/store, @latticexyz/world, create-mud)
RECS components are now dynamically created and inferred from your MUD config when using syncToRecs
.
To migrate existing projects after upgrading to this MUD version:
-
Remove
contractComponents.ts
fromclient/src/mud
-
Remove
components
argument fromsyncToRecs
-
Update
build:mud
anddev
scripts incontracts/package.json
to remove tsgen- "build:mud": "mud tablegen && mud worldgen && mud tsgen --configPath mud.config.ts --out ../client/src/mud", + "build:mud": "mud tablegen && mud worldgen",
- "dev": "pnpm mud dev-contracts --tsgenOutput ../client/src/mud", + "dev": "pnpm mud dev-contracts",
feat: bump viem to 1.6.0 (#1308) (opens in a new tab) (@latticexyz/block-logs-stream)
- removes our own
getLogs
function now that viem'sgetLogs
supports using multipleevents
per RPC call. - removes
isNonPendingBlock
andisNonPendingLog
helpers now that viem narrowsBlock
andLog
types based on inputs - simplifies
groupLogsByBlockNumber
types and tests
feat(dev-tools): use new sync stack (#1284) (opens in a new tab) (@latticexyz/dev-tools, create-mud)
MUD dev tools is updated to latest sync stack. You must now pass in all of its data requirements rather than relying on magic globals.
import { mount as mountDevTools } from "@latticexyz/dev-tools";
- mountDevTools();
+ mountDevTools({
+ config,
+ publicClient,
+ walletClient,
+ latestBlock$,
+ blockStorageOperations$,
+ worldAddress,
+ worldAbi,
+ write$,
+ // if you're using recs
+ recsWorld,
+ });
It's also advised to wrap dev tools so that it is only mounted during development mode. Here's how you do this with Vite:
// https://vitejs.dev/guide/env-and-mode.html
if (import.meta.env.DEV) {
mountDevTools({ ... });
}
Minor changes
feat(dev-tools): use new sync stack (#1284) (opens in a new tab) (@latticexyz/common)
createContract
now has an onWrite
callback so you can observe writes. This is useful for wiring up the transanction log in MUD dev tools.
import { createContract, ContractWrite } from "@latticexyz/common";
import { Subject } from "rxjs";
const write$ = new Subject<ContractWrite>();
creactContract({
...
onWrite: (write) => write$.next(write),
});
feat: bump viem to 1.6.0 (#1308) (opens in a new tab) (@latticexyz/common)
- adds
defaultPriorityFee
tomudFoundry
for better support with MUD's default anvil config and removes workaround increateContract
- improves nonce error detection using viem's custom errors
feat(store-sync,store-indexer): consolidate sync logic, add syncToSqlite (#1240) (opens in a new tab) (@latticexyz/dev-tools, @latticexyz/store-indexer, @latticexyz/store-sync)
Store sync logic is now consolidated into a createStoreSync
function exported from @latticexyz/store-sync
. This simplifies each storage sync strategy to just a simple wrapper around the storage adapter. You can now sync to RECS with syncToRecs
or SQLite with syncToSqlite
and PostgreSQL support coming soon.
There are no breaking changes if you were just using syncToRecs
from @latticexyz/store-sync
or running the sqlite-indexer
binary from @latticexyz/store-indexer
.
feat(dev-tools): use new sync stack (#1284) (opens in a new tab) (@latticexyz/react)
Adds a usePromise
hook that returns a native PromiseSettledResult
object (opens in a new tab).
const promise = fetch(url);
const result = usePromise(promise);
if (result.status === "idle" || result.status === "pending") {
return <>fetching</>;
}
if (result.status === "rejected") {
return <>error fetching: {String(result.reason)}</>;
}
if (result.status === "fulfilled") {
return <>fetch status: {result.value.status}</>;
}
Patch changes
feat: bump viem to 1.6.0 (#1308) (opens in a new tab) (@latticexyz/block-logs-stream, @latticexyz/common, @latticexyz/dev-tools, @latticexyz/network, @latticexyz/protocol-parser, @latticexyz/schema-type, @latticexyz/std-client, @latticexyz/store-indexer, @latticexyz/store-sync, create-mud)
bump viem to 1.6.0
feat(dev-tools): improve support for non-store recs components (#1302) (opens in a new tab) (@latticexyz/dev-tools, @latticexyz/store-sync)
Improves support for internal/client-only RECS components
feat: bump viem to 1.6.0 (#1308) (opens in a new tab) (@latticexyz/store-sync)
remove usages of isNonPendingBlock
and isNonPendingLog
(fixed with more specific viem types)
Version 2.0.0-next.1
Major changes
chore: fix changeset type (#1220) (opens in a new tab) (@latticexyz/store-indexer, @latticexyz/store-sync)
Adds store indexer service package with utils to query the indexer service.
You can run the indexer locally by checking out the MUD monorepo, installing/building everything, and running pnpm start:local
from packages/store-indexer
.
To query the indexer in the client, you can create a tRPC client with a URL pointing to the indexer service and call the available tRPC methods:
import { createIndexerClient } from "@latticexyz/store-sync/trpc-indexer";
const indexer = createIndexerClient({ url: indexerUrl });
const result = await indexer.findAll.query({
chainId: publicClient.chain.id,
address,
});
If you're using syncToRecs
, you can just pass in the indexerUrl
option as a shortcut to the above:
import { syncToRecs } from "@latticexyz/store-sync/recs";
syncToRecs({
...
indexerUrl: "https://your.indexer.service",
});
fix: changeset package name (#1270) (opens in a new tab) (@latticexyz/cli, @latticexyz/common, @latticexyz/recs, @latticexyz/store-indexer, create-mud)
Templates and examples now use MUD's new sync packages, all built on top of viem (opens in a new tab). This greatly speeds up and stabilizes our networking code and improves types throughout.
These new sync packages come with support for our recs
package, including encodeEntity
and decodeEntity
utilities for composite keys.
If you're using store-cache
and useRow
/useRows
, you should wait to upgrade until we have a suitable replacement for those libraries. We're working on a sql.js (opens in a new tab)-powered sync module that will replace store-cache
.
Migrate existing RECS apps to new sync packages
As you migrate, you may find some features replaced, removed, or not included by default. Please open an issue (opens in a new tab) and let us know if we missed anything.
-
Add
@latticexyz/store-sync
package to your app'sclient
package and make sureviem
is pinned to version1.3.1
(otherwise you may get type errors) -
In your
supportedChains.ts
, replacefoundry
chain with our newmudFoundry
chain.- import { foundry } from "viem/chains"; - import { MUDChain, latticeTestnet } from "@latticexyz/common/chains"; + import { MUDChain, latticeTestnet, mudFoundry } from "@latticexyz/common/chains"; - export const supportedChains: MUDChain[] = [foundry, latticeTestnet]; + export const supportedChains: MUDChain[] = [mudFoundry, latticeTestnet];
-
In
getNetworkConfig.ts
, remove the return type (to let TS infer it for now), remove now-unused config values, and add the viemchain
object.- export async function getNetworkConfig(): Promise<NetworkConfig> { + export async function getNetworkConfig() {
const initialBlockNumber = params.has("initialBlockNumber") ? Number(params.get("initialBlockNumber")) - : world?.blockNumber ?? -1; // -1 will attempt to find the block number from RPC + : world?.blockNumber ?? 0n;
+ return { + privateKey: getBurnerWallet().value, + chain, + worldAddress, + initialBlockNumber, + faucetServiceUrl: params.get("faucet") ?? chain.faucetUrl, + };
-
In
setupNetwork.ts
, replacesetupMUDV2Network
withsyncToRecs
.- import { setupMUDV2Network } from "@latticexyz/std-client"; - import { createFastTxExecutor, createFaucetService, getSnapSyncRecords } from "@latticexyz/network"; + import { createFaucetService } from "@latticexyz/network"; + import { createPublicClient, fallback, webSocket, http, createWalletClient, getContract, Hex, parseEther, ClientConfig } from "viem"; + import { encodeEntity, syncToRecs } from "@latticexyz/store-sync/recs"; + import { createBurnerAccount, createContract, transportObserver } from "@latticexyz/common";
- const result = await setupMUDV2Network({ - ... - }); + const clientOptions = { + chain: networkConfig.chain, + transport: transportObserver(fallback([webSocket(), http()])), + pollingInterval: 1000, + } as const satisfies ClientConfig; + const publicClient = createPublicClient(clientOptions); + const burnerAccount = createBurnerAccount(networkConfig.privateKey as Hex); + const burnerWalletClient = createWalletClient({ + ...clientOptions, + account: burnerAccount, + }); + const { components, latestBlock$, blockStorageOperations$, waitForTransaction } = await syncToRecs({ + world, + config: storeConfig, + address: networkConfig.worldAddress as Hex, + publicClient, + components: contractComponents, + startBlock: BigInt(networkConfig.initialBlockNumber), + indexerUrl: networkConfig.indexerUrl ?? undefined, + }); + const worldContract = createContract({ + address: networkConfig.worldAddress as Hex, + abi: IWorld__factory.abi, + publicClient, + walletClient: burnerWalletClient, + });
// Request drip from faucet - const signer = result.network.signer.get(); - if (networkConfig.faucetServiceUrl && signer) { - const address = await signer.getAddress(); + if (networkConfig.faucetServiceUrl) { + const address = burnerAccount.address;
const requestDrip = async () => { - const balance = await signer.getBalance(); + const balance = await publicClient.getBalance({ address }); console.info(`[Dev Faucet]: Player balance -> ${balance}`); - const lowBalance = balance?.lte(utils.parseEther("1")); + const lowBalance = balance < parseEther("1");
You can remove the previous ethers
worldContract
, snap sync code, and fast transaction executor.The return of
setupNetwork
is a bit different than before, so you may have to do corresponding app changes.+ return { + world, + components, + playerEntity: encodeEntity({ address: "address" }, { address: burnerWalletClient.account.address }), + publicClient, + walletClient: burnerWalletClient, + latestBlock$, + blockStorageOperations$, + waitForTransaction, + worldContract, + };
-
Update
createSystemCalls
with the new return type ofsetupNetwork
.export function createSystemCalls( - { worldSend, txReduced$, singletonEntity }: SetupNetworkResult, + { worldContract, waitForTransaction }: SetupNetworkResult, { Counter }: ClientComponents ) { const increment = async () => { - const tx = await worldSend("increment", []); - await awaitStreamValue(txReduced$, (txHash) => txHash === tx.hash); + const tx = await worldContract.write.increment(); + await waitForTransaction(tx); return getComponentValue(Counter, singletonEntity); };
-
(optional) If you still need a clock, you can create it with:
import { map, filter } from "rxjs"; import { createClock } from "@latticexyz/network"; const clock = createClock({ period: 1000, initialTime: 0, syncInterval: 5000, }); world.registerDisposer(() => clock.dispose()); latestBlock$ .pipe( map((block) => Number(block.timestamp) * 1000), // Map to timestamp in ms filter((blockTimestamp) => blockTimestamp !== clock.lastUpdateTime), // Ignore if the clock was already refreshed with this block filter((blockTimestamp) => blockTimestamp !== clock.currentTime) // Ignore if the current local timestamp is correct ) .subscribe(clock.update); // Update the local clock
If you're using the previous LoadingState
component, you'll want to migrate to the new SyncProgress
:
import { SyncStep, singletonEntity } from "@latticexyz/store-sync/recs";
const syncProgress = useComponentValue(SyncProgress, singletonEntity, {
message: "Connecting",
percentage: 0,
step: SyncStep.INITIALIZE,
});
if (syncProgress.step === SyncStep.LIVE) {
// we're live!
}
feat(common): replace TableId with tableIdToHex/hexToTableId (#1258) (opens in a new tab) (@latticexyz/cli, @latticexyz/common, @latticexyz/dev-tools, @latticexyz/network, @latticexyz/std-client, @latticexyz/store-sync)
Add tableIdToHex
and hexToTableId
pure functions and move/deprecate TableId
.
feat(common): add createContract, createNonceManager utils (#1261) (opens in a new tab) (@latticexyz/common)
Add utils for using viem with MUD
createContract
is a wrapper around viem'sgetContract
(opens in a new tab) but with better nonce handling for faster executing of transactions. It has the same arguments and return type asgetContract
.createNonceManager
helps track local nonces, used bycreateContract
.
Also renames mudTransportObserver
to transportObserver
.
Minor changes
feat(common): add viem utils (#1245) (opens in a new tab) (@latticexyz/common)
Add utils for using viem with MUD
mudFoundry
chain with a transaction request formatter that temporarily removes max fees to work better with anvil--base-fee 0
createBurnerAccount
that also temporarily removes max fees during transaction signing to work better with anvil--base-fee 0
mudTransportObserver
that will soon let MUD Dev Tools observe transactions
You can use them like:
import { createBurnerAccount, mudTransportObserver } from "@latticexyz/common";
import { mudFoundry } from "@latticexyz/common/chains";
createWalletClient({
account: createBurnerAccount(privateKey),
chain: mudFoundry,
transport: mudTransportObserver(http()),
pollingInterval: 1000,
});
feat(store-indexer,store-sync): make chain optional, configure indexer with RPC (#1234) (opens in a new tab) (@latticexyz/store-indexer, @latticexyz/store-sync)
- Accept a plain viem
PublicClient
(instead of requiring aChain
to be set) instore-sync
andstore-indexer
functions. These functions now fetch chain ID usingpublicClient.getChainId()
when nopublicClient.chain.id
is present. - Allow configuring
store-indexer
with a set of RPC URLs (RPC_HTTP_URL
andRPC_WS_URL
) instead ofCHAIN_ID
.
feat(store-sync): export singletonEntity as const, allow startBlock in syncToRecs (#1235) (opens in a new tab) (@latticexyz/store-sync)
Export singletonEntity
as const rather than within the syncToRecs
result.
- const { singletonEntity, ... } = syncToRecs({ ... });
+ import { singletonEntity, syncToRecs } from "@latticexyz/store-sync/recs";
+ const { ... } = syncToRecs({ ... });
feat(schema-type): add type narrowing isStaticAbiType (#1196) (opens in a new tab) (@latticexyz/schema-type)
add type narrowing isStaticAbiType
feat(common): move zero gas fee override to createContract
(#1266) (opens in a new tab) (@latticexyz/common)
- Moves zero gas fee override to
createContract
until https://github.com/wagmi-dev/viem/pull/963 (opens in a new tab) or similar feature lands - Skip simulation if
gas
is provided
Patch changes
fix(cli): add support for legacy transactions in deploy script (#1178) (opens in a new tab) (@latticexyz/cli)
Add support for legacy transactions in deploy script by falling back to gasPrice
if lastBaseFeePerGas
is not available
feat: protocol-parser in go (#1116) (opens in a new tab) (@latticexyz/services)
protocol-parser in Go
refactor(store): optimize Storage library (#1194) (opens in a new tab) (@latticexyz/store)
Optimize storage library
feat(common): remove need for tx queue in createContract
(#1271) (opens in a new tab) (@latticexyz/common)
- Remove need for tx queue in
createContract
feat(store-sync): add block numbers to SyncProgress (#1228) (opens in a new tab) (@latticexyz/store-sync)
Adds latestBlockNumber
and lastBlockNumberProcessed
to internal SyncProgress
component
feat(store-sync): sync to RECS (#1197) (opens in a new tab) (@latticexyz/store-sync)
Add RECS sync strategy and corresponding utils
import { createPublicClient, http } from 'viem';
import { syncToRecs } from '@latticexyz/store-sync';
import storeConfig from 'contracts/mud.config';
import { defineContractComponents } from './defineContractComponents';
const publicClient = createPublicClient({
chain,
transport: http(),
pollingInterval: 1000,
});
const { components, singletonEntity, latestBlock$, blockStorageOperations$, waitForTransaction } = await syncToRecs({
world,
config: storeConfig,
address: '0x...',
publicClient,
components: defineContractComponents(...),
});
fix(store): align Store event names between IStoreWrite and StoreCore (#1237) (opens in a new tab) (@latticexyz/store)
Align Store events parameter naming between IStoreWrite and StoreCore
fix(cli): explicit import of world as type (#1206) (opens in a new tab) (@latticexyz/cli, @latticexyz/std-client)
Generated contractComponents
now properly import World
as type
feat(store-sync): export singletonEntity as const, allow startBlock in syncToRecs (#1235) (opens in a new tab) (@latticexyz/store-sync)
Add startBlock
option to syncToRecs
.
import { syncToRecs } from "@latticexyz/store-sync/recs";
import worlds from "contracts/worlds.json";
syncToRecs({
startBlock: worlds['31337'].blockNumber,
...
});
chore: pin node to 18.16.1 (#1200) (opens in a new tab) (@latticexyz/network)
Remove devEmit function when sending network events from SyncWorker because they can't be serialized across the web worker boundary.
feat(cli,recs,std-client): update RECS components with v2 key/value schemas (#1195) (opens in a new tab) (@latticexyz/cli, @latticexyz/recs, @latticexyz/std-client)
Update RECS components with v2 key/value schemas. This helps with encoding/decoding composite keys and strong types for keys/values.
This may break if you were previously dependent on component.id
, component.metadata.componentId
, or component.metadata.tableId
:
component.id
is now the on-chainbytes32
hex representation of the table IDcomponent.metadata.componentName
is the table name (e.g.Position
)component.metadata.tableName
is the namespaced table name (e.g.myworld:Position
)component.metadata.keySchema
is an object with key names and their corresponding ABI typescomponent.metadata.valueSchema
is an object with field names and their corresponding ABI types
refactor(store): update tightcoder codegen, optimize TightCoder library (#1210) (opens in a new tab) (@latticexyz/common, @latticexyz/store, @latticexyz/world)
- Refactor tightcoder to use typescript functions instead of ejs
- Optimize
TightCoder
library - Add
isLeftAligned
andgetLeftPaddingBits
common codegen helpers
Version 2.0.0-next.0
Minor changes
feat(store-sync): add store sync package (#1075) (opens in a new tab) (@latticexyz/block-logs-stream, @latticexyz/protocol-parser, @latticexyz/store-sync, @latticexyz/store)
Add store sync package
feat(protocol-parser): add abiTypesToSchema (#1100) (opens in a new tab) (@latticexyz/protocol-parser)
feat: add abiTypesToSchema, a util to turn a list of abi types into a Schema by separating static and dynamic types
chore(protocol-parser): add changeset for #1099 (#1111) (opens in a new tab) (@latticexyz/protocol-parser)
feat: add encodeKeyTuple
, a util to encode key tuples in Typescript (equivalent to key tuple encoding in Solidity and inverse of decodeKeyTuple
).
Example:
encodeKeyTuple({ staticFields: ["uint256", "int32", "bytes16", "address", "bool", "int8"], dynamicFields: [] }, [
42n,
-42,
"0x12340000000000000000000000000000",
"0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF",
true,
3,
]);
// [
// "0x000000000000000000000000000000000000000000000000000000000000002a",
// "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd6",
// "0x1234000000000000000000000000000000000000000000000000000000000000",
// "0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff",
// "0x0000000000000000000000000000000000000000000000000000000000000001",
// "0x0000000000000000000000000000000000000000000000000000000000000003",
// ]
feat(store-sync): rework blockLogsToStorage (#1176) (opens in a new tab) (@latticexyz/block-logs-stream, @latticexyz/store-sync)
- Replace
blockEventsToStorage
withblockLogsToStorage
that exposes astoreOperations
callback to perform database writes from store operations. This helps encapsulates database adapters into a single wrapper/instance ofblockLogsToStorage
and allows for wrapping a block of store operations in a database transaction. - Add
toBlock
option togroupLogsByBlockNumber
and removeblockHash
from results. This helps track the last block number for a given set of logs when used in the context of RxJS streams.
feat(block-logs-stream): add block logs stream package (#1070) (opens in a new tab) (@latticexyz/block-logs-stream)
Add block logs stream package
import { filter, map, mergeMap } from "rxjs";
import { createPublicClient, parseAbi } from "viem";
import {
createBlockStream,
isNonPendingBlock,
groupLogsByBlockNumber,
blockRangeToLogs,
} from "@latticexyz/block-logs-stream";
const publicClient = createPublicClient({
// your viem public client config here
});
const latestBlock$ = await createBlockStream({ publicClient, blockTag: "latest" });
const latestBlockNumber$ = latestBlock$.pipe(
filter(isNonPendingBlock),
map((block) => block.number)
);
latestBlockNumber$
.pipe(
map((latestBlockNumber) => ({ startBlock: 0n, endBlock: latestBlockNumber })),
blockRangeToLogs({
publicClient,
address,
events: parseAbi([
"event StoreDeleteRecord(bytes32 table, bytes32[] key)",
"event StoreSetField(bytes32 table, bytes32[] key, uint8 schemaIndex, bytes data)",
"event StoreSetRecord(bytes32 table, bytes32[] key, bytes data)",
"event StoreEphemeralRecord(bytes32 table, bytes32[] key, bytes data)",
]),
}),
mergeMap(({ logs }) => from(groupLogsByBlockNumber(logs)))
)
.subscribe((block) => {
console.log("got events for block", block);
});
feat(gas-report): create package, move relevant files to it (#1147) (opens in a new tab) (@latticexyz/cli, @latticexyz/gas-report, @latticexyz/store)
Create gas-report package, move gas-report cli command and GasReporter contract to it
refactor(store,world): replace isStore with storeAddress (#1061) (opens in a new tab) (@latticexyz/std-contracts, @latticexyz/store, @latticexyz/world)
Rename MudV2Test
to MudTest
and move from @latticexyz/std-contracts
to @latticexyz/store
.
// old import
import { MudV2Test } from "@latticexyz/std-contracts/src/test/MudV2Test.t.sol";
// new import
import { MudTest } from "@latticexyz/store/src/MudTest.sol";
Refactor StoreSwitch
to use a storage slot instead of function isStore()
to determine which contract is Store:
- Previously
StoreSwitch
calledisStore()
onmsg.sender
to determine ifmsg.sender
is aStore
contract. If the call succeeded, theStore
methods were called onmsg.sender
, otherwise the data was written to the own storage. - With this change
StoreSwitch
instead checks for anaddress
in a known storage slot. If the address equals the own address, data is written to the own storage. If it is an external address,Store
methods are called on this address. If it is unset (address(0)
), store methods are called onmsg.sender
. - In practice this has the same effect as before: By default the
World
contracts sets its own address inStoreSwitch
, whileSystem
contracts keep the Store address undefined, soSystems
write to their caller (World
) if they are executed viacall
or directly to theWorld
storage if they are executed viadelegatecall
. - Besides gas savings, this change has two additional benefits:
- it is now possible for
Systems
to explicitly set aStore
address to make them exclusive to thatStore
and - table libraries can now be used in tests without having to provide an explicit
Store
argument, because theMudTest
base contract redirects reads and writes to the internalWorld
contract.
- it is now possible for
feat(store-sync): sync to sqlite (#1185) (opens in a new tab) (@latticexyz/store-sync)
blockLogsToStorage(sqliteStorage(...))
converts block logs to SQLite operations. You can use it like:
import { drizzle } from "drizzle-orm/better-sqlite3";
import Database from "better-sqlite3";
import { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core";
import { createPublicClient } from "viem";
import { blockLogsToStorage } from "@latticexyz/store-sync";
import { sqliteStorage } from "@latticexyz/store-sync/sqlite";
const database = drizzle(new Database('store.db')) as any as BaseSQLiteDatabase<"sync", void>;
const publicClient = createPublicClient({ ... });
blockLogs$
.pipe(
concatMap(blockLogsToStorage(sqliteStorage({ database, publicClient }))),
tap(({ blockNumber, operations }) => {
console.log("stored", operations.length, "operations for block", blockNumber);
})
)
.subscribe();
feat(common): new utils, truncate table ID parts (#1173) (opens in a new tab) (@latticexyz/common)
TableId.toHex()
now truncates name/namespace to 16 bytes each, to properly fit into a bytes32
hex string.
Also adds a few utils we'll need in the indexer:
bigIntMin
is similar toMath.min
but forbigint
sbigIntMax
is similar toMath.max
but forbigint
sbigIntSort
for sorting an array ofbigint
schunk
to split an array into chunkswait
returns aPromise
that resolves after specified number of milliseconds
feat(cli): update set-version to match new release structure, add --tag
, --commit
(#1157) (opens in a new tab) (@latticexyz/cli)
- update the
set-version
cli command to work with the new release process by adding two new options:--tag
: install the latest version of the given tag. For snapshot releases tags correspond to the branch name, commits tomain
result in an automatic snapshot release, so--tag main
is equivalent to what used to be-v canary
--commit
: install a version based on a given commit hash. Since commits frommain
result in an automatic snapshot release it works for all commits on main, and it works for manual snapshot releases from branches other than main
set-version
now updates allpackage.json
nested below the current working directory (expectnode_modules
), so no need for running it each workspace of a monorepo separately.
Example:
pnpm mud set-version --tag main && pnpm install
pnpm mud set-version --commit db19ea39 && pnpm install
Patch changes
fix(protocol-parser): properly decode empty records (#1177) (opens in a new tab) (@latticexyz/protocol-parser)
decodeRecord
now properly decodes empty records
refactor(store): clean up Memory, make mcopy pure (#1153) (opens in a new tab) (@latticexyz/cli, @latticexyz/common, @latticexyz/store, @latticexyz/world)
Clean up Memory.sol, make mcopy pure
fix(recs): improve messages for v2 components (#1167) (opens in a new tab) (@latticexyz/recs)
improve RECS error messages for v2 components
test: bump forge-std and ds-test (#1168) (opens in a new tab) (@latticexyz/cli, @latticexyz/gas-report, @latticexyz/noise, @latticexyz/schema-type, @latticexyz/solecs, @latticexyz/std-contracts, @latticexyz/store, @latticexyz/world, create-mud)
bump forge-std and ds-test dependencies
fix(schema-type): fix byte lengths for uint64/int64 (#1175) (opens in a new tab) (@latticexyz/schema-type)
Fix byte lengths for uint64
and int64
.
build: bump TS (#1165) (opens in a new tab) (@latticexyz/cli, create-mud, @latticexyz/utils, @latticexyz/world)
bump to latest TS version (5.1.6)
build: bump viem, abitype (#1179) (opens in a new tab) (@latticexyz/block-logs-stream, @latticexyz/cli, @latticexyz/common, @latticexyz/dev-tools, @latticexyz/network, @latticexyz/protocol-parser, @latticexyz/schema-type, @latticexyz/std-client, @latticexyz/store-cache, @latticexyz/store-sync, @latticexyz/store)
- bump to viem 1.3.0 and abitype 0.9.3
- move
@wagmi/chains
imports toviem/chains
- refine a few types
test(e2e): add more test cases (#1074) (opens in a new tab) (@latticexyz/services)
fix a bug related to encoding negative bigints in MODE
fix: remove devEmit when sending events from SyncWorker (#1109) (opens in a new tab) (@latticexyz/network)
Remove devEmit function when sending network events from SyncWorker because they can't be serialized across the web worker boundary.