@mud-classic/network
v0.0.6
Published
This package includes general low level utilities to interact with Ethereum contracts, as well as specialized MUD contract/client sync utilities.
Downloads
23
Readme
Network
This package includes general low level utilities to interact with Ethereum contracts, as well as specialized MUD contract/client sync utilities.
The general utilities can be used without MUD. For the specialized but much more powerful utilities, usage of solecs
is required and recs
is recommended.
See mud.dev/network for the detailed API documentation.
Example
This is a real-world example from an upcoming game built with MUD.
// setupContracts.ts
import {
createNetwork,
createContracts,
Mappings,
createTxQueue,
createSyncWorker,
createEncoder,
NetworkComponentUpdate,
createSystemExecutor,
} from "@mud-classic/network";
import { Component as SolecsComponent, World as WorldContract } from "@mud-classic/solecs";
import { abi as WorldAbi } from "@mud-classic/solecs/abi/World.json";
import ComponentAbi from "@mud-classic/solecs/abi/Component.json";
import {
Component,
Components,
EntityIndex,
getComponentEntities,
getComponentValueStrict,
removeComponent,
Schema,
setComponent,
Type,
World,
} from "@mud-classic/recs";
import { keccak256, stretch, toEthAddress } from "@mud-classic/utils";
import { defineStringComponent } from "@mud-classic/std-client";
import { bufferTime, filter, Observable, Subject } from "rxjs";
import { computed, IComputedValue } from "mobx";
import { Contract, ethers, Signer, Wallet } from "ethers";
import { JsonRpcProvider } from "@ethersproject/providers";
import { SystemTypes } from "<contracts>/types/SystemTypes";
import { SystemAbis } from "<contracts>/types/SystemAbis.mjs";
import { config } from "./config";
export type ContractComponents = {
[key: string]: Component<Schema, { contractId: string }>;
};
export async function setupContracts<C extends ContractComponents>(world: World, components: C) {
const SystemsRegistry = defineStringComponent(world, {
id: "SystemsRegistry",
metadata: { contractId: "world.component.systems" },
});
const ComponentsRegistry = defineStringComponent(world, {
id: "ComponentsRegistry",
metadata: { contractId: "world.component.components" },
});
components = {
...components,
SystemsRegistry,
ComponentsRegistry,
};
const mappings: Mappings<C> = {};
for (const key of Object.keys(components)) {
const { contractId } = components[key].metadata;
mappings[keccak256(contractId)] = key;
}
const network = await createNetwork(config);
const signerOrProvider = computed(() => network.signer.get() || network.providers.get().json);
const { contracts, config: contractsConfig } = await createContracts<{ World: WorldContract }>({
config: { World: { abi: WorldAbi, address: config.worldAddress } },
signerOrProvider,
});
const { txQueue, dispose: disposeTxQueue } = createTxQueue(contracts, network);
const systems = createSystemExecutor<SystemTypes>(world, network, SystemsRegistry, SystemAbis, {
devMode: config.devMode,
});
const { ecsEvent$, config$, dispose } = createSyncWorker<C>();
function startSync() {
config$.next({
provider: networkConfig.provider,
worldContract: contractsConfig.World,
initialBlockNumber: config.initialBlockNumber ?? 0,
chainId: config.chainId,
disableCache: false,
snapshotServiceUrl: networkConfig.snapshotServiceUrl,
streamServiceUrl: networkConfig.streamServiceUrl,
});
}
const { txReduced$ } = applyNetworkUpdates(world, components, ecsEvent$, mappings);
const encoders = createEncoders(world, ComponentsRegistry, signerOrProvider);
return { txQueue, txReduced$, encoders, network, startSync, systems };
}
async function createEncoders(
world: World,
components: Component<{ value: Type.String }>,
signerOrProvider: IComputedValue<JsonRpcProvider | Signer>
) {
const encoders = {} as Record<string, ReturnType<typeof createEncoder>>;
async function fetchAndCreateEncoder(entity: EntityIndex) {
const componentAddress = toEthAddress(world.entities[entity]);
const componentId = getComponentValueStrict(components, entity).value;
const componentContract = new Contract(
componentAddress,
ComponentAbi.abi,
signerOrProvider.get()
) as SolecsComponent;
const [componentSchemaPropNames, componentSchemaTypes] = await componentContract.getSchema();
encoders[componentId] = createEncoder(componentSchemaPropNames, componentSchemaTypes);
}
// Initial setup
for (const entity of getComponentEntities(components)) fetchAndCreateEncoder(entity);
// Keep up to date
const subscription = components.update$.subscribe((update) => fetchAndCreateEncoder(update.entity));
world.registerDisposer(() => subscription?.unsubscribe());
return encoders;
}
/**
* Sets up synchronization between contract components and client components
*/
function applyNetworkUpdates<C extends Components>(
world: World,
components: C,
ecsEvent$: Observable<NetworkComponentUpdate<C>>,
mappings: Mappings<C>
) {
const txReduced$ = new Subject<string>();
const ecsEventSub = ecsEvent$.subscribe((update) => {
const entityIndex = world.entityToIndex.get(update.entity) ?? world.registerEntity({ id: update.entity });
const componentKey = mappings[update.component];
if (update.value === undefined) {
// undefined value means component removed
removeComponent(components[componentKey] as Component<Schema>, entityIndex);
} else {
setComponent(components[componentKey] as Component<Schema>, entityIndex, update.value);
}
if (update.lastEventInTx) txReduced$.next(update.txHash);
});
return { txReduced$: txReduced$.asObservable() };
}