Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(std-client): add setupContracts #154

Merged
merged 3 commits into from
Sep 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/std-client/package.json
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { setupContracts } from "./setupContracts";
185 changes: 185 additions & 0 deletions packages/std-client/src/setup/setupContracts.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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