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
19 changes: 16 additions & 3 deletions examples/minimal/packages/client-react/src/index.tsx
Original file line number Diff line number Diff line change
@@ -2,18 +2,31 @@ 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$,
});
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
@@ -6,7 +6,8 @@ import { defineContractComponents } from "./contractComponents";
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>>;

@@ -28,6 +29,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,
@@ -70,11 +80,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
@@ -5,6 +5,7 @@ import {
Chain,
GetContractParameters,
GetContractReturnType,
Hex,
PublicClient,
SimulateContractParameters,
Transport,
@@ -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,
@@ -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,
@@ -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,
@@ -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?
@@ -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 () => {
@@ -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;
};
},
}
2 changes: 2 additions & 0 deletions packages/dev-tools/package.json
Original file line number Diff line number Diff line change
@@ -26,6 +26,8 @@
"@latticexyz/network": "workspace:*",
"@latticexyz/react": "workspace:*",
"@latticexyz/std-client": "workspace:*",
"@latticexyz/store": "workspace:*",
"@latticexyz/store-sync": "workspace:*",
"@latticexyz/utils": "workspace:*",
"@latticexyz/world": "workspace:*",
"abitype": "0.9.3",
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;
};
4 changes: 2 additions & 2 deletions packages/dev-tools/src/RootPage.tsx
Original file line number Diff line number Diff line change
@@ -30,14 +30,14 @@ export function RootPage() {
>
Store log
</NavButton>
<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>
</NavButton> */}
</div>
<div className="flex-1 overflow-auto">
<Outlet />
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");
@@ -13,7 +14,7 @@ export function ActionsPage() {
containerRef.current?.scrollIntoView({ behavior: scrollBehaviorRef.current, block: "end" });
}
scrollBehaviorRef.current = "smooth";
}, [transactions]);
}, [writes]);

return (
<div
@@ -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…</>
)}
Loading