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(dev-tools): use new sync stack #1284

Merged
merged 15 commits into from
Aug 14, 2023
Merged
20 changes: 17 additions & 3 deletions examples/minimal/packages/client-react/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,32 @@ import ReactDOM from "react-dom/client";
import { App } from "./App";
import { setup } from "./mud/setup";
import { MUDProvider } from "./MUDContext";
import { mount as mountDevTools } from "@latticexyz/dev-tools";
import storeConfig from "contracts/mud.config";

const rootElement = document.getElementById("react-root");
if (!rootElement) throw new Error("React root not found");
const root = ReactDOM.createRoot(rootElement);

// TODO: figure out if we actually want this to be async or if we should render something else in the meantime
setup().then((result) => {
setup().then(async (result) => {
root.render(
<MUDProvider value={result}>
<App />
</MUDProvider>
);
mountDevTools();

if (import.meta.env.DEV) {
const { mount: mountDevTools } = await import("@latticexyz/dev-tools");
mountDevTools({
config: storeConfig,
publicClient: result.network.publicClient,
walletClient: result.network.walletClient,
latestBlock$: result.network.latestBlock$,
blockStorageOperations$: result.network.blockStorageOperations$,
worldAddress: result.network.worldContract.address,
worldAbi: result.network.worldContract.abi,
write$: result.network.write$,
recsWorld: result.network.world,
});
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alvrs thoughts on this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks fine, maybe add a comment linking to https://vitejs.dev/guide/env-and-mode.html near import.meta.env.DEV?

}
});
20 changes: 13 additions & 7 deletions examples/minimal/packages/client-react/src/mud/setupNetwork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { getNetworkConfig } from "./getNetworkConfig";
import { world } from "./world";
import { IWorld__factory } from "contracts/types/ethers-contracts/factories/IWorld__factory";
import storeConfig from "contracts/mud.config";
import { createBurnerAccount, createContract, transportObserver } from "@latticexyz/common";
import { ContractWrite, createBurnerAccount, createContract, transportObserver } from "@latticexyz/common";
import { Subject, share } from "rxjs";

export type SetupNetworkResult = Awaited<ReturnType<typeof setupNetwork>>;

Expand All @@ -26,6 +27,15 @@ export async function setupNetwork() {
account: burnerAccount,
});

const write$ = new Subject<ContractWrite>();
const worldContract = createContract({
address: networkConfig.worldAddress as Hex,
abi: IWorld__factory.abi,
publicClient,
walletClient: burnerWalletClient,
onWrite: (write) => write$.next(write),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wondering if we wanna export something like write$ from createContract rather than the onWrite callback where user land has to convert it to an observable

downside is that common will need rxjs as a dep, which is quite heavy

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i like the lightweight onWrite callback

});

const { components, latestBlock$, blockStorageOperations$, waitForTransaction } = await syncToRecs({
world,
config: storeConfig,
Expand Down Expand Up @@ -67,11 +77,7 @@ export async function setupNetwork() {
latestBlock$,
blockStorageOperations$,
waitForTransaction,
worldContract: createContract({
address: networkConfig.worldAddress as Hex,
abi: IWorld__factory.abi,
publicClient,
walletClient: burnerWalletClient,
}),
worldContract,
write$: write$.asObservable().pipe(share()),
};
}
108 changes: 71 additions & 37 deletions packages/common/src/createContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Chain,
GetContractParameters,
GetContractReturnType,
Hex,
PublicClient,
SimulateContractParameters,
Transport,
Expand All @@ -31,6 +32,24 @@ function getFunctionParameters(values: [args?: readonly unknown[], options?: obj
return { args, options };
}

export type ContractWrite = {
id: string;
request: WriteContractParameters;
result: Promise<Hex>;
};

export type CreateContractOptions<
TTransport extends Transport,
TAddress extends Address,
TAbi extends Abi,
TChain extends Chain,
TAccount extends Account,
TPublicClient extends PublicClient<TTransport, TChain>,
TWalletClient extends WalletClient<TTransport, TChain, TAccount>
> = Required<GetContractParameters<TTransport, TChain, TAccount, TAbi, TPublicClient, TWalletClient, TAddress>> & {
onWrite?: (write: ContractWrite) => void;
};

export function createContract<
TTransport extends Transport,
TAddress extends Address,
Expand All @@ -44,8 +63,15 @@ export function createContract<
address,
publicClient,
walletClient,
}: Required<
GetContractParameters<TTransport, TChain, TAccount, TAbi, TPublicClient, TWalletClient, TAddress>
onWrite,
}: CreateContractOptions<
TTransport,
TAddress,
TAbi,
TChain,
TAccount,
TPublicClient,
TWalletClient
>): GetContractReturnType<TAbi, TPublicClient, TWalletClient, TAddress> {
const contract = getContract<TTransport, TAddress, TAbi, TChain, TAccount, TPublicClient, TWalletClient>({
abi,
Expand All @@ -55,6 +81,7 @@ export function createContract<
}) as unknown as GetContractReturnType<Abi, PublicClient, WalletClient>;

if (contract.write) {
let nextWriteId = 0;
const nonceManager = createNonceManager({
publicClient: publicClient as PublicClient,
address: walletClient.account.address,
Expand All @@ -65,14 +92,24 @@ export function createContract<
{},
{
get(_, functionName: string): GetContractReturnType<Abi, PublicClient, WalletClient>["write"][string] {
return async (...parameters) => {
const { args, options } = <
{
args: unknown[];
options: UnionOmit<WriteContractParameters, "abi" | "address" | "functionName" | "args">;
}
>getFunctionParameters(parameters as any);
async function prepareWrite(
options: WriteContractParameters
): Promise<WriteContractParameters<TAbi, typeof functionName, TChain, TAccount>> {
if (options.gas) {
debug("gas provided, skipping simulate", functionName, options);
return options as unknown as WriteContractParameters<TAbi, typeof functionName, TChain, TAccount>;
}

debug("simulating write", functionName, options);
const { request } = await publicClient.simulateContract({
...options,
account: options.account ?? walletClient.account,
} as unknown as SimulateContractParameters<TAbi, typeof functionName, TChain>);

return request as unknown as WriteContractParameters<TAbi, typeof functionName, TChain, TAccount>;
}

async function write(options: WriteContractParameters): Promise<Hex> {
// Temporarily override base fee for our default anvil config
// TODO: replace with https://github.com/wagmi-dev/viem/pull/1006 once merged
// TODO: more specific mud foundry check? or can we safely assume anvil+mud will be block fee zero for now?
Expand All @@ -86,34 +123,7 @@ export function createContract<
options.maxPriorityFeePerGas = 0n;
}

async function prepareWrite(): Promise<
WriteContractParameters<TAbi, typeof functionName, TChain, TAccount>
> {
if (options.gas) {
debug("gas provided, skipping simulate", functionName, args, options);
return {
address,
abi,
functionName,
args,
...options,
} as unknown as WriteContractParameters<TAbi, typeof functionName, TChain, TAccount>;
}

debug("simulating write", functionName, args, options);
const { request } = await publicClient.simulateContract({
address,
abi,
functionName,
args,
...options,
account: options.account ?? walletClient.account,
} as unknown as SimulateContractParameters<TAbi, typeof functionName, TChain>);

return request as unknown as WriteContractParameters<TAbi, typeof functionName, TChain, TAccount>;
}

const preparedWrite = await prepareWrite();
const preparedWrite = await prepareWrite(options);

return await pRetry(
async () => {
Expand Down Expand Up @@ -142,6 +152,30 @@ export function createContract<
},
}
);
}

return (...parameters) => {
const id = `${walletClient.chain.id}:${walletClient.account.address}:${nextWriteId++}`;
const { args, options } = <
{
args: unknown[];
options: UnionOmit<WriteContractParameters, "address" | "abi" | "functionName" | "args">;
}
>getFunctionParameters(parameters as any);

const request = {
address,
abi,
functionName,
args,
...options,
};

const result = write(request);

onWrite?.({ id, request, result });

return result;
};
},
}
Expand Down
3 changes: 3 additions & 0 deletions packages/dev-tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
"@latticexyz/common": "workspace:*",
"@latticexyz/network": "workspace:*",
"@latticexyz/react": "workspace:*",
"@latticexyz/recs": "workspace:*",
"@latticexyz/std-client": "workspace:*",
"@latticexyz/store": "workspace:*",
"@latticexyz/store-sync": "workspace:*",
"@latticexyz/utils": "workspace:*",
"@latticexyz/world": "workspace:*",
"abitype": "0.9.3",
Expand Down
56 changes: 56 additions & 0 deletions packages/dev-tools/src/DevToolsContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { createContext, ReactNode, useContext, useEffect, useState } from "react";
import { DevToolsOptions } from "./common";
import { ContractWrite } from "@latticexyz/common";
import { StorageOperation } from "@latticexyz/store-sync";
import { StoreConfig } from "@latticexyz/store";

type DevToolsContextValue = DevToolsOptions & {
writes: ContractWrite[];
storageOperations: StorageOperation<StoreConfig>[];
};

const DevToolsContext = createContext<DevToolsContextValue | null>(null);

type Props = {
children: ReactNode;
value: DevToolsOptions;
};

export const DevToolsProvider = ({ children, value }: Props) => {
const currentValue = useContext(DevToolsContext);
if (currentValue) throw new Error("DevToolsProvider can only be used once");

const [writes, setWrites] = useState<ContractWrite[]>([]);
useEffect(() => {
const sub = value.write$.subscribe((write) => {
setWrites((val) => [...val, write]);
});
return () => sub.unsubscribe();
}, [value.write$]);

const [storageOperations, setStorageOperations] = useState<StorageOperation<StoreConfig>[]>([]);
useEffect(() => {
const sub = value.blockStorageOperations$.subscribe(({ operations }) => {
setStorageOperations((val) => [...val, ...operations]);
});
return () => sub.unsubscribe();
}, [value.blockStorageOperations$]);

return (
<DevToolsContext.Provider
value={{
...value,
writes,
storageOperations,
}}
>
{children}
</DevToolsContext.Provider>
);
};

export const useDevToolsContext = () => {
const value = useContext(DevToolsContext);
if (!value) throw new Error("Must be used within a DevToolsProvider");
return value;
};
20 changes: 12 additions & 8 deletions packages/dev-tools/src/RootPage.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { twMerge } from "tailwind-merge";
import { Outlet } from "react-router-dom";
import { NavButton } from "./NavButton";
import { useDevToolsContext } from "./DevToolsContext";

export function RootPage() {
const { recsWorld } = useDevToolsContext();
return (
<>
<div className="flex-none bg-slate-900 text-white/60 font-medium">
Expand Down Expand Up @@ -30,14 +32,16 @@ export function RootPage() {
>
Store log
</NavButton>
<NavButton
to="/tables"
className={({ isActive }) =>
twMerge("py-1.5 px-3", isActive ? "bg-slate-800 text-white" : "hover:bg-blue-800 hover:text-white")
}
>
Store data
</NavButton>
{recsWorld ? (
<NavButton
to="/components"
className={({ isActive }) =>
twMerge("py-1.5 px-3", isActive ? "bg-slate-800 text-white" : "hover:bg-blue-800 hover:text-white")
}
>
Components
</NavButton>
) : null}
</div>
<div className="flex-1 overflow-auto">
<Outlet />
Expand Down
13 changes: 7 additions & 6 deletions packages/dev-tools/src/actions/ActionsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useRef, useEffect } from "react";
import { useStore } from "../useStore";
import { TransactionSummary } from "./TransactionSummary";
import { WriteSummary } from "./WriteSummary";
import { useDevToolsContext } from "../DevToolsContext";

export function ActionsPage() {
const transactions = useStore((state) => state.transactions);
const { writes } = useDevToolsContext();

const containerRef = useRef<HTMLDivElement>(null);
const hoveredRef = useRef(false);
const scrollBehaviorRef = useRef<ScrollBehavior>("auto");
Expand All @@ -13,7 +14,7 @@ export function ActionsPage() {
containerRef.current?.scrollIntoView({ behavior: scrollBehaviorRef.current, block: "end" });
}
scrollBehaviorRef.current = "smooth";
}, [transactions]);
}, [writes]);

return (
<div
Expand All @@ -26,8 +27,8 @@ export function ActionsPage() {
hoveredRef.current = false;
}}
>
{transactions.length ? (
transactions.map((hash) => <TransactionSummary key={hash} hash={hash} />)
{writes.length ? (
writes.map((write) => <WriteSummary key={write.id} write={write} />)
) : (
<>Waiting for transactions…</>
)}
Expand Down
Loading