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

refactor(common): move createContract's internal write logic to writeContract #1693

Merged
merged 8 commits into from
Oct 3, 2023
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
15 changes: 15 additions & 0 deletions .changeset/rotten-beers-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@latticexyz/common": minor
---

- Moves contract write logic out of `createContract` into its own `writeContract` method so that it can be used outside of the contract instance, and for consistency with viem.
- Deprecates `createContract` in favor of `getContract` for consistency with viem.
- Reworks `createNonceManager`'s `BroadcastChannel` setup and moves out the notion of a "nonce manager ID" to `getNonceManagerId` so we can create an internal cache with `getNonceManager` for use in `writeContract`.

If you were using the `createNonceManager` before, you'll just need to rename `publicClient` argument to `client`:

```diff
const publicClient = createPublicClient({ ... });
- const nonceManager = createNonceManager({ publicClient, ... });
+ const nonceManager = createNonceManager({ client: publicClient, ... });
```
4 changes: 2 additions & 2 deletions e2e/packages/client-vanilla/src/mud/setupNetwork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { encodeEntity, syncToRecs } from "@latticexyz/store-sync/recs";
import { getNetworkConfig } from "./getNetworkConfig";
import { world } from "./world";
import IWorldAbi from "contracts/out/IWorld.sol/IWorld.abi.json";
import { createBurnerAccount, createContract, transportObserver } from "@latticexyz/common";
import { createBurnerAccount, getContract, transportObserver } from "@latticexyz/common";
import mudConfig from "contracts/mud.config";

export type SetupNetworkResult = Awaited<ReturnType<typeof setupNetwork>>;
Expand All @@ -26,7 +26,7 @@ export async function setupNetwork() {
account: burnerAccount,
});

const worldContract = createContract({
const worldContract = getContract({
address: networkConfig.worldAddress as Hex,
abi: IWorldAbi,
publicClient,
Expand Down
4 changes: 2 additions & 2 deletions e2e/packages/test-data/generate-test-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
numberToHex,
} from "viem";
import { mudFoundry } from "@latticexyz/common/chains";
import { createContract } from "@latticexyz/common";
import { getContract } from "@latticexyz/common";
import { storeEventsAbi } from "@latticexyz/store";
import { privateKeyToAccount } from "viem/accounts";
import IWorldAbi from "../contracts/out/IWorld.sol/IWorld.abi.json";
Expand Down Expand Up @@ -63,7 +63,7 @@ const walletClient = createWalletClient({
account,
});

const worldContract = createContract({
const worldContract = getContract({
address: worldAddress,
abi: IWorldAbi,
publicClient,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { encodeEntity, syncToRecs } from "@latticexyz/store-sync/recs";
import { getNetworkConfig } from "./getNetworkConfig";
import { world } from "./world";
import IWorldAbi from "contracts/out/IWorld.sol/IWorld.abi.json";
import { createBurnerAccount, createContract, transportObserver, ContractWrite } from "@latticexyz/common";
import { createBurnerAccount, getContract, transportObserver, ContractWrite } from "@latticexyz/common";
import { Subject, share } from "rxjs";
import mudConfig from "contracts/mud.config";

Expand All @@ -28,7 +28,7 @@ export async function setupNetwork() {
});

const write$ = new Subject<ContractWrite>();
const worldContract = createContract({
const worldContract = getContract({
address: networkConfig.worldAddress as Hex,
abi: IWorldAbi,
publicClient,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { encodeEntity, syncToRecs } from "@latticexyz/store-sync/recs";
import { getNetworkConfig } from "./getNetworkConfig";
import { world } from "./world";
import IWorldAbi from "contracts/out/IWorld.sol/IWorld.abi.json";
import { ContractWrite, createBurnerAccount, createContract, transportObserver } from "@latticexyz/common";
import { ContractWrite, createBurnerAccount, getContract, transportObserver } from "@latticexyz/common";
import { Subject, share } from "rxjs";
import mudConfig from "contracts/mud.config";
import { createClient as createFaucetClient } from "@latticexyz/faucet";
Expand All @@ -29,7 +29,7 @@ export async function setupNetwork() {
});

const write$ = new Subject<ContractWrite>();
const worldContract = createContract({
const worldContract = getContract({
address: networkConfig.worldAddress as Hex,
abi: IWorldAbi,
publicClient,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { encodeEntity, syncToRecs } from "@latticexyz/store-sync/recs";
import { getNetworkConfig } from "./getNetworkConfig";
import { world } from "./world";
import IWorldAbi from "contracts/out/IWorld.sol/IWorld.abi.json";
import { createBurnerAccount, createContract, transportObserver, ContractWrite } from "@latticexyz/common";
import { createBurnerAccount, getContract, transportObserver, ContractWrite } from "@latticexyz/common";
import { Subject, share } from "rxjs";
import mudConfig from "contracts/mud.config";

Expand All @@ -28,7 +28,7 @@ export async function setupNetwork() {
});

const write$ = new Subject<ContractWrite>();
const worldContract = createContract({
const worldContract = getContract({
address: networkConfig.worldAddress as Hex,
abi: IWorldAbi,
publicClient,
Expand Down
175 changes: 3 additions & 172 deletions packages/common/src/createContract.ts
Original file line number Diff line number Diff line change
@@ -1,173 +1,4 @@
import {
Abi,
Account,
Address,
Chain,
GetContractParameters,
GetContractReturnType,
Hex,
PublicClient,
SimulateContractParameters,
Transport,
WalletClient,
WriteContractParameters,
getContract,
} from "viem";
import pRetry from "p-retry";
import { createNonceManager } from "./createNonceManager";
import { debug as parentDebug } from "./debug";
import { UnionOmit } from "./type-utils/common";
import { getContract } from "./getContract";

const debug = parentDebug.extend("createContract");

// copied from viem because this isn't exported
// TODO: import from viem?
function getFunctionParameters(values: [args?: readonly unknown[], options?: object]): {
args: readonly unknown[];
options: object;
} {
const hasArgs = values.length && Array.isArray(values[0]);
const args = hasArgs ? values[0]! : [];
const options = (hasArgs ? values[1] : values[0]) ?? {};
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,
TAbi extends Abi,
TChain extends Chain,
TAccount extends Account,
TPublicClient extends PublicClient<TTransport, TChain>,
TWalletClient extends WalletClient<TTransport, TChain, TAccount>
>({
abi,
address,
publicClient,
walletClient,
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,
address,
publicClient,
walletClient,
}) as unknown as GetContractReturnType<Abi, PublicClient, WalletClient>;

if (contract.write) {
let nextWriteId = 0;
const nonceManager = createNonceManager({
publicClient: publicClient as PublicClient,
address: walletClient.account.address,
});

// Replace write calls with our own proxy. Implemented ~the same as viem, but adds better handling of nonces (via queue + retries).
contract.write = new Proxy(
{},
{
get(_, functionName: string): GetContractReturnType<Abi, PublicClient, WalletClient>["write"][string] {
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> {
const preparedWrite = await prepareWrite(options);

return await pRetry(
async () => {
if (!nonceManager.hasNonce()) {
await nonceManager.resetNonce();
}

const nonce = nonceManager.nextNonce();
debug("calling write function with nonce", nonce, preparedWrite);
return await walletClient.writeContract({
nonce,
...preparedWrite,
});
},
{
retries: 3,
onFailedAttempt: async (error) => {
// On nonce errors, reset the nonce and retry
if (nonceManager.shouldResetNonce(error)) {
debug("got nonce error, retrying", error);
await nonceManager.resetNonce();
return;
}
// TODO: prepareWrite again if there are gas errors?
throw error;
},
}
);
}

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

return contract as unknown as GetContractReturnType<TAbi, TPublicClient, TWalletClient, TAddress>;
}
/** @deprecated use `getContract` instead */
export const createContract = getContract;
Copy link
Member Author

@holic holic Oct 3, 2023

Choose a reason for hiding this comment

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

Slightly different approach to what we've been doing in the past to avoid a breaking change. We can just remove this in a future version as a breaking change, potentially batching with other breaking changes.

I kinda like the deprecate-then-break pattern so playing with it here.

49 changes: 24 additions & 25 deletions packages/common/src/createNonceManager.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,45 @@
import {
BaseError,
BlockTag,
Hex,
NonceTooHighError,
NonceTooLowError,
PublicClient,
TransactionExecutionError,
} from "viem";
import { BaseError, BlockTag, Client, Hex, NonceTooHighError, NonceTooLowError } from "viem";
import { debug as parentDebug } from "./debug";
import { getNonceManagerId } from "./getNonceManagerId";
import { getTransactionCount } from "viem/actions";

const debug = parentDebug.extend("createNonceManager");

type CreateNonceManagerOptions = {
publicClient: PublicClient;
export type CreateNonceManagerOptions = {
client: Client;
address: Hex;
blockTag?: BlockTag;
broadcastChannelName?: string;
};

type CreateNonceManagerResult = {
export type CreateNonceManagerResult = {
hasNonce: () => boolean;
nextNonce: () => number;
resetNonce: () => Promise<void>;
shouldResetNonce: (error: unknown) => boolean;
};

export function createNonceManager({
publicClient,
client,
address,
blockTag,
blockTag = "latest",
broadcastChannelName,
}: CreateNonceManagerOptions): CreateNonceManagerResult {
const nonceRef = { current: -1 };
const channel =
typeof BroadcastChannel !== "undefined"
? // TODO: fetch chain ID or require it via types?
new BroadcastChannel(`mud:createNonceManager:${publicClient.chain?.id}:${address}`)
: null;
let channel: BroadcastChannel | null = null;

if (channel) {
channel.addEventListener("message", (event) => {
const nonce = JSON.parse(event.data);
debug("got nonce from broadcast channel", nonce);
nonceRef.current = nonce;
if (typeof BroadcastChannel !== "undefined") {
const channelName = broadcastChannelName
? Promise.resolve(broadcastChannelName)
: getNonceManagerId({ client, address, blockTag });
channelName.then((name) => {
channel = new BroadcastChannel(name);
// TODO: emit some sort of "connected" event so other channels can broadcast current nonce
channel.addEventListener("message", (event) => {
const nonce = JSON.parse(event.data);
debug("got nonce from broadcast channel", nonce);
nonceRef.current = nonce;
});
});
}

Expand All @@ -56,7 +55,7 @@ export function createNonceManager({
}

async function resetNonce(): Promise<void> {
const nonce = await publicClient.getTransactionCount({ address, blockTag });
const nonce = await getTransactionCount(client, { address, blockTag });
nonceRef.current = nonce;
channel?.postMessage(JSON.stringify(nonceRef.current));
debug("reset nonce to", nonceRef.current);
Expand Down
Loading