Commit b2c51065 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #919 from blockscout/feat/contract-code-language

feat: contract code language
parents 5f069ef3 5c087298
...@@ -10,6 +10,7 @@ export function monaco(): CspDev.DirectiveDescriptor { ...@@ -10,6 +10,7 @@ export function monaco(): CspDev.DirectiveDescriptor {
'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/editor/editor.main.js', 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/editor/editor.main.js',
'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/editor/editor.main.nls.js', 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/editor/editor.main.nls.js',
'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/basic-languages/solidity/solidity.js', 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/basic-languages/solidity/solidity.js',
'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/basic-languages/elixir/elixir.js',
'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/base/worker/workerMain.js', 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/base/worker/workerMain.js',
], ],
'style-src': [ 'style-src': [
......
...@@ -30,6 +30,7 @@ export const verified: Partial<SmartContract> = { ...@@ -30,6 +30,7 @@ export const verified: Partial<SmartContract> = {
{ address_hash: '0xa62744BeE8646e237441CDbfdedD3458861748A8', name: 'Sol' }, { address_hash: '0xa62744BeE8646e237441CDbfdedD3458861748A8', name: 'Sol' },
{ address_hash: '0xa62744BeE8646e237441CDbfdedD3458861748A8', name: 'math' }, { address_hash: '0xa62744BeE8646e237441CDbfdedD3458861748A8', name: 'math' },
], ],
language: 'solidity',
}; };
export const withMultiplePaths: Partial<SmartContract> = { export const withMultiplePaths: Partial<SmartContract> = {
......
...@@ -27,7 +27,6 @@ export interface SmartContract { ...@@ -27,7 +27,6 @@ export interface SmartContract {
constructor_args: string | null; constructor_args: string | null;
decoded_constructor_args: Array<SmartContractDecodedConstructorArg> | null; decoded_constructor_args: Array<SmartContractDecodedConstructorArg> | null;
can_be_visualized_via_sol2uml: boolean | null; can_be_visualized_via_sol2uml: boolean | null;
is_vyper_contract: boolean | null;
file_path: string; file_path: string;
additional_sources: Array<{ file_path: string; source_code: string }>; additional_sources: Array<{ file_path: string; source_code: string }>;
external_libraries: Array<SmartContractExternalLibrary> | null; external_libraries: Array<SmartContractExternalLibrary> | null;
...@@ -37,6 +36,7 @@ export interface SmartContract { ...@@ -37,6 +36,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;
} }
export type SmartContractDecodedConstructorArg = [ export type SmartContractDecodedConstructorArg = [
......
...@@ -22,7 +22,7 @@ export interface VerifiedContractsResponse { ...@@ -22,7 +22,7 @@ export interface VerifiedContractsResponse {
export interface VerifiedContractsFilters { export interface VerifiedContractsFilters {
q: string | undefined; q: string | undefined;
filter: 'vyper' | 'solidity' | undefined; filter: 'vyper' | 'solidity' | 'yul' | undefined;
} }
export type VerifiedContractsCounters = { export type VerifiedContractsCounters = {
......
...@@ -23,9 +23,19 @@ function getEditorData(contractInfo: SmartContract | undefined) { ...@@ -23,9 +23,19 @@ function getEditorData(contractInfo: SmartContract | undefined) {
return undefined; return undefined;
} }
const defaultName = contractInfo.is_vyper_contract ? '/index.vy' : '/index.sol'; const extension = (() => {
switch (contractInfo.language) {
case 'vyper':
return 'vy';
case 'yul':
return 'yul';
default:
return 'sol';
}
})();
return [ return [
{ file_path: formatFilePath(contractInfo.file_path || defaultName), source_code: contractInfo.source_code }, { file_path: formatFilePath(contractInfo.file_path || `index.${ extension }`), source_code: contractInfo.source_code },
...(contractInfo.additional_sources || []).map((source) => ({ ...source, file_path: formatFilePath(source.file_path) })), ...(contractInfo.additional_sources || []).map((source) => ({ ...source, file_path: formatFilePath(source.file_path) })),
]; ];
} }
...@@ -74,7 +84,7 @@ const ContractSourceCode = ({ address, implementationAddress }: Props) => { ...@@ -74,7 +84,7 @@ const ContractSourceCode = ({ address, implementationAddress }: Props) => {
const heading = ( const heading = (
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 }> <Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>
<span>Contract source code</span> <span>Contract source code</span>
<Text whiteSpace="pre" as="span" variant="secondary"> ({ activeContract?.is_vyper_contract ? 'Vyper' : 'Solidity' })</Text> <Text whiteSpace="pre" as="span" variant="secondary" textTransform="capitalize"> ({ activeContract?.language })</Text>
</Skeleton> </Skeleton>
); );
...@@ -132,11 +142,19 @@ const ContractSourceCode = ({ address, implementationAddress }: Props) => { ...@@ -132,11 +142,19 @@ const ContractSourceCode = ({ address, implementationAddress }: Props) => {
return ( return (
<> <>
<Box display={ sourceType === 'primary' ? 'block' : 'none' }> <Box display={ sourceType === 'primary' ? 'block' : 'none' }>
<CodeEditor data={ primaryEditorData } remappings={ primaryContractQuery.data?.compiler_settings?.remappings }/> <CodeEditor
data={ primaryEditorData }
remappings={ primaryContractQuery.data?.compiler_settings?.remappings }
language={ primaryContractQuery.data?.language ?? undefined }
/>
</Box> </Box>
{ secondaryEditorData && ( { secondaryEditorData && (
<Box display={ sourceType === 'secondary' ? 'block' : 'none' }> <Box display={ sourceType === 'secondary' ? 'block' : 'none' }>
<CodeEditor data={ secondaryEditorData } remappings={ secondaryContractQuery.data?.compiler_settings?.remappings }/> <CodeEditor
data={ secondaryEditorData }
remappings={ secondaryContractQuery.data?.compiler_settings?.remappings }
language={ secondaryContractQuery.data?.language ?? undefined }
/>
</Box> </Box>
) } ) }
</> </>
......
...@@ -61,14 +61,10 @@ const VerifiedContracts = () => { ...@@ -61,14 +61,10 @@ const VerifiedContracts = () => {
return; return;
} }
if ((value === 'vyper' || value === 'solidity')) { const filter = value === 'all' ? undefined : value as VerifiedContractsFilters['filter'];
onFilterChange({ q: debouncedSearchTerm, filter: value });
setType(value);
return;
}
onFilterChange({ q: debouncedSearchTerm, filter: undefined }); onFilterChange({ q: debouncedSearchTerm, filter });
setType(undefined); setType(filter);
}, [ debouncedSearchTerm, onFilterChange ]); }, [ debouncedSearchTerm, onFilterChange ]);
const handleSortToggle = React.useCallback((field: SortField) => { const handleSortToggle = React.useCallback((field: SortField) => {
......
...@@ -36,9 +36,10 @@ const EDITOR_HEIGHT = 500; ...@@ -36,9 +36,10 @@ const EDITOR_HEIGHT = 500;
interface Props { interface Props {
data: Array<File>; data: Array<File>;
remappings?: Array<string>; remappings?: Array<string>;
language?: string;
} }
const CodeEditor = ({ data, remappings }: Props) => { const CodeEditor = ({ data, remappings, language }: Props) => {
const [ instance, setInstance ] = React.useState<Monaco | undefined>(); const [ instance, setInstance ] = React.useState<Monaco | undefined>();
const [ editor, setEditor ] = React.useState<monaco.editor.IStandaloneCodeEditor | undefined>(); const [ editor, setEditor ] = React.useState<monaco.editor.IStandaloneCodeEditor | undefined>();
const [ index, setIndex ] = React.useState(0); const [ index, setIndex ] = React.useState(0);
...@@ -53,6 +54,8 @@ const CodeEditor = ({ data, remappings }: Props) => { ...@@ -53,6 +54,8 @@ const CodeEditor = ({ data, remappings }: Props) => {
const editorWidth = containerRect ? containerRect.width - (isMobile ? 0 : SIDE_BAR_WIDTH) : 0; const editorWidth = containerRect ? containerRect.width - (isMobile ? 0 : SIDE_BAR_WIDTH) : 0;
const editorLanguage = language === 'vyper' ? 'elixir' : 'sol';
React.useEffect(() => { React.useEffect(() => {
instance?.editor.setTheme(colorMode === 'light' ? 'blockscout-light' : 'blockscout-dark'); instance?.editor.setTheme(colorMode === 'light' ? 'blockscout-light' : 'blockscout-dark');
}, [ colorMode, instance?.editor ]); }, [ colorMode, instance?.editor ]);
...@@ -69,7 +72,7 @@ const CodeEditor = ({ data, remappings }: Props) => { ...@@ -69,7 +72,7 @@ const CodeEditor = ({ data, remappings }: Props) => {
const loadedModelsPaths = loadedModels.map((model) => model.uri.path); const loadedModelsPaths = loadedModels.map((model) => model.uri.path);
const newModels = data.slice(1) const newModels = data.slice(1)
.filter((file) => !loadedModelsPaths.includes(file.file_path)) .filter((file) => !loadedModelsPaths.includes(file.file_path))
.map((file) => monaco.editor.createModel(file.source_code, 'sol', monaco.Uri.parse(file.file_path))); .map((file) => monaco.editor.createModel(file.source_code, editorLanguage, monaco.Uri.parse(file.file_path)));
loadedModels.concat(newModels).forEach(addFileImportDecorations); loadedModels.concat(newModels).forEach(addFileImportDecorations);
...@@ -185,7 +188,7 @@ const CodeEditor = ({ data, remappings }: Props) => { ...@@ -185,7 +188,7 @@ const CodeEditor = ({ data, remappings }: Props) => {
return ( return (
<Box overflow="hidden" borderRadius="md" height={ `${ EDITOR_HEIGHT }px` }> <Box overflow="hidden" borderRadius="md" height={ `${ EDITOR_HEIGHT }px` }>
<MonacoEditor <MonacoEditor
language="sol" language={ editorLanguage }
path={ data[index].file_path } path={ data[index].file_path }
defaultValue={ data[index].source_code } defaultValue={ data[index].source_code }
options={ EDITOR_OPTIONS } options={ EDITOR_OPTIONS }
...@@ -216,7 +219,7 @@ const CodeEditor = ({ data, remappings }: Props) => { ...@@ -216,7 +219,7 @@ const CodeEditor = ({ data, remappings }: Props) => {
<MonacoEditor <MonacoEditor
className="editor-container" className="editor-container"
height={ `${ EDITOR_HEIGHT }px` } height={ `${ EDITOR_HEIGHT }px` }
language="sol" language={ editorLanguage }
path={ data[index].file_path } path={ data[index].file_path }
defaultValue={ data[index].source_code } defaultValue={ data[index].source_code }
options={ EDITOR_OPTIONS } options={ EDITOR_OPTIONS }
......
...@@ -35,6 +35,7 @@ const VerifiedContractsFilter = ({ onChange, defaultValue, isActive }: Props) => ...@@ -35,6 +35,7 @@ const VerifiedContractsFilter = ({ onChange, defaultValue, isActive }: Props) =>
<MenuItemOption value="all">All</MenuItemOption> <MenuItemOption value="all">All</MenuItemOption>
<MenuItemOption value="solidity">Solidity</MenuItemOption> <MenuItemOption value="solidity">Solidity</MenuItemOption>
<MenuItemOption value="vyper">Vyper</MenuItemOption> <MenuItemOption value="vyper">Vyper</MenuItemOption>
<MenuItemOption value="yul">Yul</MenuItemOption>
</MenuOptionGroup> </MenuOptionGroup>
</MenuList> </MenuList>
</Menu> </Menu>
......
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