From d2e4d0fbbc011e64e593b0dd784cb8f2d0da7522 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Tue, 2 Apr 2024 02:27:27 +0100 Subject: [PATCH] feat(common,world): improvements for smart accounts (#2578) Co-authored-by: mous <97399882+mouseless-eth@users.noreply.github.com> Co-authored-by: alvrs Co-authored-by: karooolis --- .changeset/smooth-ducks-sell.md | 9 ++++++++ .changeset/smooth-laws-float.md | 5 +++++ .../common/src/actions/transactionQueue.ts | 18 ++++++++++----- packages/common/src/actions/writeObserver.ts | 4 ++-- packages/common/src/createBurnerAccount.ts | 4 ++-- packages/common/src/getContract.ts | 8 +++++-- packages/common/src/sendTransaction.ts | 22 ++++++++++--------- packages/common/src/writeContract.ts | 22 +++++++++++++------ .../src/actions/getTransactionResult.ts | 5 ++++- packages/world/ts/actions/callFrom.ts | 13 +++++++---- 10 files changed, 76 insertions(+), 34 deletions(-) create mode 100644 .changeset/smooth-ducks-sell.md create mode 100644 .changeset/smooth-laws-float.md diff --git a/.changeset/smooth-ducks-sell.md b/.changeset/smooth-ducks-sell.md new file mode 100644 index 0000000000..4bc67eb934 --- /dev/null +++ b/.changeset/smooth-ducks-sell.md @@ -0,0 +1,9 @@ +--- +"@latticexyz/common": patch +--- + +`transactionQueue` decorator now accepts an optional `publicClient` argument, which will be used in place of the extended viem client for making public action calls (`getChainId`, `getTransactionCount`, `simulateContract`, `call`). This helps in cases where the extended viem client is a smart account client, like in [permissionless.js](https://github.com/pimlicolabs/permissionless.js), where the transport is the bundler, not an RPC. + +`writeObserver` decorator now accepts any `Client`, not just a `WalletClient`. + +`createBurnerAccount` now returns a `PrivateKeyAccount`, the more specific `Account` type. diff --git a/.changeset/smooth-laws-float.md b/.changeset/smooth-laws-float.md new file mode 100644 index 0000000000..d909c7f003 --- /dev/null +++ b/.changeset/smooth-laws-float.md @@ -0,0 +1,5 @@ +--- +"@latticexyz/world": patch +--- + +`callFrom` decorator now accepts any `Client`, not just a `WalletClient`. It also no longer attempts to wrap/redirect calls to `call`, `callFrom`, and `registerDelegationWithSignature`. diff --git a/packages/common/src/actions/transactionQueue.ts b/packages/common/src/actions/transactionQueue.ts index dfa8adbb6a..76b0c58b69 100644 --- a/packages/common/src/actions/transactionQueue.ts +++ b/packages/common/src/actions/transactionQueue.ts @@ -1,14 +1,20 @@ -import type { Transport, Chain, Account, WalletActions, WalletClient } from "viem"; +import type { Transport, Chain, Account, WalletActions, Client, PublicClient } from "viem"; import { writeContract as mud_writeContract } from "../writeContract"; import { sendTransaction as mud_sendTransaction } from "../sendTransaction"; -export function transactionQueue(): ( - client: WalletClient, -) => Pick, "writeContract" | "sendTransaction"> { +export type TransactionQueueOptions = { + publicClient?: PublicClient; +}; + +export function transactionQueue({ + publicClient, +}: TransactionQueueOptions = {}): ( + client: Client, +) => Pick, "writeContract" | "sendTransaction"> { return (client) => ({ // Applies to: `client.writeContract`, `getContract(client, ...).write` - writeContract: (args) => mud_writeContract(client, args), + writeContract: (args) => mud_writeContract(client, args, publicClient), // Applies to: `client.sendTransaction` - sendTransaction: (args) => mud_sendTransaction(client, args), + sendTransaction: (args) => mud_sendTransaction(client, args, publicClient), }); } diff --git a/packages/common/src/actions/writeObserver.ts b/packages/common/src/actions/writeObserver.ts index 1a1fc10cf7..b24f7d8132 100644 --- a/packages/common/src/actions/writeObserver.ts +++ b/packages/common/src/actions/writeObserver.ts @@ -4,8 +4,8 @@ import type { Chain, Account, WalletActions, - WalletClient, WriteContractReturnType, + Client, } from "viem"; import { getAction } from "viem/utils"; import { writeContract } from "viem/actions"; @@ -16,7 +16,7 @@ type WriteObserverParameters = { onWrite: (write: ContractWrite) => void }; export function writeObserver({ onWrite, }: WriteObserverParameters): ( - client: WalletClient, + client: Client, ) => Pick, "writeContract"> { let nextWriteId = 0; diff --git a/packages/common/src/createBurnerAccount.ts b/packages/common/src/createBurnerAccount.ts index 8c0effda9d..59b332f1dd 100644 --- a/packages/common/src/createBurnerAccount.ts +++ b/packages/common/src/createBurnerAccount.ts @@ -1,7 +1,7 @@ -import { Account, Hex } from "viem"; +import { Hex, PrivateKeyAccount } from "viem"; import { privateKeyToAccount } from "viem/accounts"; -export function createBurnerAccount(privateKey: Hex): Account { +export function createBurnerAccount(privateKey: Hex): PrivateKeyAccount { const account = privateKeyToAccount(privateKey); // We may override account features here return { diff --git a/packages/common/src/getContract.ts b/packages/common/src/getContract.ts index 4f67ad9cae..ab0972dcfa 100644 --- a/packages/common/src/getContract.ts +++ b/packages/common/src/getContract.ts @@ -118,10 +118,14 @@ export function getContract< TChain, TAccount >; - const result = writeContract(walletClient, request); + const result = writeContract(walletClient, request, publicClient); const id = `${walletClient.chain.id}:${walletClient.account.address}:${nextWriteId++}`; - onWrite?.({ id, request: request as WriteContractParameters, result }); + onWrite?.({ + id, + request: request as WriteContractParameters, + result, + }); return result; }; diff --git a/packages/common/src/sendTransaction.ts b/packages/common/src/sendTransaction.ts index e9748ddc0a..316cbc052e 100644 --- a/packages/common/src/sendTransaction.ts +++ b/packages/common/src/sendTransaction.ts @@ -6,6 +6,7 @@ import { SendTransactionParameters, Transport, SendTransactionReturnType, + PublicClient, } from "viem"; import { call, sendTransaction as viem_sendTransaction } from "viem/actions"; import pRetry from "p-retry"; @@ -19,12 +20,13 @@ const debug = parentDebug.extend("sendTransaction"); /** @deprecated Use `walletClient.extend(transactionQueue())` instead. */ export async function sendTransaction< - TChain extends Chain | undefined, - TAccount extends Account | undefined, - TChainOverride extends Chain | undefined, + chain extends Chain | undefined, + account extends Account | undefined, + chainOverride extends Chain | undefined, >( - client: Client, - request: SendTransactionParameters, + client: Client, + request: SendTransactionParameters, + publicClient?: PublicClient, ): Promise { const rawAccount = request.account ?? client.account; if (!rawAccount) { @@ -34,23 +36,23 @@ export async function sendTransaction< const account = parseAccount(rawAccount); const nonceManager = await getNonceManager({ - client, + client: publicClient ?? client, address: account.address, blockTag: "pending", }); - async function prepare(): Promise> { + async function prepare(): Promise> { if (request.gas) { debug("gas provided, skipping simulate", request.to); return request; } debug("simulating tx to", request.to); - await call(client, { + await call(publicClient ?? client, { ...request, blockTag: "pending", account, - } as CallParameters); + } as CallParameters); return request; } @@ -67,7 +69,7 @@ export async function sendTransaction< const nonce = nonceManager.nextNonce(); debug("sending tx with nonce", nonce, "to", preparedRequest.to); - const parameters: SendTransactionParameters = { nonce, ...preparedRequest }; + const parameters: SendTransactionParameters = { nonce, ...preparedRequest }; return await viem_sendTransaction(client, parameters); }, { diff --git a/packages/common/src/writeContract.ts b/packages/common/src/writeContract.ts index 99bce2b5e0..874f1d30a6 100644 --- a/packages/common/src/writeContract.ts +++ b/packages/common/src/writeContract.ts @@ -9,6 +9,7 @@ import { WriteContractReturnType, ContractFunctionName, ContractFunctionArgs, + PublicClient, } from "viem"; import { simulateContract, writeContract as viem_writeContract } from "viem/actions"; import pRetry from "p-retry"; @@ -31,6 +32,7 @@ export async function writeContract< >( client: Client, request: WriteContractParameters, + publicClient?: PublicClient, ): Promise { const rawAccount = request.account ?? client.account; if (!rawAccount) { @@ -40,7 +42,7 @@ export async function writeContract< const account = parseAccount(rawAccount); const nonceManager = await getNonceManager({ - client, + client: publicClient ?? client, address: account.address, blockTag: "pending", }); @@ -54,11 +56,14 @@ export async function writeContract< } debug("simulating", request.functionName, "at", request.address); - const result = await simulateContract(client, { - ...request, - blockTag: "pending", - account, - } as unknown as SimulateContractParameters); + const result = await simulateContract( + publicClient ?? client, + { + ...request, + blockTag: "pending", + account, + } as unknown as SimulateContractParameters, + ); return result.request as unknown as WriteContractParameters; } @@ -75,7 +80,10 @@ export async function writeContract< const nonce = nonceManager.nextNonce(); debug("calling", preparedWrite.functionName, "with nonce", nonce, "at", preparedWrite.address); - return await viem_writeContract(client, { nonce, ...preparedWrite } as typeof preparedWrite); + return await viem_writeContract(client, { + nonce, + ...preparedWrite, + } as typeof preparedWrite); }, { retries: 3, diff --git a/packages/dev-tools/src/actions/getTransactionResult.ts b/packages/dev-tools/src/actions/getTransactionResult.ts index 380f027b36..c7f3d4a5cf 100644 --- a/packages/dev-tools/src/actions/getTransactionResult.ts +++ b/packages/dev-tools/src/actions/getTransactionResult.ts @@ -19,7 +19,10 @@ export function getTransactionResult( const transaction = getTransaction(publicClient, write); const transactionReceipt = getTransactionReceipt(publicClient, write); cache[write.id] = Promise.all([transaction, transactionReceipt]).then(([tx, receipt]) => { - const { functionName, args } = decodeFunctionData({ abi: worldAbi, data: tx.input }); + const { functionName, args } = decodeFunctionData({ + abi: worldAbi, + data: tx.input, + }); return publicClient.simulateContract({ account: tx.from, address: tx.to!, diff --git a/packages/world/ts/actions/callFrom.ts b/packages/world/ts/actions/callFrom.ts index 8261b729a0..95646b572c 100644 --- a/packages/world/ts/actions/callFrom.ts +++ b/packages/world/ts/actions/callFrom.ts @@ -1,7 +1,6 @@ import { slice, concat, - type WalletClient, type Transport, type Chain, type Account, @@ -10,6 +9,7 @@ import { type WriteContractReturnType, type EncodeFunctionDataParameters, type PublicClient, + Client, } from "viem"; import { getAction, encodeFunctionData } from "viem/utils"; import { writeContract } from "viem/actions"; @@ -52,12 +52,17 @@ type SystemFunction = { systemId: Hex; systemFunctionSelector: Hex }; // The function mapping is cached to avoid redundant retrievals for the same World function. export function callFrom( params: CallFromParameters, -): (client: WalletClient) => Pick, "writeContract"> { +): (client: Client) => Pick, "writeContract"> { return (client) => ({ // Applies to: `client.writeContract`, `getContract(client, ...).write` writeContract: async (writeArgs): Promise => { - // Skip if the contract isn't the World. - if (writeArgs.address !== params.worldAddress) { + // Skip if the contract isn't the World or the function called should not be redirected through `callFrom`. + if ( + writeArgs.address !== params.worldAddress || + writeArgs.functionName === "call" || + writeArgs.functionName === "callFrom" || + writeArgs.functionName === "registerDelegationWithSignature" + ) { return getAction(client, writeContract, "writeContract")(writeArgs); }