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.