Skip to content

Commit

Permalink
feat(std-client): add setupContracts (#154)
Browse files Browse the repository at this point in the history
* feat(std-client): add setupContracts

* chore: remove duplicate dependency
  • Loading branch information
alvrs committed Sep 20, 2022
1 parent c691c6e commit be86d24
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 2 deletions.
3 changes: 3 additions & 0 deletions packages/std-client/package.json
Expand Up @@ -23,7 +23,9 @@
"devDependencies": {
"@latticexyz/network": "^0.13.0",
"@latticexyz/recs": "^0.13.0",
"@latticexyz/solecs": "0.13.0",
"@latticexyz/utils": "^0.13.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.1.3",
"@rollup/plugin-typescript": "^8.3.1",
"@types/jest": "^27.4.1",
Expand Down Expand Up @@ -53,6 +55,7 @@
"@latticexyz/network": "^0.7.0",
"@latticexyz/recs": "^0.7.0",
"@latticexyz/utils": "^0.7.0",
"@latticexyz/solecs": "0.13.0",
"ethers": "^5.6.6",
"lodash": "^4.17.21",
"mobx": "^6.4.2",
Expand Down
3 changes: 2 additions & 1 deletion packages/std-client/rollup.config.js
Expand Up @@ -2,6 +2,7 @@ import typescript from "@rollup/plugin-typescript";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import commonjs from "rollup-plugin-commonjs";
import peerDepsExternal from "rollup-plugin-peer-deps-external";
import pluginJson from "@rollup/plugin-json"

import { defineConfig } from "rollup";

Expand All @@ -12,5 +13,5 @@ export default defineConfig({
dir: "dist",
sourcemap: true,
},
plugins: [nodeResolve(), typescript(), commonjs(), peerDepsExternal()],
plugins: [nodeResolve(), typescript(), commonjs(), peerDepsExternal(), pluginJson()],
});
1 change: 1 addition & 0 deletions packages/std-client/src/index.ts
Expand Up @@ -2,3 +2,4 @@ export * from "./components";
export * from "./utils";
export * from "./hooks";
export * from "./systems";
export * from "./setup";
1 change: 1 addition & 0 deletions packages/std-client/src/setup/index.ts
@@ -0,0 +1 @@
export { setupContracts } from "./setupContracts";
185 changes: 185 additions & 0 deletions packages/std-client/src/setup/setupContracts.ts
@@ -0,0 +1,185 @@
import {
createNetwork,
createContracts,
Mappings,
createTxQueue,
createSyncWorker,
createEncoder,
NetworkComponentUpdate,
createSystemExecutor,
NetworkConfig,
SyncWorkerConfig,
} from "@latticexyz/network";
import { bufferTime, filter, Observable, Subject } from "rxjs";
import {
Component,
Components,
EntityIndex,
getComponentEntities,
getComponentValueStrict,
removeComponent,
Schema,
setComponent,
Type,
World,
} from "@latticexyz/recs";
import { computed, IComputedValue } from "mobx";
import { keccak256, stretch, toEthAddress } from "@latticexyz/utils";
import ComponentAbi from "@latticexyz/solecs/abi/Component.json";
import { Contract, ContractInterface, Signer } from "ethers";
import { Component as SolecsComponent } from "@latticexyz/solecs";
import { JsonRpcProvider } from "@ethersproject/providers";
import { World as WorldContract } from "@latticexyz/solecs/types/ethers-contracts/World";
import { abi as WorldAbi } from "@latticexyz/solecs/abi/World.json";
import { defineStringComponent } from "../components";

export type SetupContractConfig = NetworkConfig &
Omit<SyncWorkerConfig, "worldContract" | "mappings"> & { worldAddress: string; devMode?: boolean };

export type ContractComponents = {
[key: string]: Component<Schema, { contractId: string }>;
};

export async function setupContracts<C extends ContractComponents, SystemTypes extends { [key: string]: Contract }>(
networkConfig: SetupContractConfig,
world: World,
components: C,
SystemAbis: { [key in keyof SystemTypes]: ContractInterface }
) {
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(networkConfig);
world.registerDisposer(network.dispose);

console.log(
"initial block",
networkConfig.initialBlockNumber,
await network.providers.get().json.getBlock(networkConfig.initialBlockNumber)
);

const signerOrProvider = computed(() => network.signer.get() || network.providers.get().json);

const { contracts, config: contractsConfig } = await createContracts<{ World: WorldContract }>({
config: { World: { abi: WorldAbi, address: networkConfig.worldAddress } },
signerOrProvider,
});

const { txQueue, dispose: disposeTxQueue } = createTxQueue(contracts, network, { devMode: networkConfig.devMode });
world.registerDisposer(disposeTxQueue);

const systems = createSystemExecutor<SystemTypes>(world, network, SystemsRegistry, SystemAbis, {
devMode: networkConfig.devMode,
});

// Create sync worker
const { ecsEvent$, config$, dispose } = createSyncWorker<C>();
world.registerDisposer(dispose);
function startSync() {
config$.next({
provider: networkConfig.provider,
worldContract: contractsConfig.World,
initialBlockNumber: networkConfig.initialBlockNumber ?? 0,
chainId: networkConfig.chainId,
disableCache: networkConfig.devMode, // Disable cache on hardhat
checkpointServiceUrl: networkConfig.checkpointServiceUrl,
});
}

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$
.pipe(
// We throttle the client side event processing to 1000 events every 16ms, so 62.500 events per second.
// This means if the chain were to emit more than 62.500 events per second, the client would not keep up.
// The only time we get close to this number is when initializing from a checkpoint/cache.
bufferTime(16, null, 1000),
filter((updates) => updates.length > 0),
stretch(16)
)
.subscribe((updates) => {
// Running this in a mobx action would result in only one system update per frame (should increase performance)
// but it currently breaks defineUpdateAction (https://linear.app/latticexyz/issue/LAT-594/defineupdatequery-does-not-work-when-running-multiple-component)
for (const update of updates) {
const entityIndex = world.entityToIndex.get(update.entity) ?? world.registerEntity({ id: update.entity });
const componentKey = mappings[update.component];
if (!componentKey) return console.warn("Unknown component:", update);

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

world.registerDisposer(() => ecsEventSub?.unsubscribe());
return { txReduced$: txReduced$.asObservable() };
}
9 changes: 8 additions & 1 deletion yarn.lock
Expand Up @@ -3995,6 +3995,13 @@
magic-string "^0.25.7"
resolve "^1.17.0"

"@rollup/plugin-json@^4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-4.1.0.tgz#54e09867ae6963c593844d8bd7a9c718294496f3"
integrity sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==
dependencies:
"@rollup/pluginutils" "^3.0.8"

"@rollup/plugin-node-resolve@^11.2.1":
version "11.2.1"
resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz#82aa59397a29cd4e13248b106e6a4a1880362a60"
Expand Down Expand Up @@ -4035,7 +4042,7 @@
"@rollup/pluginutils" "^3.1.0"
resolve "^1.17.0"

"@rollup/pluginutils@^3.1.0":
"@rollup/pluginutils@^3.0.8", "@rollup/pluginutils@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b"
integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==
Expand Down

0 comments on commit be86d24

Please sign in to comment.