Embed verify.sourcify.dev as Sourcify verification method (#3027)

* Embed verify.sourcify.dev as Sourcify verification method

* Rephrase Sourcify option inside verification method selector

* Mock iframe content in test

* Fix test

* Update screenshot for test
This commit is contained in:
Manuel Wedler
2025-11-04 12:31:49 +01:00
committed by GitHub
parent 01941f6b02
commit 90fd801b45
6 changed files with 37 additions and 128 deletions
@@ -3,7 +3,6 @@ import React from 'react';
import type { SmartContractVerificationConfig } from 'types/client/contract';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import * as socketServer from 'playwright/fixtures/socketServer';
import { test, expect } from 'playwright/lib';
import ContractVerificationForm from './ContractVerificationForm';
@@ -102,49 +101,26 @@ test('standard input json method', async({ render, page }) => {
await expect(component).toHaveScreenshot();
});
test.describe('sourcify', () => {
test.describe.configure({ mode: 'serial', timeout: 20_000 });
test('with multiple contracts', async({ render, page, createSocket }) => {
const component = await render(
<ContractVerificationForm config={ formConfig } hash={ hash }/>,
{ hooksConfig },
{ withSocket: true },
);
// select method
await component.locator('button').filter({ hasText: 'Verification method' }).click();
await page.getByRole('option', { name: 'Solidity (Sourcify)' }).click();
await page.getByText(/drop files/i).click();
await page.locator('input[name="sources"]').setInputFiles([
'./playwright/mocks/file_mock_1.json',
'./playwright/mocks/file_mock_2.json',
'./playwright/mocks/file_mock_with_very_long_name.json',
]);
await expect(component).toHaveScreenshot();
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ hash.toLowerCase() }`);
await page.getByRole('button', { name: /verify/i }).click();
socketServer.sendMessage(socket, channel, 'verification_result', {
status: 'error',
errors: {
// eslint-disable-next-line max-len
files: [ 'Detected 5 contracts (ERC20, IERC20, IERC20Metadata, Context, MockERC20), but can only verify 1 at a time. Please choose a main contract and click Verify again.' ],
},
test('sourcify method', async({ render, page }) => {
await page.route('https://verify.sourcify.dev/widget**', (route) => {
return route.fulfill({
status: 200,
contentType: 'text/html',
path: './playwright/mocks/page.html',
});
await component.locator('button').filter({ hasText: 'Contract name*' }).click();
const contractNameOption = page.getByRole('option', { name: 'MockERC20' });
await expect(contractNameOption).toBeVisible();
await expect(component).toHaveScreenshot();
});
const component = await render(
<ContractVerificationForm config={ formConfig } hash={ hash }/>,
{ hooksConfig },
{ withSocket: true },
);
// select method
await component.locator('button').filter({ hasText: 'Verification method' }).click();
await page.getByRole('option', { name: 'Sourcify (Solidity or Vyper)' }).click();
await expect(component).toHaveScreenshot();
});
test('multi-part files method', async({ render, page }) => {
@@ -205,7 +205,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
</Grid>
{ content }
{ formState.errors.root?.message && <Text color="text.error" mt={ 4 } fontSize="sm" whiteSpace="pre-wrap">{ formState.errors.root.message }</Text> }
{ Boolean(method) && methodValue !== 'solidity-hardhat' && methodValue !== 'solidity-foundry' && (
{ Boolean(method) && methodValue !== 'solidity-hardhat' && methodValue !== 'solidity-foundry' && methodValue !== 'sourcify' && (
<Button
size="md"
type="submit"
Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

@@ -1,61 +0,0 @@
import { createListCollection } from '@chakra-ui/react';
import React from 'react';
import { useFormContext } from 'react-hook-form';
import type { FormFields } from '../types';
import type { SelectOption } from 'toolkit/chakra/select';
import { FormFieldSelect } from 'toolkit/components/forms/fields/FormFieldSelect';
import { useUpdateEffect } from 'toolkit/hooks/useUpdateEffect';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
const SOURCIFY_ERROR_REGEXP = /\(([^()]*)\)/;
const ContractVerificationFieldContractIndex = () => {
const [ options, setOptions ] = React.useState<Array<SelectOption>>([]);
const { formState, watch } = useFormContext<FormFields>();
const sources = watch('sources');
const sourcesError = 'sources' in formState.errors ? formState.errors.sources?.message : undefined;
useUpdateEffect(() => {
if (!sourcesError) {
return;
}
const matchResult = sourcesError.match(SOURCIFY_ERROR_REGEXP);
const parsedMethods = matchResult?.[1].split(',');
if (!Array.isArray(parsedMethods) || parsedMethods.length === 0) {
return;
}
const newOptions = parsedMethods.map((option, index) => ({ label: option, value: String(index + 1) }));
setOptions(newOptions);
}, [ sourcesError ]);
useUpdateEffect(() => {
setOptions([]);
}, [ sources ]);
const collection = React.useMemo(() => {
return createListCollection({ items: options });
}, [ options ]);
if (options.length === 0) {
return null;
}
return (
<ContractVerificationFormRow>
<FormFieldSelect<FormFields, 'contract_index'>
name="contract_index"
placeholder="Contract name"
collection={ collection }
required
/>
</ContractVerificationFormRow>
);
};
export default React.memo(ContractVerificationFieldContractIndex);
@@ -1,22 +1,27 @@
import React from 'react';
import { useFormContext } from 'react-hook-form';
import type { FormFields } from '../types';
import config from 'configs/app';
import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldContractIndex from '../fields/ContractVerificationFieldContractIndex';
import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources';
const FILE_TYPES = [ '.json' as const, '.sol' as const ];
const ContractVerificationSourcify = () => {
const { watch } = useFormContext<FormFields>();
const address = watch('address');
const iframeUrl = `https://verify.sourcify.dev/widget?chainId=${ config.chain.id }&address=${ address }`;
return (
<ContractVerificationMethod title="Contract verification via Solidity (Sourcify)">
<ContractVerificationFieldSources
fileTypes={ FILE_TYPES }
multiple
required
title="Sources and Metadata JSON"
hint="Upload all Solidity contract source files and JSON metadata file(s) created during contract compilation."
<ContractVerificationMethod title="Contract verification via Sourcify (Solidity or Vyper)">
<iframe
src={ iframeUrl }
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
referrerPolicy="no-referrer"
width="100%"
height="840"
/>
<ContractVerificationFieldContractIndex/>
</ContractVerificationMethod>
);
};
+1 -12
View File
@@ -5,7 +5,6 @@ import type {
FormFields,
FormFieldsFlattenSourceCode,
FormFieldsMultiPartFile,
FormFieldsSourcify,
FormFieldsStandardInput,
FormFieldsStandardInputZk,
FormFieldsStylusGitHubRepo,
@@ -38,7 +37,7 @@ export const SUPPORTED_VERIFICATION_METHODS: Array<SmartContractVerificationMeth
export const METHOD_LABELS: Record<SmartContractVerificationMethod, string> = {
'flattened-code': 'Solidity (Single file)',
'standard-input': 'Solidity (Standard JSON input)',
sourcify: 'Solidity (Sourcify)',
sourcify: 'Sourcify (Solidity or Vyper)',
'multi-part': 'Solidity (Multi-part files)',
'vyper-code': 'Vyper (Contract)',
'vyper-multi-part': 'Vyper (Multi-part files)',
@@ -237,16 +236,6 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
return body;
}
case 'sourcify': {
const _data = data as FormFieldsSourcify;
const body = new FormData();
addFilesToFormData(body, _data.sources, 'files');
body.set('chosen_contract_index', _data.contract_index?.value ?? '0');
_data.license_type && body.set('license_type', _data.license_type?.[0] ?? defaultLicenseType);
return body;
}
case 'multi-part': {
const _data = data as FormFieldsMultiPartFile;