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(common,world): improvements for smart accounts #2578

Merged
merged 28 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
460fdd1
feat(templates/react): setup aa infra
mouseless0x Mar 18, 2024
88495c8
fix: typo + format
mouseless0x Mar 18, 2024
1709624
Merge branch 'latticexyz:main' into aa-setup
mouseless0x Mar 19, 2024
d37a14d
use v0.7
mouseless0x Mar 20, 2024
94121e1
support v0.7 + client side changes
mouseless0x Mar 21, 2024
7f628f1
wait for deployment
mouseless0x Mar 22, 2024
95bed30
use host.docker.internal in docker container
mouseless0x Mar 22, 2024
99f0930
fix
mouseless0x Mar 22, 2024
44632c5
fix: deploy bundler through npmjs instead of docker
mouseless0x Mar 25, 2024
a8f2a37
fix: send eth_* request to publicClient
mouseless0x Mar 25, 2024
1bfc017
Merge branch 'latticexyz:main' into aa-setup
mouseless0x Mar 25, 2024
4bc1630
change signature of callFrom action to be compatible with smart accounts
alvrs Mar 26, 2024
660dee9
feat(template/react): add mock verifyingPaymaster
mouseless0x Mar 26, 2024
824ba9b
feat: add mock onchain paymaster
mouseless0x Mar 27, 2024
31272a1
Merge branch 'main' into aa-setup
alvrs Mar 27, 2024
4473d30
skip callFrom for registterDelegationWithSignature
karooolis Mar 27, 2024
80222e3
skip callFrom for call & callFrom
karooolis Mar 27, 2024
eb3f978
update pre-bundled alto dependency
mouseless0x Mar 28, 2024
53cc6ee
revert template changes
holic Apr 1, 2024
bc9f359
undo dep
holic Apr 1, 2024
2636344
use object for params
holic Apr 1, 2024
94705cd
Create smooth-ducks-sell.md
holic Apr 1, 2024
ad194e2
clarify comment
holic Apr 1, 2024
769b695
Create smooth-laws-float.md
holic Apr 1, 2024
d27f959
same treatment for sendTransaction
holic Apr 1, 2024
a513069
Update smooth-ducks-sell.md
holic Apr 1, 2024
281dec8
Update smooth-ducks-sell.md
holic Apr 1, 2024
26410cf
Apply suggestions from code review
holic Apr 1, 2024
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
9 changes: 9 additions & 0 deletions .changeset/smooth-ducks-sell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@latticexyz/common": minor
Copy link
Member Author

Choose a reason for hiding this comment

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

so according to semver, this should be minor because its an extra feature, not a fix

but maybe we don't want to trigger a bump to 2.1 yet?

holic marked this conversation as resolved.
Show resolved Hide resolved
---

`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.
5 changes: 5 additions & 0 deletions .changeset/smooth-laws-float.md
Original file line number Diff line number Diff line change
@@ -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`.
18 changes: 12 additions & 6 deletions packages/common/src/actions/transactionQueue.ts
Original file line number Diff line number Diff line change
@@ -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<TChain extends Chain, TAccount extends Account>(): (
client: WalletClient<Transport, TChain, TAccount>,
) => Pick<WalletActions<TChain, TAccount>, "writeContract" | "sendTransaction"> {
export type TransactionQueueOptions<chain extends Chain> = {
publicClient?: PublicClient<Transport, chain>;
};

export function transactionQueue<chain extends Chain, account extends Account>({
publicClient,
}: TransactionQueueOptions<chain> = {}): (
client: Client<Transport, chain, account>,
) => Pick<WalletActions<chain, account>, "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),
});
}
4 changes: 2 additions & 2 deletions packages/common/src/actions/writeObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import type {
Chain,
Account,
WalletActions,
WalletClient,
WriteContractReturnType,
Client,
} from "viem";
import { getAction } from "viem/utils";
import { writeContract } from "viem/actions";
Expand All @@ -16,7 +16,7 @@ type WriteObserverParameters = { onWrite: (write: ContractWrite) => void };
export function writeObserver<TChain extends Chain, TAccount extends Account>({
onWrite,
}: WriteObserverParameters): (
client: WalletClient<Transport, TChain, TAccount>,
client: Client<Transport, TChain, TAccount>,
) => Pick<WalletActions<TChain, TAccount>, "writeContract"> {
let nextWriteId = 0;

Expand Down
4 changes: 2 additions & 2 deletions packages/common/src/createBurnerAccount.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
8 changes: 6 additions & 2 deletions packages/common/src/getContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
22 changes: 12 additions & 10 deletions packages/common/src/sendTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<Transport, TChain, TAccount>,
request: SendTransactionParameters<TChain, TAccount, TChainOverride>,
client: Client<Transport, chain, account>,
request: SendTransactionParameters<chain, account, chainOverride>,
publicClient?: PublicClient<Transport, chain>,
): Promise<SendTransactionReturnType> {
const rawAccount = request.account ?? client.account;
if (!rawAccount) {
Expand All @@ -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<SendTransactionParameters<TChain, TAccount, TChainOverride>> {
async function prepare(): Promise<SendTransactionParameters<chain, account, chainOverride>> {
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<TChain>);
} as CallParameters<chain>);

return request;
}
Expand All @@ -67,7 +69,7 @@ export async function sendTransaction<

const nonce = nonceManager.nextNonce();
debug("sending tx with nonce", nonce, "to", preparedRequest.to);
const parameters: SendTransactionParameters<TChain, TAccount, TChainOverride> = { nonce, ...preparedRequest };
const parameters: SendTransactionParameters<chain, account, chainOverride> = { nonce, ...preparedRequest };
return await viem_sendTransaction(client, parameters);
},
{
Expand Down
22 changes: 15 additions & 7 deletions packages/common/src/writeContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -31,6 +32,7 @@ export async function writeContract<
>(
client: Client<Transport, chain, account>,
request: WriteContractParameters<abi, functionName, args, chain, account, chainOverride>,
publicClient?: PublicClient<Transport, chain>,
Copy link
Member Author

@holic holic Apr 1, 2024

Choose a reason for hiding this comment

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

this method is deprecated, so I didn't document this change in the changelog

I also don't love this pattern of adding an extra param, ideally should be an object here for easier extending, but again, this is deprecated so will leave it be

a future clean up should move this function inline into the transactionQueue decorator

Copy link
Member

@alvrs alvrs Apr 1, 2024

Choose a reason for hiding this comment

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

opened an issue for this so we can add it to the backlog: #2584

): Promise<WriteContractReturnType> {
const rawAccount = request.account ?? client.account;
if (!rawAccount) {
Expand All @@ -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",
});
Expand All @@ -54,11 +56,14 @@ export async function writeContract<
}

debug("simulating", request.functionName, "at", request.address);
const result = await simulateContract<chain, account, abi, functionName, args, chainOverride>(client, {
...request,
blockTag: "pending",
account,
} as unknown as SimulateContractParameters<abi, functionName, args, chain, chainOverride>);
const result = await simulateContract<chain, account | undefined, abi, functionName, args, chainOverride>(
publicClient ?? client,
{
...request,
blockTag: "pending",
account,
} as unknown as SimulateContractParameters<abi, functionName, args, chain, chainOverride>,
);

return result.request as unknown as WriteContractParameters<abi, functionName, args, chain, account, chainOverride>;
}
Expand All @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion packages/dev-tools/src/actions/getTransactionResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!,
Expand Down
13 changes: 9 additions & 4 deletions packages/world/ts/actions/callFrom.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
slice,
concat,
type WalletClient,
type Transport,
type Chain,
type Account,
Expand All @@ -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";
Expand Down Expand Up @@ -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<TChain extends Chain, TAccount extends Account>(
params: CallFromParameters,
): (client: WalletClient<Transport, TChain, TAccount>) => Pick<WalletActions<TChain, TAccount>, "writeContract"> {
): (client: Client<Transport, TChain, TAccount>) => Pick<WalletActions<TChain, TAccount>, "writeContract"> {
return (client) => ({
// Applies to: `client.writeContract`, `getContract(client, ...).write`
writeContract: async (writeArgs): Promise<WriteContractReturnType> => {
// 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"
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this registerDelegationWithSignature condition necessary? The registerDelegationWithSignature function should be called by a delegator, not a delegatee. This callFrom extension action is intended for the use of the delegatee's wallet client (after delegation), and the delegator's wallet client does not need to use this.

Copy link
Member Author

@holic holic Apr 1, 2024

Choose a reason for hiding this comment

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

registerDelegationWithSignature is called by the delegatee, using a signed message by the delegator! This allows us to write to the world using only the delegatee/burner account.

Copy link
Contributor

Choose a reason for hiding this comment

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

Make sense! Thanks.

) {
return getAction(client, writeContract, "writeContract")(writeArgs);
}

Expand Down
Loading