Commit 459e5ea0 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Contract: License (#1668)

* display license on contract page and in the list

* add new field to the form

* review fixes
parent 20d8c0cd
import type { ContractLicense } from 'types/client/contract';
export const CONTRACT_LICENSES: Array<ContractLicense> = [
{
type: 'none',
label: 'None',
title: 'No License',
url: 'https://choosealicense.com/no-permission/',
},
{
type: 'unlicense',
label: 'Unlicense',
title: 'The Unlicense',
url: 'https://choosealicense.com/licenses/unlicense/',
},
{
type: 'mit',
label: 'MIT',
title: 'MIT License',
url: 'https://choosealicense.com/licenses/mit/',
},
{
type: 'gnu_gpl_v2',
label: 'GNU GPLv2',
title: 'GNU General Public License v2.0',
url: 'https://choosealicense.com/licenses/gpl-2.0/',
},
{
type: 'gnu_gpl_v3',
label: 'GNU GPLv3',
title: 'GNU General Public License v3.0',
url: 'https://choosealicense.com/licenses/gpl-3.0/',
},
{
type: 'gnu_lgpl_v2_1',
label: 'GNU LGPLv2.1',
title: 'GNU Lesser General Public License v2.1',
url: 'https://choosealicense.com/licenses/lgpl-2.1/',
},
{
type: 'gnu_lgpl_v3',
label: 'GNU LGPLv3',
title: 'GNU Lesser General Public License v3.0',
url: 'https://choosealicense.com/licenses/lgpl-3.0/',
},
{
type: 'bsd_2_clause',
label: 'BSD-2-Clause',
title: 'BSD 2-clause "Simplified" license',
url: 'https://choosealicense.com/licenses/bsd-2-clause/',
},
{
type: 'bsd_3_clause',
label: 'BSD-3-Clause',
title: 'BSD 3-clause "New" Or "Revised" license',
url: 'https://choosealicense.com/licenses/bsd-3-clause/',
},
{
type: 'mpl_2_0',
label: 'MPL-2.0',
title: 'Mozilla Public License 2.0',
url: 'https://choosealicense.com/licenses/mpl-2.0/',
},
{
type: 'osl_3_0',
label: 'OSL-3.0',
title: 'Open Software License 3.0',
url: 'https://choosealicense.com/licenses/osl-3.0/',
},
{
type: 'apache_2_0',
label: 'Apache',
title: 'Apache 2.0',
url: 'https://choosealicense.com/licenses/apache-2.0/',
},
{
type: 'gnu_agpl_v3',
label: 'GNU AGPLv3',
title: 'GNU Affero General Public License',
url: 'https://choosealicense.com/licenses/agpl-3.0/',
},
{
type: 'bsl_1_1',
label: 'BSL 1.1',
title: 'Business Source License',
url: 'https://mariadb.com/bsl11/',
},
];
...@@ -31,6 +31,7 @@ export const verified: Partial<SmartContract> = { ...@@ -31,6 +31,7 @@ export const verified: Partial<SmartContract> = {
{ address_hash: '0xa62744BeE8646e237441CDbfdedD3458861748A8', name: 'math' }, { address_hash: '0xa62744BeE8646e237441CDbfdedD3458861748A8', name: 'math' },
], ],
language: 'solidity', language: 'solidity',
license_type: 'gnu_gpl_v3',
}; };
export const withMultiplePaths: Partial<SmartContract> = { export const withMultiplePaths: Partial<SmartContract> = {
......
...@@ -20,6 +20,7 @@ export const contract1: VerifiedContract = { ...@@ -20,6 +20,7 @@ export const contract1: VerifiedContract = {
optimization_enabled: false, optimization_enabled: false,
tx_count: 7334224, tx_count: 7334224,
verified_at: '2022-09-16T18:49:29.605179Z', verified_at: '2022-09-16T18:49:29.605179Z',
license_type: 'mit',
}; };
export const contract2: VerifiedContract = { export const contract2: VerifiedContract = {
...@@ -42,6 +43,7 @@ export const contract2: VerifiedContract = { ...@@ -42,6 +43,7 @@ export const contract2: VerifiedContract = {
optimization_enabled: true, optimization_enabled: true,
tx_count: 440, tx_count: 440,
verified_at: '2021-09-07T20:01:56.076979Z', verified_at: '2021-09-07T20:01:56.076979Z',
license_type: 'bsd_3_clause',
}; };
export const baseResponse: VerifiedContractsResponse = { export const baseResponse: VerifiedContractsResponse = {
......
...@@ -40,6 +40,7 @@ export const CONTRACT_CODE_VERIFIED = { ...@@ -40,6 +40,7 @@ export const CONTRACT_CODE_VERIFIED = {
optimization_runs: 200, optimization_runs: 200,
source_code: 'source_code', source_code: 'source_code',
verified_at: '2023-02-21T14:39:16.906760Z', verified_at: '2023-02-21T14:39:16.906760Z',
license_type: 'mit',
} as unknown as SmartContract; } as unknown as SmartContract;
export const VERIFIED_CONTRACT_INFO: VerifiedContract = { export const VERIFIED_CONTRACT_INFO: VerifiedContract = {
...@@ -52,6 +53,7 @@ export const VERIFIED_CONTRACT_INFO: VerifiedContract = { ...@@ -52,6 +53,7 @@ export const VERIFIED_CONTRACT_INFO: VerifiedContract = {
optimization_enabled: false, optimization_enabled: false,
tx_count: 565058, tx_count: 565058,
verified_at: '2023-04-10T13:16:33.884921Z', verified_at: '2023-04-10T13:16:33.884921Z',
license_type: 'mit',
}; };
export const VERIFIED_CONTRACTS_COUNTERS: VerifiedContractsCounters = { export const VERIFIED_CONTRACTS_COUNTERS: VerifiedContractsCounters = {
......
...@@ -3,6 +3,22 @@ import type { Abi, AbiType } from 'abitype'; ...@@ -3,6 +3,22 @@ import type { Abi, AbiType } from 'abitype';
export type SmartContractMethodArgType = AbiType; export type SmartContractMethodArgType = AbiType;
export type SmartContractMethodStateMutability = 'view' | 'nonpayable' | 'payable'; export type SmartContractMethodStateMutability = 'view' | 'nonpayable' | 'payable';
export type SmartContractLicenseType =
'none' |
'unlicense' |
'mit' |
'gnu_gpl_v2' |
'gnu_gpl_v3' |
'gnu_lgpl_v2_1' |
'gnu_lgpl_v3' |
'bsd_2_clause' |
'bsd_3_clause' |
'mpl_2_0' |
'osl_3_0' |
'apache_2_0' |
'gnu_agpl_v3' |
'bsl_1_1';
export interface SmartContract { export interface SmartContract {
deployed_bytecode: string | null; deployed_bytecode: string | null;
creation_bytecode: string | null; creation_bytecode: string | null;
...@@ -37,6 +53,7 @@ export interface SmartContract { ...@@ -37,6 +53,7 @@ export interface SmartContract {
verified_twin_address_hash: string | null; verified_twin_address_hash: string | null;
minimal_proxy_address_hash: string | null; minimal_proxy_address_hash: string | null;
language: string | null; language: string | null;
license_type: SmartContractLicenseType | null;
} }
export type SmartContractDecodedConstructorArg = [ export type SmartContractDecodedConstructorArg = [
...@@ -136,6 +153,7 @@ export interface SmartContractVerificationConfigRaw { ...@@ -136,6 +153,7 @@ export interface SmartContractVerificationConfigRaw {
vyper_compiler_versions: Array<string>; vyper_compiler_versions: Array<string>;
vyper_evm_versions: Array<string>; vyper_evm_versions: Array<string>;
is_rust_verifier_microservice_enabled: boolean; is_rust_verifier_microservice_enabled: boolean;
license_types: Record<SmartContractLicenseType, number>;
} }
export interface SmartContractVerificationConfig extends SmartContractVerificationConfigRaw { export interface SmartContractVerificationConfig extends SmartContractVerificationConfigRaw {
......
import type { AddressParam } from './addressParams'; import type { AddressParam } from './addressParams';
import type { SmartContractLicenseType } from './contract';
export interface VerifiedContract { export interface VerifiedContract {
address: AddressParam; address: AddressParam;
...@@ -10,6 +11,7 @@ export interface VerifiedContract { ...@@ -10,6 +11,7 @@ export interface VerifiedContract {
tx_count: number | null; tx_count: number | null;
verified_at: string; verified_at: string;
market_cap: string | null; market_cap: string | null;
license_type: SmartContractLicenseType | null;
} }
export interface VerifiedContractsResponse { export interface VerifiedContractsResponse {
......
import type { SmartContractLicenseType } from 'types/api/contract';
export interface ContractCodeIde { export interface ContractCodeIde {
title: string; title: string;
url: string; url: string;
icon_url: string; icon_url: string;
} }
export interface ContractLicense {
type: SmartContractLicenseType;
url: string;
label: string;
title: string;
}
...@@ -9,6 +9,7 @@ import { route } from 'nextjs-routes'; ...@@ -9,6 +9,7 @@ import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import { CONTRACT_LICENSES } from 'lib/contracts/licenses';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
...@@ -118,6 +119,23 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -118,6 +119,23 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
</Button> </Button>
); );
const licenseLink = (() => {
if (!data?.license_type) {
return null;
}
const license = CONTRACT_LICENSES.find((license) => license.type === data.license_type);
if (!license || license.type === 'none') {
return null;
}
return (
<LinkExternal href={ license.url }>
{ license.label }
</LinkExternal>
);
})();
const constructorArgs = (() => { const constructorArgs = (() => {
if (!data?.decoded_constructor_args) { if (!data?.decoded_constructor_args) {
return data?.constructor_args; return data?.constructor_args;
...@@ -233,6 +251,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -233,6 +251,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
{ data.name && <InfoItem label="Contract name" content={ data.name } isLoading={ isPlaceholderData }/> } { data.name && <InfoItem label="Contract name" content={ data.name } isLoading={ isPlaceholderData }/> }
{ data.compiler_version && <InfoItem label="Compiler version" content={ data.compiler_version } isLoading={ isPlaceholderData }/> } { data.compiler_version && <InfoItem label="Compiler version" content={ data.compiler_version } isLoading={ isPlaceholderData }/> }
{ data.evm_version && <InfoItem label="EVM version" content={ data.evm_version } textTransform="capitalize" isLoading={ isPlaceholderData }/> } { data.evm_version && <InfoItem label="EVM version" content={ data.evm_version } textTransform="capitalize" isLoading={ isPlaceholderData }/> }
{ licenseLink && <InfoItem label="License" content={ licenseLink } isLoading={ isPlaceholderData }/> }
{ typeof data.optimization_enabled === 'boolean' && { typeof data.optimization_enabled === 'boolean' &&
<InfoItem label="Optimization enabled" content={ data.optimization_enabled ? 'true' : 'false' } isLoading={ isPlaceholderData }/> } <InfoItem label="Optimization enabled" content={ data.optimization_enabled ? 'true' : 'false' } isLoading={ isPlaceholderData }/> }
{ data.optimization_runs && <InfoItem label="Optimization runs" content={ String(data.optimization_runs) } isLoading={ isPlaceholderData }/> } { data.optimization_runs && <InfoItem label="Optimization runs" content={ String(data.optimization_runs) } isLoading={ isPlaceholderData }/> }
......
...@@ -54,6 +54,22 @@ const formConfig: SmartContractVerificationConfig = { ...@@ -54,6 +54,22 @@ const formConfig: SmartContractVerificationConfig = {
'petersburg', 'petersburg',
'istanbul', 'istanbul',
], ],
license_types: {
apache_2_0: 12,
bsd_2_clause: 8,
bsd_3_clause: 9,
bsl_1_1: 14,
gnu_agpl_v3: 13,
gnu_gpl_v2: 4,
gnu_gpl_v3: 5,
gnu_lgpl_v2_1: 6,
gnu_lgpl_v3: 7,
mit: 3,
mpl_2_0: 10,
none: 1,
osl_3_0: 11,
unlicense: 2,
},
}; };
test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) => { test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) => {
...@@ -64,9 +80,16 @@ test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) = ...@@ -64,9 +80,16 @@ test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) =
{ hooksConfig }, { hooksConfig },
); );
// select license
await component.getByLabel(/contract license/i).focus();
await component.getByLabel(/contract license/i).fill('mit');
await page.getByRole('button', { name: /mit license/i }).click();
// select method
await component.getByLabel(/verification method/i).focus(); await component.getByLabel(/verification method/i).focus();
await component.getByLabel(/verification method/i).type('solidity'); await component.getByLabel(/verification method/i).fill('solidity');
await page.getByRole('button', { name: /flattened source code/i }).click(); await page.getByRole('button', { name: /flattened source code/i }).click();
await page.getByText(/add contract libraries/i).click(); await page.getByText(/add contract libraries/i).click();
await page.locator('button[aria-label="add"]').click(); await page.locator('button[aria-label="add"]').click();
...@@ -81,8 +104,9 @@ test('standard input json method', async({ mount, page }) => { ...@@ -81,8 +104,9 @@ test('standard input json method', async({ mount, page }) => {
{ hooksConfig }, { hooksConfig },
); );
// select method
await component.getByLabel(/verification method/i).focus(); await component.getByLabel(/verification method/i).focus();
await component.getByLabel(/verification method/i).type('solidity'); await component.getByLabel(/verification method/i).fill('solidity');
await page.getByRole('button', { name: /standard json input/i }).click(); await page.getByRole('button', { name: /standard json input/i }).click();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
...@@ -102,8 +126,9 @@ test.describe('sourcify', () => { ...@@ -102,8 +126,9 @@ test.describe('sourcify', () => {
{ hooksConfig }, { hooksConfig },
); );
// select method
await component.getByLabel(/verification method/i).focus(); await component.getByLabel(/verification method/i).focus();
await component.getByLabel(/verification method/i).type('solidity'); await component.getByLabel(/verification method/i).fill('solidity');
await page.getByRole('button', { name: /sourcify/i }).click(); await page.getByRole('button', { name: /sourcify/i }).click();
await page.getByText(/drop files/i).click(); await page.getByText(/drop files/i).click();
...@@ -129,7 +154,7 @@ test.describe('sourcify', () => { ...@@ -129,7 +154,7 @@ test.describe('sourcify', () => {
}); });
await component.getByLabel(/contract name/i).focus(); await component.getByLabel(/contract name/i).focus();
await component.getByLabel(/contract name/i).type('e'); await component.getByLabel(/contract name/i).fill('e');
const contractNameOption = page.getByRole('button', { name: /MockERC20/i }); const contractNameOption = page.getByRole('button', { name: /MockERC20/i });
await expect(contractNameOption).toBeVisible(); await expect(contractNameOption).toBeVisible();
...@@ -146,8 +171,9 @@ test('multi-part files method', async({ mount, page }) => { ...@@ -146,8 +171,9 @@ test('multi-part files method', async({ mount, page }) => {
{ hooksConfig }, { hooksConfig },
); );
// select method
await component.getByLabel(/verification method/i).focus(); await component.getByLabel(/verification method/i).focus();
await component.getByLabel(/verification method/i).type('solidity'); await component.getByLabel(/verification method/i).fill('solidity');
await page.getByRole('button', { name: /multi-part files/i }).click(); await page.getByRole('button', { name: /multi-part files/i }).click();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
...@@ -161,8 +187,9 @@ test('vyper contract method', async({ mount, page }) => { ...@@ -161,8 +187,9 @@ test('vyper contract method', async({ mount, page }) => {
{ hooksConfig }, { hooksConfig },
); );
// select method
await component.getByLabel(/verification method/i).focus(); await component.getByLabel(/verification method/i).focus();
await component.getByLabel(/verification method/i).type('vyper'); await component.getByLabel(/verification method/i).fill('vyper');
await page.getByRole('button', { name: /contract/i }).click(); await page.getByRole('button', { name: /contract/i }).click();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
...@@ -176,8 +203,9 @@ test('vyper multi-part method', async({ mount, page }) => { ...@@ -176,8 +203,9 @@ test('vyper multi-part method', async({ mount, page }) => {
{ hooksConfig }, { hooksConfig },
); );
// select method
await component.getByLabel(/verification method/i).focus(); await component.getByLabel(/verification method/i).focus();
await component.getByLabel(/verification method/i).type('vyper'); await component.getByLabel(/verification method/i).fill('vyper');
await page.getByRole('button', { name: /multi-part files/i }).click(); await page.getByRole('button', { name: /multi-part files/i }).click();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
...@@ -191,8 +219,9 @@ test('vyper vyper-standard-input method', async({ mount, page }) => { ...@@ -191,8 +219,9 @@ test('vyper vyper-standard-input method', async({ mount, page }) => {
{ hooksConfig }, { hooksConfig },
); );
// select method
await component.getByLabel(/verification method/i).focus(); await component.getByLabel(/verification method/i).focus();
await component.getByLabel(/verification method/i).type('vyper'); await component.getByLabel(/verification method/i).fill('vyper');
await page.getByRole('button', { name: /standard json input/i }).click(); await page.getByRole('button', { name: /standard json input/i }).click();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
......
...@@ -18,6 +18,7 @@ import useSocketChannel from 'lib/socket/useSocketChannel'; ...@@ -18,6 +18,7 @@ import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import ContractVerificationFieldAddress from './fields/ContractVerificationFieldAddress'; import ContractVerificationFieldAddress from './fields/ContractVerificationFieldAddress';
import ContractVerificationFieldLicenseType from './fields/ContractVerificationFieldLicenseType';
import ContractVerificationFieldMethod from './fields/ContractVerificationFieldMethod'; import ContractVerificationFieldMethod from './fields/ContractVerificationFieldMethod';
import ContractVerificationFlattenSourceCode from './methods/ContractVerificationFlattenSourceCode'; import ContractVerificationFlattenSourceCode from './methods/ContractVerificationFlattenSourceCode';
import ContractVerificationMultiPartFile from './methods/ContractVerificationMultiPartFile'; import ContractVerificationMultiPartFile from './methods/ContractVerificationMultiPartFile';
...@@ -37,7 +38,7 @@ interface Props { ...@@ -37,7 +38,7 @@ interface Props {
const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Props) => { const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Props) => {
const formApi = useForm<FormFields>({ const formApi = useForm<FormFields>({
mode: 'onBlur', mode: 'onBlur',
defaultValues: methodFromQuery ? getDefaultValues(methodFromQuery, config, hash) : undefined, defaultValues: methodFromQuery ? getDefaultValues(methodFromQuery, config, hash, null) : undefined,
}); });
const { control, handleSubmit, watch, formState, setError, reset, getFieldState } = formApi; const { control, handleSubmit, watch, formState, setError, reset, getFieldState } = formApi;
const submitPromiseResolver = React.useRef<(value: unknown) => void>(); const submitPromiseResolver = React.useRef<(value: unknown) => void>();
...@@ -161,12 +162,13 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -161,12 +162,13 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
}; };
}, [ config ]); }, [ config ]);
const method = watch('method'); const method = watch('method');
const licenseType = watch('license_type');
const content = methods[method?.value] || null; const content = methods[method?.value] || null;
const methodValue = method?.value; const methodValue = method?.value;
useUpdateEffect(() => { useUpdateEffect(() => {
if (methodValue) { if (methodValue) {
reset(getDefaultValues(methodValue, config, address || hash)); reset(getDefaultValues(methodValue, config, hash || address, licenseType));
const methodName = METHOD_LABELS[methodValue]; const methodName = METHOD_LABELS[methodValue];
mixpanel.logEvent(mixpanel.EventTypes.CONTRACT_VERIFICATION, { Status: 'Method selected', Method: methodName }); mixpanel.logEvent(mixpanel.EventTypes.CONTRACT_VERIFICATION, { Status: 'Method selected', Method: methodName });
...@@ -183,6 +185,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -183,6 +185,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
> >
<Grid as="section" columnGap="30px" rowGap={{ base: 2, lg: 5 }} templateColumns={{ base: '1fr', lg: 'minmax(auto, 680px) minmax(0, 340px)' }}> <Grid as="section" columnGap="30px" rowGap={{ base: 2, lg: 5 }} templateColumns={{ base: '1fr', lg: 'minmax(auto, 680px) minmax(0, 340px)' }}>
{ !hash && <ContractVerificationFieldAddress/> } { !hash && <ContractVerificationFieldAddress/> }
<ContractVerificationFieldLicenseType/>
<ContractVerificationFieldMethod <ContractVerificationFieldMethod
control={ control } control={ control }
methods={ config.verification_options } methods={ config.verification_options }
......
...@@ -42,7 +42,7 @@ const ContractVerificationFieldAddress = ({ isReadOnly }: Props) => { ...@@ -42,7 +42,7 @@ const ContractVerificationFieldAddress = ({ isReadOnly }: Props) => {
Contract address to verify Contract address to verify
</chakra.span> </chakra.span>
</ContractVerificationFormRow> </ContractVerificationFormRow>
<ContractVerificationFormRow mb={ 3 }> <ContractVerificationFormRow>
<Controller <Controller
name="address" name="address"
control={ control } control={ control }
......
import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types';
import { CONTRACT_LICENSES } from 'lib/contracts/licenses';
import useIsMobile from 'lib/hooks/useIsMobile';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
const options = CONTRACT_LICENSES.map(({ label, title, type }) => ({ label: `${ title } (${ label })`, value: type }));
import ContractVerificationFormRow from '../ContractVerificationFormRow';
const ContractVerificationFieldLicenseType = () => {
const { formState, control } = useFormContext<FormFields>();
const isMobile = useIsMobile();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'license_type'>}) => {
const error = 'license_type' in formState.errors ? formState.errors.license_type : undefined;
return (
<FancySelect
{ ...field }
options={ options }
size={ isMobile ? 'md' : 'lg' }
placeholder="Contract license"
isDisabled={ formState.isSubmitting }
error={ error }
/>
);
}, [ formState.errors, formState.isSubmitting, isMobile ]);
return (
<ContractVerificationFormRow>
<Controller
name="license_type"
control={ control }
render={ renderControl }
/>
<span>
For best practices, all contract source code holders, publishers and authors are encouraged to also
specify the accompanying license for their verified contract source code provided.
</span>
</ContractVerificationFormRow>
);
};
export default React.memo(ContractVerificationFieldLicenseType);
...@@ -11,6 +11,7 @@ import { ...@@ -11,6 +11,7 @@ import {
DarkMode, DarkMode,
ListItem, ListItem,
OrderedList, OrderedList,
Box,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps, Control } from 'react-hook-form'; import type { ControllerRenderProps, Control } from 'react-hook-form';
...@@ -97,7 +98,7 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props ...@@ -97,7 +98,7 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props
return ( return (
<> <>
<div> <Box mt={{ base: 10, lg: 6 }} gridColumn={{ lg: '1 / 3' }}>
<chakra.span fontWeight={ 500 } fontSize="lg" fontFamily="heading"> <chakra.span fontWeight={ 500 } fontSize="lg" fontFamily="heading">
Currently, Blockscout supports { methods.length } contract verification methods Currently, Blockscout supports { methods.length } contract verification methods
</chakra.span> </chakra.span>
...@@ -121,8 +122,7 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props ...@@ -121,8 +122,7 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props
</PopoverContent> </PopoverContent>
</Portal> </Portal>
</Popover> </Popover>
</div> </Box>
<div/>
<Controller <Controller
name="method" name="method"
control={ control } control={ control }
......
import type { SmartContractVerificationMethod } from 'types/api/contract'; import type { SmartContractLicenseType, SmartContractVerificationMethod } from 'types/api/contract';
import type { Option } from 'ui/shared/FancySelect/types'; import type { Option } from 'ui/shared/FancySelect/types';
export interface ContractLibrary { export interface ContractLibrary {
...@@ -11,6 +11,11 @@ interface MethodOption { ...@@ -11,6 +11,11 @@ interface MethodOption {
value: SmartContractVerificationMethod; value: SmartContractVerificationMethod;
} }
export interface LicenseOption {
label: string;
value: SmartContractLicenseType;
}
export interface FormFieldsFlattenSourceCode { export interface FormFieldsFlattenSourceCode {
address: string; address: string;
method: MethodOption; method: MethodOption;
...@@ -24,6 +29,7 @@ export interface FormFieldsFlattenSourceCode { ...@@ -24,6 +29,7 @@ export interface FormFieldsFlattenSourceCode {
autodetect_constructor_args: boolean; autodetect_constructor_args: boolean;
constructor_args: string; constructor_args: string;
libraries: Array<ContractLibrary>; libraries: Array<ContractLibrary>;
license_type: LicenseOption | null;
} }
export interface FormFieldsStandardInput { export interface FormFieldsStandardInput {
...@@ -34,6 +40,7 @@ export interface FormFieldsStandardInput { ...@@ -34,6 +40,7 @@ export interface FormFieldsStandardInput {
sources: Array<File>; sources: Array<File>;
autodetect_constructor_args: boolean; autodetect_constructor_args: boolean;
constructor_args: string; constructor_args: string;
license_type: LicenseOption | null;
} }
export interface FormFieldsSourcify { export interface FormFieldsSourcify {
...@@ -41,6 +48,7 @@ export interface FormFieldsSourcify { ...@@ -41,6 +48,7 @@ export interface FormFieldsSourcify {
method: MethodOption; method: MethodOption;
sources: Array<File>; sources: Array<File>;
contract_index?: Option; contract_index?: Option;
license_type: LicenseOption | null;
} }
export interface FormFieldsMultiPartFile { export interface FormFieldsMultiPartFile {
...@@ -52,6 +60,7 @@ export interface FormFieldsMultiPartFile { ...@@ -52,6 +60,7 @@ export interface FormFieldsMultiPartFile {
optimization_runs: string; optimization_runs: string;
sources: Array<File>; sources: Array<File>;
libraries: Array<ContractLibrary>; libraries: Array<ContractLibrary>;
license_type: LicenseOption | null;
} }
export interface FormFieldsVyperContract { export interface FormFieldsVyperContract {
...@@ -62,6 +71,7 @@ export interface FormFieldsVyperContract { ...@@ -62,6 +71,7 @@ export interface FormFieldsVyperContract {
compiler: Option | null; compiler: Option | null;
code: string; code: string;
constructor_args: string | undefined; constructor_args: string | undefined;
license_type: LicenseOption | null;
} }
export interface FormFieldsVyperMultiPartFile { export interface FormFieldsVyperMultiPartFile {
...@@ -71,6 +81,7 @@ export interface FormFieldsVyperMultiPartFile { ...@@ -71,6 +81,7 @@ export interface FormFieldsVyperMultiPartFile {
evm_version: Option | null; evm_version: Option | null;
sources: Array<File>; sources: Array<File>;
interfaces: Array<File>; interfaces: Array<File>;
license_type: LicenseOption | null;
} }
export interface FormFieldsVyperStandardInput { export interface FormFieldsVyperStandardInput {
...@@ -78,6 +89,7 @@ export interface FormFieldsVyperStandardInput { ...@@ -78,6 +89,7 @@ export interface FormFieldsVyperStandardInput {
method: MethodOption; method: MethodOption;
compiler: Option | null; compiler: Option | null;
sources: Array<File>; sources: Array<File>;
license_type: LicenseOption | null;
} }
export type FormFields = FormFieldsFlattenSourceCode | FormFieldsStandardInput | FormFieldsSourcify | export type FormFields = FormFieldsFlattenSourceCode | FormFieldsStandardInput | FormFieldsSourcify |
......
...@@ -11,7 +11,12 @@ import type { ...@@ -11,7 +11,12 @@ import type {
FormFieldsVyperMultiPartFile, FormFieldsVyperMultiPartFile,
FormFieldsVyperStandardInput, FormFieldsVyperStandardInput,
} from './types'; } from './types';
import type { SmartContractVerificationMethod, SmartContractVerificationError, SmartContractVerificationConfig } from 'types/api/contract'; import type {
SmartContractVerificationMethod,
SmartContractVerificationError,
SmartContractVerificationConfig,
SmartContractLicenseType,
} from 'types/api/contract';
import type { Params as FetchParams } from 'lib/hooks/useFetch'; import type { Params as FetchParams } from 'lib/hooks/useFetch';
...@@ -52,6 +57,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields> ...@@ -52,6 +57,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
autodetect_constructor_args: true, autodetect_constructor_args: true,
constructor_args: '', constructor_args: '',
libraries: [], libraries: [],
license_type: null,
}, },
'standard-input': { 'standard-input': {
address: '', address: '',
...@@ -64,6 +70,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields> ...@@ -64,6 +70,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
sources: [], sources: [],
autodetect_constructor_args: true, autodetect_constructor_args: true,
constructor_args: '', constructor_args: '',
license_type: null,
}, },
sourcify: { sourcify: {
address: '', address: '',
...@@ -72,6 +79,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields> ...@@ -72,6 +79,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
label: METHOD_LABELS.sourcify, label: METHOD_LABELS.sourcify,
}, },
sources: [], sources: [],
license_type: null,
}, },
'multi-part': { 'multi-part': {
address: '', address: '',
...@@ -85,6 +93,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields> ...@@ -85,6 +93,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
optimization_runs: '200', optimization_runs: '200',
sources: [], sources: [],
libraries: [], libraries: [],
license_type: null,
}, },
'vyper-code': { 'vyper-code': {
address: '', address: '',
...@@ -97,6 +106,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields> ...@@ -97,6 +106,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
evm_version: null, evm_version: null,
code: '', code: '',
constructor_args: '', constructor_args: '',
license_type: null,
}, },
'vyper-multi-part': { 'vyper-multi-part': {
address: '', address: '',
...@@ -107,6 +117,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields> ...@@ -107,6 +117,7 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
compiler: null, compiler: null,
evm_version: null, evm_version: null,
sources: [], sources: [],
license_type: null,
}, },
'vyper-standard-input': { 'vyper-standard-input': {
address: '', address: '',
...@@ -116,11 +127,17 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields> ...@@ -116,11 +127,17 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
}, },
compiler: null, compiler: null,
sources: [], sources: [],
license_type: null,
}, },
}; };
export function getDefaultValues(method: SmartContractVerificationMethod, config: SmartContractVerificationConfig, hash?: string) { export function getDefaultValues(
const defaultValues = { ...DEFAULT_VALUES[method], address: hash }; method: SmartContractVerificationMethod,
config: SmartContractVerificationConfig,
hash: string | undefined,
licenseType: FormFields['license_type'],
) {
const defaultValues: FormFields = { ...DEFAULT_VALUES[method], address: hash || '', license_type: licenseType };
if ('evm_version' in defaultValues) { if ('evm_version' in defaultValues) {
if (method === 'flattened-code' || method === 'multi-part') { if (method === 'flattened-code' || method === 'multi-part') {
...@@ -162,6 +179,8 @@ export function sortVerificationMethods(methodA: SmartContractVerificationMethod ...@@ -162,6 +179,8 @@ export function sortVerificationMethods(methodA: SmartContractVerificationMethod
} }
export function prepareRequestBody(data: FormFields): FetchParams['body'] { export function prepareRequestBody(data: FormFields): FetchParams['body'] {
const defaultLicenseType: SmartContractLicenseType = 'none';
switch (data.method.value) { switch (data.method.value) {
case 'flattened-code': { case 'flattened-code': {
const _data = data as FormFieldsFlattenSourceCode; const _data = data as FormFieldsFlattenSourceCode;
...@@ -176,6 +195,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] { ...@@ -176,6 +195,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
evm_version: _data.evm_version?.value, evm_version: _data.evm_version?.value,
autodetect_constructor_args: _data.autodetect_constructor_args, autodetect_constructor_args: _data.autodetect_constructor_args,
constructor_args: _data.constructor_args, constructor_args: _data.constructor_args,
license_type: _data.license_type?.value ?? defaultLicenseType,
}; };
} }
...@@ -184,6 +204,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] { ...@@ -184,6 +204,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
const body = new FormData(); const body = new FormData();
_data.compiler && body.set('compiler_version', _data.compiler.value); _data.compiler && body.set('compiler_version', _data.compiler.value);
body.set('license_type', _data.license_type?.value ?? defaultLicenseType);
body.set('contract_name', _data.name); body.set('contract_name', _data.name);
body.set('autodetect_constructor_args', String(Boolean(_data.autodetect_constructor_args))); body.set('autodetect_constructor_args', String(Boolean(_data.autodetect_constructor_args)));
body.set('constructor_args', _data.constructor_args); body.set('constructor_args', _data.constructor_args);
...@@ -196,7 +217,8 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] { ...@@ -196,7 +217,8 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
const _data = data as FormFieldsSourcify; const _data = data as FormFieldsSourcify;
const body = new FormData(); const body = new FormData();
addFilesToFormData(body, _data.sources, 'files'); addFilesToFormData(body, _data.sources, 'files');
_data.contract_index && body.set('chosen_contract_index', _data.contract_index.value); body.set('chosen_contract_index', _data.contract_index?.value ?? defaultLicenseType);
_data.license_type && body.set('license_type', _data.license_type.value);
return body; return body;
} }
...@@ -207,6 +229,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] { ...@@ -207,6 +229,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
const body = new FormData(); const body = new FormData();
_data.compiler && body.set('compiler_version', _data.compiler.value); _data.compiler && body.set('compiler_version', _data.compiler.value);
_data.evm_version && body.set('evm_version', _data.evm_version.value); _data.evm_version && body.set('evm_version', _data.evm_version.value);
body.set('license_type', _data.license_type?.value ?? defaultLicenseType);
body.set('is_optimization_enabled', String(Boolean(_data.is_optimization_enabled))); body.set('is_optimization_enabled', String(Boolean(_data.is_optimization_enabled)));
_data.is_optimization_enabled && body.set('optimization_runs', _data.optimization_runs); _data.is_optimization_enabled && body.set('optimization_runs', _data.optimization_runs);
...@@ -226,6 +249,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] { ...@@ -226,6 +249,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
source_code: _data.code, source_code: _data.code,
contract_name: _data.name, contract_name: _data.name,
constructor_args: _data.constructor_args, constructor_args: _data.constructor_args,
license_type: _data.license_type?.value ?? defaultLicenseType,
}; };
} }
...@@ -235,6 +259,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] { ...@@ -235,6 +259,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
const body = new FormData(); const body = new FormData();
_data.compiler && body.set('compiler_version', _data.compiler.value); _data.compiler && body.set('compiler_version', _data.compiler.value);
_data.evm_version && body.set('evm_version', _data.evm_version.value); _data.evm_version && body.set('evm_version', _data.evm_version.value);
body.set('license_type', _data.license_type?.value ?? defaultLicenseType);
addFilesToFormData(body, _data.sources, 'files'); addFilesToFormData(body, _data.sources, 'files');
addFilesToFormData(body, _data.interfaces, 'interfaces'); addFilesToFormData(body, _data.interfaces, 'interfaces');
...@@ -246,6 +271,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] { ...@@ -246,6 +271,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
const body = new FormData(); const body = new FormData();
_data.compiler && body.set('compiler_version', _data.compiler.value); _data.compiler && body.set('compiler_version', _data.compiler.value);
body.set('license_type', _data.license_type?.value ?? defaultLicenseType);
addFilesToFormData(body, _data.sources, 'files'); addFilesToFormData(body, _data.sources, 'files');
return body; return body;
......
...@@ -5,6 +5,7 @@ import React from 'react'; ...@@ -5,6 +5,7 @@ import React from 'react';
import type { VerifiedContract } from 'types/api/contracts'; import type { VerifiedContract } from 'types/api/contracts';
import config from 'configs/app'; import config from 'configs/app';
import { CONTRACT_LICENSES } from 'lib/contracts/licenses';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import { currencyUnits } from 'lib/units'; import { currencyUnits } from 'lib/units';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
...@@ -23,6 +24,15 @@ const VerifiedContractsListItem = ({ data, isLoading }: Props) => { ...@@ -23,6 +24,15 @@ const VerifiedContractsListItem = ({ data, isLoading }: Props) => {
BigNumber(data.coin_balance).div(10 ** config.chain.currency.decimals).dp(6).toFormat() : BigNumber(data.coin_balance).div(10 ** config.chain.currency.decimals).dp(6).toFormat() :
'0'; '0';
const license = (() => {
const license = CONTRACT_LICENSES.find((license) => license.type === data.license_type);
if (!license || license.type === 'none') {
return '-';
}
return license.label;
})();
return ( return (
<ListItemMobile rowGap={ 3 }> <ListItemMobile rowGap={ 3 }>
<Flex w="100%"> <Flex w="100%">
...@@ -77,12 +87,12 @@ const VerifiedContractsListItem = ({ data, isLoading }: Props) => { ...@@ -77,12 +87,12 @@ const VerifiedContractsListItem = ({ data, isLoading }: Props) => {
</Skeleton> </Skeleton>
</Flex> </Flex>
</Flex> </Flex>
{ /* <Flex columnGap={ 3 }> <Flex columnGap={ 3 }>
<Box fontWeight={ 500 }>Market cap</Box> <Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>License</Skeleton>
<Box color="text_secondary"> <Skeleton isLoaded={ !isLoading } color="text_secondary">
N/A <span>{ license }</span>
</Box> </Skeleton>
</Flex> */ } </Flex>
</ListItemMobile> </ListItemMobile>
); );
}; };
......
...@@ -47,7 +47,7 @@ const VerifiedContractsTable = ({ data, sort, setSorting, isLoading }: Props) => ...@@ -47,7 +47,7 @@ const VerifiedContractsTable = ({ data, sort, setSorting, isLoading }: Props) =>
<Th width="50%">Compiler / version</Th> <Th width="50%">Compiler / version</Th>
<Th width="80px">Settings</Th> <Th width="80px">Settings</Th>
<Th width="150px">Verified</Th> <Th width="150px">Verified</Th>
{ /* <Th width="120px">Market cap</Th> */ } <Th width="130px">License</Th>
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
......
...@@ -5,6 +5,7 @@ import React from 'react'; ...@@ -5,6 +5,7 @@ import React from 'react';
import type { VerifiedContract } from 'types/api/contracts'; import type { VerifiedContract } from 'types/api/contracts';
import config from 'configs/app'; import config from 'configs/app';
import { CONTRACT_LICENSES } from 'lib/contracts/licenses';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
...@@ -21,6 +22,15 @@ const VerifiedContractsTableItem = ({ data, isLoading }: Props) => { ...@@ -21,6 +22,15 @@ const VerifiedContractsTableItem = ({ data, isLoading }: Props) => {
BigNumber(data.coin_balance).div(10 ** config.chain.currency.decimals).dp(6).toFormat() : BigNumber(data.coin_balance).div(10 ** config.chain.currency.decimals).dp(6).toFormat() :
'0'; '0';
const license = (() => {
const license = CONTRACT_LICENSES.find((license) => license.type === data.license_type);
if (!license || license.type === 'none') {
return '-';
}
return license.label;
})();
return ( return (
<Tr> <Tr>
<Td> <Td>
...@@ -51,8 +61,10 @@ const VerifiedContractsTableItem = ({ data, isLoading }: Props) => { ...@@ -51,8 +61,10 @@ const VerifiedContractsTableItem = ({ data, isLoading }: Props) => {
<Td> <Td>
<Flex flexWrap="wrap" columnGap={ 2 }> <Flex flexWrap="wrap" columnGap={ 2 }>
<Skeleton isLoaded={ !isLoading } textTransform="capitalize" my={ 1 }>{ data.language }</Skeleton> <Skeleton isLoaded={ !isLoading } textTransform="capitalize" my={ 1 }>{ data.language }</Skeleton>
<Skeleton isLoaded={ !isLoading } color="text_secondary" wordBreak="break-all" my={ 1 }> <Skeleton isLoaded={ !isLoading } color="text_secondary" wordBreak="break-all" my={ 1 } cursor="pointer">
<span>{ data.compiler_version }</span> <Tooltip label={ data.compiler_version }>
<span>{ data.compiler_version.split('+')[0] }</span>
</Tooltip>
</Skeleton> </Skeleton>
</Flex> </Flex>
</Td> </Td>
...@@ -80,9 +92,11 @@ const VerifiedContractsTableItem = ({ data, isLoading }: Props) => { ...@@ -80,9 +92,11 @@ const VerifiedContractsTableItem = ({ data, isLoading }: Props) => {
</Skeleton> </Skeleton>
</Flex> </Flex>
</Td> </Td>
{ /* <Td> <Td>
N/A <Skeleton isLoaded={ !isLoading } my={ 1 } display="inline-block">
</Td> */ } { license }
</Skeleton>
</Td>
</Tr> </Tr>
); );
}; };
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment