Skip to content

Commit

Permalink
Merge pull request #25 from ethereum-optimism/11-26-feat_add_contract…
Browse files Browse the repository at this point in the history
…_verification_flow

feat: add contract verification flow
  • Loading branch information
jakim929 authored Nov 27, 2024
2 parents 05a2132 + 40ec8fc commit e8f8467
Show file tree
Hide file tree
Showing 14 changed files with 699 additions and 16 deletions.
2 changes: 2 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
"@vitejs/plugin-react": "^4.3.3",
"abitype": "^1.0.6",
"figures": "^6.1.0",
"form-data": "^4.0.1",
"immer": "^10.1.1",
"ink": "^5.0.1",
"pastel": "^3.0.0",
"react": "^18.2.0",
Expand Down
30 changes: 20 additions & 10 deletions packages/cli/src/actions/deployCreateXCreate2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,28 @@ export const zodDeployCreateXCreate2Params = z.object({
alias: 'pk',
}),
),
chains: z
.string()
.transform(value => value.split(','))
.describe(
option({
description: 'Chains to deploy to',
alias: 'c',
}),
),
chains: z.array(z.string()).describe(
option({
description: 'Chains to deploy to',
alias: 'c',
}),
),
network: zodSupportedNetwork.describe(
option({
description: 'Network to deploy to',
alias: 'n',
}),
),
verify: z
.boolean()
.default(false)
.optional()
.describe(
option({
description: 'Verify contract on deployed chains',
alias: 'v',
}),
),
});

export type DeployCreateXCreate2Params = z.infer<
Expand Down Expand Up @@ -107,7 +114,10 @@ const createDeployContext = async ({
);

const publicClients = selectedChains.map(chain => {
return createPublicClient({chain, transport: http()});
return createPublicClient({
chain,
transport: http(),
});
});

return {
Expand Down
121 changes: 121 additions & 0 deletions packages/cli/src/actions/verifyContract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import {fromFoundryArtifactPath} from '@/forge/foundryProject';
import {queryMappingChainListItemByIdentifier} from '@/queries/chainRegistryItemByIdentifier';
import {useContractVerificationStore} from '@/stores/contractVerification';
import {zodSupportedNetwork} from '@/superchain-registry/fetchChainList';
import {zodAddress} from '@/validators/schemas';
import {verifyContractOnBlockscout} from '@/verify/blockscout';
import {createStandardJsonInput} from '@/verify/createStandardJsonInput';
import {identifyExplorer} from '@/verify/identifyExplorerType';
import {option} from 'pastel';
import {z} from 'zod';

export const zodVerifyContractParams = z.object({
forgeArtifactPath: z
.string()
.describe(
option({
description: 'Path to the Forge artifact',
alias: 'f',
}),
)
.min(1),
contractAddress: zodAddress.describe(
option({description: 'Contract address', alias: 'a'}),
),
network: zodSupportedNetwork.describe(
option({
description: 'Network to verify on',
alias: 'n',
}),
),
chains: z.array(z.string()).describe(
option({
description: 'Chains to verify on',
alias: 'c',
}),
),
});

const getVerifyContractContext = async (
params: z.infer<typeof zodVerifyContractParams>,
) => {
const {forgeArtifactPath, network, chains} = params;
const chainIdentifiers = chains.map(chain => `${network}/${chain}`);

const chainListItemByIdentifier =
await queryMappingChainListItemByIdentifier();
const {foundryProject, contractFileName} = await fromFoundryArtifactPath(
forgeArtifactPath,
);

return {
selectedChainList: chainIdentifiers.map(
identifier => chainListItemByIdentifier[identifier],
),
foundryProject,
contractFileName,
};
};

export const verifyContract = async (
params: z.infer<typeof zodVerifyContractParams>,
) => {
const store = useContractVerificationStore.getState();

const {contractAddress, chains} = params;

let context: Awaited<ReturnType<typeof getVerifyContractContext>>;
try {
context = await getVerifyContractContext(params);

store.setPrepareSuccess(
context.selectedChainList.map(chain => chain!.chainId),
);
} catch (e) {
store.setPrepareError(e as Error);
return;
}

const {foundryProject, contractFileName} = context;

// TODO: Type this
let standardJsonInput: any;
try {
standardJsonInput = await createStandardJsonInput(
foundryProject.baseDir,
contractFileName,
);
store.setGenerateSuccess();
} catch (e) {
store.setGenerateError(e as Error);
return;
}

await Promise.all(
context.selectedChainList.map(async chain => {
const chainId = chain!.chainId;
const explorer = chain!.explorers[0]!;

store.setVerifyPending(chainId);

const explorerType = await identifyExplorer(explorer).catch(() => null);

if (explorerType !== 'blockscout') {
throw new Error('Unsupported explorer');
}

try {
await verifyContractOnBlockscout(
chain!.explorers[0]!,
contractAddress,
contractFileName.replace('.sol', ''),
standardJsonInput,
);
store.setVerifySuccess(chainId);
} catch (e) {
store.setVerifyError(chainId, e as Error);
return;
}
}),
);
};
6 changes: 6 additions & 0 deletions packages/cli/src/commands/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import type {AppProps} from 'pastel';
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
import {useInput} from 'ink';

export const queryClient = new QueryClient();

export default function App({Component, commandProps}: AppProps) {
useInput((input, key) => {
if (input === 'c' && key.ctrl) {
process.exit();
}
});
return (
<QueryClientProvider client={queryClient}>
<Component {...commandProps} />
Expand Down
20 changes: 19 additions & 1 deletion packages/cli/src/commands/deploy/create2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {z} from 'zod';
import {option} from 'pastel';
import {DeployCreate2Wizard} from '@/deploy-create2-wizard/DeployCreate2Wizard';
import {fromZodError} from 'zod-validation-error';
import VerifyCommand from '@/commands/verify';

const statusBadge = {
pending: <Spinner />,
Expand All @@ -35,8 +36,8 @@ const zodDeployCreate2CommandEntrypointOptions = zodDeployCreateXCreate2Params
z.object({
interactive: z
.boolean()
.optional()
.default(false)
.optional()
.describe(
option({
description: 'Interactive mode',
Expand Down Expand Up @@ -99,6 +100,23 @@ const DeployCreate2Command = ({
return <Spinner />;
}

if (deployment.state === 'completed') {
console.log(options);
if (options.verify) {
return (
<VerifyCommand
options={{
contractAddress: deterministicAddress,
forgeArtifactPath: options.forgeArtifactPath,
network: options.network,
chains: options.chains,
}}
/>
);
}
return <Text bold>Deployment run completed</Text>;
}

return (
<Box flexDirection="column" gap={1} paddingTop={2} paddingX={2}>
<Text bold>Superchain ERC20 Deployment</Text>
Expand Down
91 changes: 91 additions & 0 deletions packages/cli/src/commands/verify.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {Box, Text} from 'ink';
import {useEffect} from 'react';
import {
verifyContract,
zodVerifyContractParams,
} from '@/actions/verifyContract';
import {z} from 'zod';
import {useContractVerificationStore} from '@/stores/contractVerification';
import {Spinner} from '@inkjs/ui';
import {useMappingChainById} from '@/queries/chainById';

const zodVerifyContractCommandParams = zodVerifyContractParams;

const VerifyCommand = ({
options,
}: {
options: z.infer<typeof zodVerifyContractCommandParams>;
}) => {
useEffect(() => {
verifyContract(options);
}, []);

const {currentStep, stateByChainId} = useContractVerificationStore();
const {data: chainById} = useMappingChainById();

if (currentStep === 'prepare' || !chainById) {
return (
<Box>
<Spinner label="Preparing..." />
</Box>
);
}

if (currentStep === 'generate-standard-json-input') {
return (
<Box>
<Spinner label="Generating standard JSON input..." />
</Box>
);
}

const chains = Object.values(stateByChainId).sort(
(a, b) => a.chainId - b.chainId,
);

return (
<Box flexDirection="column">
<Box marginBottom={1}>
<Text bold>Superchain Contract Verification</Text>
</Box>
{chains.map(chain => (
<Box key={chain.chainId} flexDirection="column">
<Box gap={1}>
<Box width={24}>
<Text color="blue">{`${options.network}/${
chainById[chain.chainId]!.name
}`}</Text>
</Box>
{chain.verificationStatus === 'pending' && (
<Box gap={1}>
<Spinner />
<Text dimColor>Verification in progress...</Text>
</Box>
)}
{chain.verificationStatus === 'success' && (
<Text color="green">
✓ Verification successful{' '}
{`${
chainById[chain.chainId]?.blockExplorers?.default.url
}/address/${options.contractAddress}`}
</Text>
)}
{chain.verificationStatus === 'failure' && (
<Text color="red">✗ Verification failed</Text>
)}
</Box>
{chain.error && (
<Box marginLeft={2}>
<Text color="red" dimColor>
Error: {chain.error.message || JSON.stringify(chain.error)}
</Text>
</Box>
)}
</Box>
))}
</Box>
);
};

export default VerifyCommand;
export const options = zodVerifyContractCommandParams;
Loading

0 comments on commit e8f8467

Please sign in to comment.