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

wip smart account deploy #3433

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
126 changes: 126 additions & 0 deletions packages/cli/src/deploy/getDeployClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { transactionQueue } from "@latticexyz/common/actions";
import { rhodolite } from "@latticexyz/common/chains";
import { claimGasPass, getAllowance, hasPassIssuer, gasEstimator } from "@latticexyz/paymaster/internal";
import { smartAccountActions } from "permissionless";
import { toSimpleSmartAccount } from "permissionless/accounts";
import {
Account,
Chain,
ChainContract,
Client,
ClientConfig,
LocalAccount,
Transport,
createClient,
http,
zeroAddress,
parseEther,
} from "viem";
import { getChainId, getTransactionReceipt, setBalance } from "viem/actions";
import { anvil } from "viem/chains";
import { createBundlerClient } from "viem/account-abstraction";
import { debug } from "./debug";
import { getAction } from "viem/utils";

const knownChains = [anvil, rhodolite] as const;

export async function getDeployClient(opts: {
rpcUrl: string;
rpcBatch?: boolean;
account: LocalAccount;
useSmartAccount: boolean;
}): Promise<Client<Transport, Chain | undefined, Account>> {
const chainId = await getChainId(createClient({ transport: http(opts.rpcUrl) }));
const chain: Chain | undefined = knownChains.find((c) => c.id === chainId);

const transport = http(opts.rpcUrl, {
batch: opts.rpcBatch ? { batchSize: 100, wait: 1000 } : undefined,
});

const clientOptions = {
chain,
transport,
pollingInterval: chainId === 31337 ? 100 : 500,
} as const satisfies ClientConfig;

if (opts.useSmartAccount) {
const bundlerHttpUrl = chain?.rpcUrls.bundler?.http[0];
// It would be nice to use viem's `getChainContractAddress` here, but it throws
const paymasterContract = chain?.contracts?.quarryPaymaster as ChainContract | undefined;
const paymasterAddress = paymasterContract?.address;

// TODO: error instead of a conditional?

if (bundlerHttpUrl && paymasterAddress) {
debug("setting up smart account");

const client = createClient(clientOptions);
const account = await toSimpleSmartAccount({ client, owner: opts.account });
const bundlerClient = createBundlerClient({
chain,
// TODO: figure out how to remove gas estimator (currently times out if not present)
transport: gasEstimator(http(bundlerHttpUrl)),
account,
paymaster: {
getPaymasterData: async () => ({
paymaster: paymasterAddress,
paymasterData: "0x",
}),
},
// TODO: figure out why viem isn't falling back to `chain.fees.estimateFeesPerGas` when this isn't set
userOperation: {
estimateFeesPerGas:
// TODO: move this to gas estimator transport?
chain
? async () => ({
maxFeePerGas: 100_000n,
maxPriorityFeePerGas: 0n,
})
: undefined,
},
}).extend(smartAccountActions());

// using bundler client for public actions may fail for other chains
// TODO: add publicActions bound to client (had deep TS errors) or use separate read/write clients in deployer

if (hasPassIssuer(chain)) {
const allowance = await getAllowance({ client, paymasterAddress, userAddress: account.address });
if (allowance >= parseEther("0.01")) {
debug("deployer smart account should have enough gas allowance");
} else {
debug("claimimg gas pass for deployer smart account");
await claimGasPass({ chain: rhodolite, userAddress: account.address });
}
}

if (!(await account.isDeployed())) {
// send empty tx to create the smart account, otherwise the first tx may fail due to bad gas estimation
debug("creating deployer smart account at", account.address);
const hash = await bundlerClient.sendTransaction({ to: zeroAddress });
debug("tx:", hash);
const receipt = await getAction(bundlerClient, getTransactionReceipt, "getTransactionReceipt")({ hash });
debug("receipt:", receipt.status);
}

return bundlerClient;
}
}

const client = createClient({
...clientOptions,
account: opts.account,
}).extend(transactionQueue());

if (chainId == 31337) {
debug("setting anvil balance");
await setBalance(
client.extend(() => ({ mode: "anvil" })),
{
address: client.account.address,
value: parseEther("100"),
},
);
}

return client;
}
48 changes: 30 additions & 18 deletions packages/cli/src/runDeploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import path from "node:path";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { InferredOptionTypes, Options } from "yargs";
import { deploy } from "./deploy/deploy";
import { createWalletClient, http, Hex, isHex, stringToHex } from "viem";
import { Hex, isHex, stringToHex } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { loadConfig, resolveConfigPath } from "@latticexyz/config/node";
import { World as WorldConfig } from "@latticexyz/world";
Expand All @@ -18,6 +18,9 @@ import { kmsKeyToAccount } from "@latticexyz/common/kms";
import { configToModules } from "./deploy/configToModules";
import { findContractArtifacts } from "@latticexyz/world/node";
import { enableAutomine } from "./utils/enableAutomine";
import { getDeployClient } from "./deploy/getDeployClient";
import { debug } from "./debug";
import { getAction } from "viem/utils";
import { defaultChains } from "./defaultChains";

export const deployOptions = {
Expand Down Expand Up @@ -49,6 +52,11 @@ export const deployOptions = {
type: "boolean",
desc: "Deploy the World with an AWS KMS key instead of local private key.",
},
smartAccount: {
type: "boolean",
desc: "Deploy using a smart account. A smart account will be created, owned by the provided private key, and use gas sponsorship when possible.",
default: false,
},
indexerUrl: {
type: "string",
desc: "The indexer URL to use.",
Expand Down Expand Up @@ -87,6 +95,7 @@ export async function runDeploy(opts: DeployOptions): Promise<WorldDeploy> {
// Run build
if (!opts.skipBuild) {
await build({ rootDir, config, foundryProfile: profile });
console.log();
}

const { systems, libraries } = await resolveConfig({
Expand Down Expand Up @@ -127,16 +136,14 @@ export async function runDeploy(opts: DeployOptions): Promise<WorldDeploy> {
}
})();

const client = createWalletClient({
transport: http(rpc, {
batch: opts.rpcBatch
? {
batchSize: 100,
wait: 1000,
}
: undefined,
}),
console.log("Deploying from", account.address);
debug("deploying from", account.address);

const client = await getDeployClient({
rpcUrl: rpc,
rpcBatch: opts.rpcBatch,
account,
useSmartAccount: opts.smartAccount,
});

const chainId = await getChainId(client);
Expand Down Expand Up @@ -171,14 +178,18 @@ export async function runDeploy(opts: DeployOptions): Promise<WorldDeploy> {
chainId,
});
if (opts.worldAddress == null || opts.alwaysRunPostDeploy) {
await postDeploy(
config.deploy.postDeployScript,
worldDeploy.address,
rpc,
profile,
opts.forgeScriptOptions,
opts.kms ? true : false,
);
if (client.account.type === "smart") {
console.log("Skipping post deploy for smart account (feature coming soon)");
} else {
await postDeploy(
config.deploy.postDeployScript,
worldDeploy.address,
rpc,
profile,
opts.forgeScriptOptions,
opts.kms ? true : false,
);
}
}

// Reset mining mode after deploy
Expand All @@ -192,6 +203,7 @@ export async function runDeploy(opts: DeployOptions): Promise<WorldDeploy> {
};

if (opts.saveDeployment) {
const chainId = client.chain?.id ?? (await getAction(client, getChainId, "getChainId")({}));
const deploysDir = path.join(config.deploy.deploysDirectory, chainId.toString());
mkdirSync(deploysDir, { recursive: true });
writeFileSync(path.join(deploysDir, "latest.json"), JSON.stringify(deploymentInfo, null, 2));
Expand Down
Loading