diff --git a/.changeset/moody-shirts-carry.md b/.changeset/moody-shirts-carry.md new file mode 100644 index 0000000000..c6f1943f1b --- /dev/null +++ b/.changeset/moody-shirts-carry.md @@ -0,0 +1,25 @@ +--- +"@latticexyz/common": minor +"create-mud": minor +--- + +Added viem custom client actions that work the same as MUD's now-deprecated `getContract`, `writeContract`, and `sendTransaction` wrappers. Templates have been updated to reflect the new patterns. + +You can migrate your own code like this: + +```diff +-import { createWalletClient } from "viem"; +-import { getContract, writeContract, sendTransaction } from "@latticexyz/common"; ++import { createWalletClient, getContract } from "viem"; ++import { transactionQueue, writeObserver } from "@latticexyz/common/actions"; + +-const walletClient = createWalletClient(...); ++const walletClient = createWalletClient(...) ++ .extend(transactionQueue()) ++ .extend(writeObserver({ onWrite }); + + const worldContract = getContract({ + client: { publicClient, walletClient }, +- onWrite, + }); +``` diff --git a/packages/common/package.json b/packages/common/package.json index 851a3659ef..47b0beb187 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -11,6 +11,7 @@ "type": "module", "exports": { ".": "./dist/index.js", + "./actions": "./dist/actions.js", "./chains": "./dist/chains.js", "./codegen": "./dist/codegen.js", "./errors": "./dist/errors.js", @@ -23,6 +24,9 @@ "index": [ "./src/index.ts" ], + "actions": [ + "./src/actions/index.ts" + ], "chains": [ "./src/chains/index.ts" ], diff --git a/packages/common/src/actions/index.ts b/packages/common/src/actions/index.ts new file mode 100644 index 0000000000..79e6f9f176 --- /dev/null +++ b/packages/common/src/actions/index.ts @@ -0,0 +1,2 @@ +export * from "./transactionQueue"; +export * from "./writeObserver"; diff --git a/packages/common/src/actions/transactionQueue.ts b/packages/common/src/actions/transactionQueue.ts new file mode 100644 index 0000000000..dfa8adbb6a --- /dev/null +++ b/packages/common/src/actions/transactionQueue.ts @@ -0,0 +1,14 @@ +import type { Transport, Chain, Account, WalletActions, WalletClient } from "viem"; +import { writeContract as mud_writeContract } from "../writeContract"; +import { sendTransaction as mud_sendTransaction } from "../sendTransaction"; + +export function transactionQueue(): ( + client: WalletClient, +) => Pick, "writeContract" | "sendTransaction"> { + return (client) => ({ + // Applies to: `client.writeContract`, `getContract(client, ...).write` + writeContract: (args) => mud_writeContract(client, args), + // Applies to: `client.sendTransaction` + sendTransaction: (args) => mud_sendTransaction(client, args), + }); +} diff --git a/packages/common/src/actions/writeObserver.ts b/packages/common/src/actions/writeObserver.ts new file mode 100644 index 0000000000..1a1fc10cf7 --- /dev/null +++ b/packages/common/src/actions/writeObserver.ts @@ -0,0 +1,34 @@ +import type { + WriteContractParameters, + Transport, + Chain, + Account, + WalletActions, + WalletClient, + WriteContractReturnType, +} from "viem"; +import { getAction } from "viem/utils"; +import { writeContract } from "viem/actions"; +import { type ContractWrite } from "../getContract"; + +type WriteObserverParameters = { onWrite: (write: ContractWrite) => void }; + +export function writeObserver({ + onWrite, +}: WriteObserverParameters): ( + client: WalletClient, +) => Pick, "writeContract"> { + let nextWriteId = 0; + + return (client) => ({ + // Applies to: `client.writeContract`, `getContract(client, ...).write` + writeContract: (args): Promise => { + const result = getAction(client, writeContract, "writeContract")(args); + + const id = `${client.chain.id}:${client.account.address}:${nextWriteId++}`; + onWrite({ id, request: args as WriteContractParameters, result }); + + return result; + }, + }); +} diff --git a/packages/common/src/getContract.ts b/packages/common/src/getContract.ts index 863eec55a2..4f67ad9cae 100644 --- a/packages/common/src/getContract.ts +++ b/packages/common/src/getContract.ts @@ -56,6 +56,7 @@ export type GetContractOptions< // TODO: migrate away from this approach once we can hook into viem: https://github.com/wagmi-dev/viem/discussions/1230 +/** @deprecated Use `walletClient.extend(transactionQueue()).extend(writeObserver({ onWrite }))` and viem's `getContract` instead. */ export function getContract< TTransport extends Transport, TAddress extends Address, diff --git a/packages/common/src/sendTransaction.ts b/packages/common/src/sendTransaction.ts index 7d08930f57..b450f5c8d2 100644 --- a/packages/common/src/sendTransaction.ts +++ b/packages/common/src/sendTransaction.ts @@ -17,6 +17,7 @@ const debug = parentDebug.extend("sendTransaction"); // TODO: migrate away from this approach once we can hook into viem's nonce management: https://github.com/wagmi-dev/viem/discussions/1230 +/** @deprecated Use `walletClient.extend(transactionQueue())` instead. */ export async function sendTransaction< TChain extends Chain | undefined, TAccount extends Account | undefined, diff --git a/packages/common/src/writeContract.ts b/packages/common/src/writeContract.ts index 5818f18db4..5ce34e5c24 100644 --- a/packages/common/src/writeContract.ts +++ b/packages/common/src/writeContract.ts @@ -20,6 +20,7 @@ const debug = parentDebug.extend("writeContract"); // TODO: migrate away from this approach once we can hook into viem's nonce management: https://github.com/wagmi-dev/viem/discussions/1230 +/** @deprecated Use `walletClient.extend(transactionQueue())` instead. */ export async function writeContract< chain extends Chain | undefined, account extends Account | undefined, diff --git a/packages/common/tsup.config.ts b/packages/common/tsup.config.ts index ba8ccf3917..0a6d6d0491 100644 --- a/packages/common/tsup.config.ts +++ b/packages/common/tsup.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from "tsup"; export default defineConfig({ entry: { index: "src/index.ts", + actions: "src/actions/index.ts", chains: "src/chains/index.ts", codegen: "src/codegen/index.ts", errors: "src/errors/index.ts", diff --git a/templates/phaser/packages/client/src/mud/setupNetwork.ts b/templates/phaser/packages/client/src/mud/setupNetwork.ts index 573f059d38..dc58cfb1e5 100644 --- a/templates/phaser/packages/client/src/mud/setupNetwork.ts +++ b/templates/phaser/packages/client/src/mud/setupNetwork.ts @@ -3,13 +3,24 @@ * (https://viem.sh/docs/getting-started.html). * This line imports the functions we need from it. */ -import { createPublicClient, fallback, webSocket, http, createWalletClient, Hex, parseEther, ClientConfig } from "viem"; +import { + createPublicClient, + fallback, + webSocket, + http, + createWalletClient, + Hex, + parseEther, + ClientConfig, + getContract, +} from "viem"; import { createFaucetService } from "@latticexyz/services/faucet"; 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, getContract, transportObserver, ContractWrite } from "@latticexyz/common"; +import { createBurnerAccount, transportObserver, ContractWrite } from "@latticexyz/common"; +import { transactionQueue, writeObserver } from "@latticexyz/common/actions"; import { Subject, share } from "rxjs"; /* @@ -39,6 +50,12 @@ export async function setupNetwork() { const publicClient = createPublicClient(clientOptions); + /* + * Create an observable for contract writes that we can + * pass into MUD dev tools for transaction observability. + */ + const write$ = new Subject(); + /* * Create a temporary wallet and a viem client for it * (see https://viem.sh/docs/clients/wallet.html). @@ -47,13 +64,9 @@ export async function setupNetwork() { const burnerWalletClient = createWalletClient({ ...clientOptions, account: burnerAccount, - }); - - /* - * Create an observable for contract writes that we can - * pass into MUD dev tools for transaction observability. - */ - const write$ = new Subject(); + }) + .extend(transactionQueue()) + .extend(writeObserver({ onWrite: (write) => write$.next(write) })); /* * Create an object for communicating with the deployed World. @@ -62,7 +75,6 @@ export async function setupNetwork() { address: networkConfig.worldAddress as Hex, abi: IWorldAbi, client: { public: publicClient, wallet: burnerWalletClient }, - onWrite: (write) => write$.next(write), }); /* diff --git a/templates/react-ecs/packages/client/src/mud/setupNetwork.ts b/templates/react-ecs/packages/client/src/mud/setupNetwork.ts index 60dfc7bdda..0d1c6ef75b 100644 --- a/templates/react-ecs/packages/client/src/mud/setupNetwork.ts +++ b/templates/react-ecs/packages/client/src/mud/setupNetwork.ts @@ -3,14 +3,25 @@ * (https://viem.sh/docs/getting-started.html). * This line imports the functions we need from it. */ -import { createPublicClient, fallback, webSocket, http, createWalletClient, Hex, parseEther, ClientConfig } from "viem"; +import { + createPublicClient, + fallback, + webSocket, + http, + createWalletClient, + Hex, + parseEther, + ClientConfig, + getContract, +} from "viem"; import { createFaucetService } from "@latticexyz/services/faucet"; 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, getContract, transportObserver, ContractWrite } from "@latticexyz/common"; +import { createBurnerAccount, transportObserver, ContractWrite } from "@latticexyz/common"; +import { transactionQueue, writeObserver } from "@latticexyz/common/actions"; import { Subject, share } from "rxjs"; @@ -41,6 +52,12 @@ export async function setupNetwork() { const publicClient = createPublicClient(clientOptions); + /* + * Create an observable for contract writes that we can + * pass into MUD dev tools for transaction observability. + */ + const write$ = new Subject(); + /* * Create a temporary wallet and a viem client for it * (see https://viem.sh/docs/clients/wallet.html). @@ -49,13 +66,9 @@ export async function setupNetwork() { const burnerWalletClient = createWalletClient({ ...clientOptions, account: burnerAccount, - }); - - /* - * Create an observable for contract writes that we can - * pass into MUD dev tools for transaction observability. - */ - const write$ = new Subject(); + }) + .extend(transactionQueue()) + .extend(writeObserver({ onWrite: (write) => write$.next(write) })); /* * Create an object for communicating with the deployed World. @@ -64,7 +77,6 @@ export async function setupNetwork() { address: networkConfig.worldAddress as Hex, abi: IWorldAbi, client: { public: publicClient, wallet: burnerWalletClient }, - onWrite: (write) => write$.next(write), }); /* diff --git a/templates/react/packages/client/src/mud/setupNetwork.ts b/templates/react/packages/client/src/mud/setupNetwork.ts index f401de4702..c9b54e4094 100644 --- a/templates/react/packages/client/src/mud/setupNetwork.ts +++ b/templates/react/packages/client/src/mud/setupNetwork.ts @@ -3,12 +3,23 @@ * (https://viem.sh/docs/getting-started.html). * This line imports the functions we need from it. */ -import { createPublicClient, fallback, webSocket, http, createWalletClient, Hex, parseEther, ClientConfig } from "viem"; +import { + createPublicClient, + fallback, + webSocket, + http, + createWalletClient, + Hex, + parseEther, + ClientConfig, + getContract, +} from "viem"; import { createFaucetService } from "@latticexyz/services/faucet"; import { syncToZustand } from "@latticexyz/store-sync/zustand"; import { getNetworkConfig } from "./getNetworkConfig"; import IWorldAbi from "contracts/out/IWorld.sol/IWorld.abi.json"; -import { createBurnerAccount, getContract, transportObserver, ContractWrite } from "@latticexyz/common"; +import { createBurnerAccount, transportObserver, ContractWrite } from "@latticexyz/common"; +import { transactionQueue, writeObserver } from "@latticexyz/common/actions"; import { Subject, share } from "rxjs"; /* @@ -38,6 +49,12 @@ export async function setupNetwork() { const publicClient = createPublicClient(clientOptions); + /* + * Create an observable for contract writes that we can + * pass into MUD dev tools for transaction observability. + */ + const write$ = new Subject(); + /* * Create a temporary wallet and a viem client for it * (see https://viem.sh/docs/clients/wallet.html). @@ -46,13 +63,9 @@ export async function setupNetwork() { const burnerWalletClient = createWalletClient({ ...clientOptions, account: burnerAccount, - }); - - /* - * Create an observable for contract writes that we can - * pass into MUD dev tools for transaction observability. - */ - const write$ = new Subject(); + }) + .extend(transactionQueue()) + .extend(writeObserver({ onWrite: (write) => write$.next(write) })); /* * Create an object for communicating with the deployed World. @@ -61,7 +74,6 @@ export async function setupNetwork() { address: networkConfig.worldAddress as Hex, abi: IWorldAbi, client: { public: publicClient, wallet: burnerWalletClient }, - onWrite: (write) => write$.next(write), }); /* diff --git a/templates/threejs/packages/client/src/mud/setupNetwork.ts b/templates/threejs/packages/client/src/mud/setupNetwork.ts index 20a639ceca..1d98c384ac 100644 --- a/templates/threejs/packages/client/src/mud/setupNetwork.ts +++ b/templates/threejs/packages/client/src/mud/setupNetwork.ts @@ -3,13 +3,24 @@ * (https://viem.sh/docs/getting-started.html). * This line imports the functions we need from it. */ -import { createPublicClient, fallback, webSocket, http, createWalletClient, Hex, parseEther, ClientConfig } from "viem"; +import { + createPublicClient, + fallback, + webSocket, + http, + createWalletClient, + Hex, + parseEther, + ClientConfig, + getContract, +} from "viem"; import { createFaucetService } from "@latticexyz/services/faucet"; 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, getContract, transportObserver, ContractWrite } from "@latticexyz/common"; +import { createBurnerAccount, transportObserver, ContractWrite } from "@latticexyz/common"; +import { transactionQueue, writeObserver } from "@latticexyz/common/actions"; import { Subject, share } from "rxjs"; /* @@ -39,6 +50,12 @@ export async function setupNetwork() { const publicClient = createPublicClient(clientOptions); + /* + * Create an observable for contract writes that we can + * pass into MUD dev tools for transaction observability. + */ + const write$ = new Subject(); + /* * Create a temporary wallet and a viem client for it * (see https://viem.sh/docs/clients/wallet.html). @@ -47,13 +64,9 @@ export async function setupNetwork() { const burnerWalletClient = createWalletClient({ ...clientOptions, account: burnerAccount, - }); - - /* - * Create an observable for contract writes that we can - * pass into MUD dev tools for transaction observability. - */ - const write$ = new Subject(); + }) + .extend(transactionQueue()) + .extend(writeObserver({ onWrite: (write) => write$.next(write) })); /* * Create an object for communicating with the deployed World. @@ -62,7 +75,6 @@ export async function setupNetwork() { address: networkConfig.worldAddress as Hex, abi: IWorldAbi, client: { public: publicClient, wallet: burnerWalletClient }, - onWrite: (write) => write$.next(write), }); /* diff --git a/templates/vanilla/packages/client/src/mud/setupNetwork.ts b/templates/vanilla/packages/client/src/mud/setupNetwork.ts index 60dfc7bdda..0d1c6ef75b 100644 --- a/templates/vanilla/packages/client/src/mud/setupNetwork.ts +++ b/templates/vanilla/packages/client/src/mud/setupNetwork.ts @@ -3,14 +3,25 @@ * (https://viem.sh/docs/getting-started.html). * This line imports the functions we need from it. */ -import { createPublicClient, fallback, webSocket, http, createWalletClient, Hex, parseEther, ClientConfig } from "viem"; +import { + createPublicClient, + fallback, + webSocket, + http, + createWalletClient, + Hex, + parseEther, + ClientConfig, + getContract, +} from "viem"; import { createFaucetService } from "@latticexyz/services/faucet"; 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, getContract, transportObserver, ContractWrite } from "@latticexyz/common"; +import { createBurnerAccount, transportObserver, ContractWrite } from "@latticexyz/common"; +import { transactionQueue, writeObserver } from "@latticexyz/common/actions"; import { Subject, share } from "rxjs"; @@ -41,6 +52,12 @@ export async function setupNetwork() { const publicClient = createPublicClient(clientOptions); + /* + * Create an observable for contract writes that we can + * pass into MUD dev tools for transaction observability. + */ + const write$ = new Subject(); + /* * Create a temporary wallet and a viem client for it * (see https://viem.sh/docs/clients/wallet.html). @@ -49,13 +66,9 @@ export async function setupNetwork() { const burnerWalletClient = createWalletClient({ ...clientOptions, account: burnerAccount, - }); - - /* - * Create an observable for contract writes that we can - * pass into MUD dev tools for transaction observability. - */ - const write$ = new Subject(); + }) + .extend(transactionQueue()) + .extend(writeObserver({ onWrite: (write) => write$.next(write) })); /* * Create an object for communicating with the deployed World. @@ -64,7 +77,6 @@ export async function setupNetwork() { address: networkConfig.worldAddress as Hex, abi: IWorldAbi, client: { public: publicClient, wallet: burnerWalletClient }, - onWrite: (write) => write$.next(write), }); /*