Skip to content

Commit

Permalink
Merge pull request #79 from ethereum-optimism/02-23-feat_add_tx-sende…
Browse files Browse the repository at this point in the history
…r_class_and_fix_private_key_deployments_causing_replacement_tx

feat: add tx-sender class and fix private key deployments causing replacement tx
  • Loading branch information
jakim929 authored Feb 23, 2025
2 parents 61276c6 + fe860ce commit 0a99025
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 33 deletions.
5 changes: 5 additions & 0 deletions .changeset/tough-squids-breathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@eth-optimism/super-cli": patch
---

fixed private key deployments causing replacement tx
12 changes: 9 additions & 3 deletions packages/cli/src/actions/deploy-create2/DeployCreate2Command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
import {getSponsoredSenderWalletRpcUrl} from '@/util/sponsoredSender';
import {DeployStatus} from '@/actions/deploy-create2/components/DeployStatus';
import {DeploymentParams} from '@/actions/deploy-create2/types';
import {createTxSenderFromPrivateKeyAccount} from '@/util/TxSender';

// Prepares any required data or loading state if waiting
export const DeployCreate2Command = ({
Expand Down Expand Up @@ -116,6 +117,13 @@ const DeployCreate2CommandInner = ({
throw new Error('No chains to deploy to');
}

const txSender = options.privateKey
? createTxSenderFromPrivateKeyAccount(
wagmiConfig,
privateKeyToAccount(options.privateKey),
)
: undefined;

return deployCreate2({
wagmiConfig,
deterministicAddress,
Expand All @@ -124,9 +132,7 @@ const DeployCreate2CommandInner = ({
chains: chainsToDeployTo,
foundryArtifactPath: options.forgeArtifactPath,
contractArguments: options.constructorArgs?.split(',') ?? [],
account: options.privateKey
? privateKeyToAccount(options.privateKey)
: undefined,
txSender,
});
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ export const PrivateKeyExecution = ({
}

if (error) {
return <Text>Error deploying contract: {error.message}</Text>;
return (
<Text>
{/* @ts-expect-error */}
Error deploying contract: {error.shortMessage || error.message}
</Text>
);
}

if (isReceiptLoading) {
Expand Down
28 changes: 11 additions & 17 deletions packages/cli/src/actions/deploy-create2/deployCreate2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,41 +8,35 @@ import {fromFoundryArtifactPath} from '@/util/forge/foundryProject';

import {runOperation, runOperationsMany} from '@/stores/operationStore';
import {requestTransactionTask} from '@/stores/transactionTaskStore';
import {
Config,
getTransaction,
sendTransaction,
waitForTransactionReceipt,
} from '@wagmi/core';
import {Address, Chain, encodeFunctionData, Hex, PrivateKeyAccount} from 'viem';
import {wagmiConfig} from '@/commands/_app';
import {Config, getTransaction, waitForTransactionReceipt} from '@wagmi/core';
import {Address, Chain, encodeFunctionData, Hex} from 'viem';
import {TxSender} from '@/util/TxSender';

export const executeTransactionOperation = ({
chainId,
deterministicAddress,
initCode,
baseSalt,
account,
txSender,
}: {
chainId: number;
deterministicAddress: Address;
initCode: Hex;
baseSalt: Hex;
account?: PrivateKeyAccount;
txSender?: TxSender;
}) => {
return {
key: ['executeTransaction', chainId, deterministicAddress],
fn: async () => {
if (account) {
return await sendTransaction(wagmiConfig, {
if (txSender) {
return await txSender.sendTx({
chainId,
to: CREATEX_ADDRESS,
data: encodeFunctionData({
abi: createXABI,
functionName: 'deployCreate2',
args: [baseSalt, initCode],
}),
account,
chainId,
});
}
return await requestTransactionTask({
Expand Down Expand Up @@ -102,7 +96,7 @@ export const deployCreate2 = async ({
chains,
foundryArtifactPath,
contractArguments,
account,
txSender,
}: {
wagmiConfig: Config;
deterministicAddress: Address;
Expand All @@ -111,7 +105,7 @@ export const deployCreate2 = async ({
chains: Chain[];
foundryArtifactPath: string;
contractArguments: string[];
account?: PrivateKeyAccount;
txSender?: TxSender;
}) => {
const transactionHashes = await runOperationsMany(
chains.map(chain =>
Expand All @@ -120,7 +114,7 @@ export const deployCreate2 = async ({
deterministicAddress,
initCode,
baseSalt,
account,
txSender,
}),
),
);
Expand Down
12 changes: 12 additions & 0 deletions packages/cli/src/actions/deploy-create2/sendAllTransactionTasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,22 @@ import {
useTransactionTaskStore,
} from '@/stores/transactionTaskStore';
import {chainById} from '@/util/chains/chains';
import {TxSender} from '@/util/TxSender';
import {http, sendTransaction} from '@wagmi/core';
import {createWalletClient, zeroAddress} from 'viem';
import {PrivateKeyAccount} from 'viem/accounts';

export const sendAllTransactionTasks = async (txSender: TxSender) => {
const taskEntryById = useTransactionTaskStore.getState().taskEntryById;

await Promise.all(
Object.values(taskEntryById).map(async task => {
const hash = await txSender.sendTx(task.request);
onTaskSuccess(task.id, hash);
}),
);
};

export const sendAllTransactionTasksWithPrivateKeyAccount = async (
account: PrivateKeyAccount,
) => {
Expand Down
39 changes: 27 additions & 12 deletions packages/cli/src/commands/deploy/create2-many.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,17 @@ import {
import {VerifyCommandInner} from '@/commands/verify';

import {deployCreate2} from '@/actions/deploy-create2/deployCreate2';
import {
sendAllTransactionTasksWithPrivateKeyAccount,
sendAllTransactionTasksWithCustomWalletRpc,
} from '@/actions/deploy-create2/sendAllTransactionTasks';
import {sendAllTransactionTasks} from '@/actions/deploy-create2/sendAllTransactionTasks';
import {getSponsoredSenderWalletRpcUrl} from '@/util/sponsoredSender';
import {zodPrivateKey} from '@/util/schemas';
import {useChecksForChainsForContracts} from '@/actions/deploy-create2/hooks/useChecksForChainsForContracts';
import {DeployStatus} from '@/actions/deploy-create2/components/DeployStatus';
import {DeploymentParams} from '@/actions/deploy-create2/types';
import {
createTxSenderFromCustomWalletRpc,
createTxSenderFromPrivateKeyAccount,
TxSender,
} from '@/util/TxSender';

const zodContractConfig = z
.object({
Expand Down Expand Up @@ -282,6 +284,13 @@ const DeployCreate2ManyCommandInner = ({
throw new Error('No chains to deploy to');
}

const txSender = options.privateKey
? createTxSenderFromPrivateKeyAccount(
wagmiConfig,
privateKeyToAccount(options.privateKey),
)
: undefined;

return Promise.all(
deploymentParams.map(async ({intent, computedParams}) => {
const {forgeArtifactPath, constructorArgs, chains} = intent;
Expand All @@ -300,9 +309,7 @@ const DeployCreate2ManyCommandInner = ({
chains: chains.filter(chain => chainIdsToDeployTo.has(chain.id)),
foundryArtifactPath: forgeArtifactPath,
contractArguments: constructorArgs || [],
account: options.privateKey
? privateKeyToAccount(options.privateKey)
: undefined,
txSender,
});
}),
);
Expand Down Expand Up @@ -384,22 +391,30 @@ const DeployCreate2ManyCommandInner = ({
<ChooseExecutionOption
label={'🚀 Ready to deploy!'}
onSubmit={async executionOption => {
setExecutionOption(executionOption);

if (executionOption.type === 'externalSigner') {
return;
}

let txSender: TxSender;
if (executionOption.type === 'privateKey') {
setExecutionOption(executionOption);
await sendAllTransactionTasksWithPrivateKeyAccount(
txSender = createTxSenderFromPrivateKeyAccount(
wagmiConfig,
privateKeyToAccount(executionOption.privateKey),
);
} else if (executionOption.type === 'sponsoredSender') {
setExecutionOption(executionOption);
await sendAllTransactionTasksWithCustomWalletRpc(chainId =>
txSender = createTxSenderFromCustomWalletRpc(chainId =>
getSponsoredSenderWalletRpcUrl(
executionOption.apiKey,
chainId,
),
);
} else {
setExecutionOption(executionOption);
throw new Error('Invariant broken');
}

await sendAllTransactionTasks(txSender);
}}
/>
</Box>
Expand Down
48 changes: 48 additions & 0 deletions packages/cli/src/util/AsyncQueue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
export interface AsyncQueueItem<T, R> {
item: T;
resolve: (value: R) => void;
reject: (error: any) => void;
}

// Simple queue that processes items sequentially
export class AsyncQueue<T, R> {
private queue: AsyncQueueItem<T, R>[] = [];
private processing = false;
private processFn: (item: T) => Promise<R>;

constructor(processFn: (item: T) => Promise<R>) {
this.processFn = processFn;
}

public enqueue(item: T): Promise<R> {
return new Promise((resolve, reject) => {
this.queue.push({item, resolve, reject});
this.processQueue();
});
}

private async processQueue(): Promise<void> {
if (this.processing) {
return;
}
this.processing = true;

try {
while (this.queue.length > 0) {
const queueItem = this.queue.shift();
if (!queueItem) continue;
try {
const result = await this.processFn(queueItem.item);
queueItem.resolve(result);
} catch (error) {
queueItem.reject(error);
}
}
} finally {
this.processing = false;
if (this.queue.length > 0) {
this.processQueue();
}
}
}
}
77 changes: 77 additions & 0 deletions packages/cli/src/util/TxSender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {AsyncQueue} from '@/util/AsyncQueue';
import {chainById} from '@/util/chains/chains';
import {TransactionTask} from '@/util/transactionTask';
import {sendTransaction, waitForTransactionReceipt} from '@wagmi/core';
import {
createWalletClient,
Hash,
http,
PrivateKeyAccount,
zeroAddress,
} from 'viem';
import {Config} from 'wagmi';

type WalletRpcUrlFactory = (chainId: number) => string;

type TxSenderTx = TransactionTask;

export interface TxSender {
sendTx: (tx: TxSenderTx) => Promise<Hash>;
}

export const createTxSenderFromPrivateKeyAccount = (
config: Config,
account: PrivateKeyAccount,
): TxSender => {
const queueByChainId = {} as Record<number, AsyncQueue<TxSenderTx, Hash>>;

config.chains.forEach(chain => {
queueByChainId[chain.id] = new AsyncQueue<TxSenderTx, Hash>(async tx => {
const hash = await sendTransaction(config, {
chainId: tx.chainId,
to: tx.to,
data: tx.data,
account,
});
// Prevent replacement tx.
await waitForTransactionReceipt(config, {
hash,
chainId: tx.chainId,
pollingInterval: 1000,
});

return hash;
});
});

return {
sendTx: async tx => {
if (!queueByChainId[tx.chainId]) {
throw new Error(`Chain tx queue for ${tx.chainId} not found`);
}
return await queueByChainId[tx.chainId]!.enqueue(tx);
},
};
};

export const createTxSenderFromCustomWalletRpc = (
getRpcUrl: WalletRpcUrlFactory,
): TxSender => {
return {
sendTx: async tx => {
const chain = chainById[tx.chainId];
if (!chain) {
throw new Error(`Chain not found for ${tx.chainId}`);
}
const walletClient = createWalletClient({
transport: http(getRpcUrl(tx.chainId)),
});
return await walletClient.sendTransaction({
to: tx.to,
data: tx.data,
account: zeroAddress, // will be ignored
chain: chain,
});
},
};
};

0 comments on commit 0a99025

Please sign in to comment.