Commit 93bb45df authored by tom's avatar tom

submit form to api

parent 81c3dac7
...@@ -13,9 +13,10 @@ export default function fetchFactory( ...@@ -13,9 +13,10 @@ export default function fetchFactory(
// first arg can be only a string // first arg can be only a string
// FIXME migrate to RequestInfo later if needed // FIXME migrate to RequestInfo later if needed
return function fetch(url: string, init?: RequestInit): Promise<Response> { return function fetch(url: string, init?: RequestInit): Promise<Response> {
const incomingContentType = _req.headers['content-type'];
const headers = { const headers = {
accept: 'application/json', accept: 'application/json',
'content-type': 'application/json', 'content-type': incomingContentType?.match(/^multipart\/form-data/) ? incomingContentType : 'application/json',
cookie: `${ cookies.NAMES.API_TOKEN }=${ _req.cookies[cookies.NAMES.API_TOKEN] }`, cookie: `${ cookies.NAMES.API_TOKEN }=${ _req.cookies[cookies.NAMES.API_TOKEN] }`,
}; };
...@@ -25,10 +26,23 @@ export default function fetchFactory( ...@@ -25,10 +26,23 @@ export default function fetchFactory(
req: _req, req: _req,
}); });
const body = (() => {
const _body = init?.body;
if (!_body) {
return;
}
if (typeof _body === 'string') {
return _body;
}
return JSON.stringify(_body);
})();
return nodeFetch(url, { return nodeFetch(url, {
...init, ...init,
headers, headers,
body: init?.body ? JSON.stringify(init.body) : undefined, body,
}); });
}; };
} }
...@@ -207,6 +207,9 @@ export const RESOURCES = { ...@@ -207,6 +207,9 @@ export const RESOURCES = {
contract_verification_config: { contract_verification_config: {
path: '/api/v2/smart-contracts/verification/config', path: '/api/v2/smart-contracts/verification/config',
}, },
contract_verification_via: {
path: '/api/v2/smart-contracts/:id/verification/via/:method',
},
// TOKEN // TOKEN
token: { token: {
......
...@@ -11,7 +11,7 @@ export interface Params { ...@@ -11,7 +11,7 @@ export interface Params {
method?: RequestInit['method']; method?: RequestInit['method'];
headers?: RequestInit['headers']; headers?: RequestInit['headers'];
signal?: RequestInit['signal']; signal?: RequestInit['signal'];
body?: Record<string, unknown>; body?: Record<string, unknown> | FormData;
credentials?: RequestCredentials; credentials?: RequestCredentials;
} }
...@@ -20,13 +20,27 @@ export default function useFetch() { ...@@ -20,13 +20,27 @@ export default function useFetch() {
const { token } = queryClient.getQueryData<CsrfData>(getResourceKey('csrf')) || {}; const { token } = queryClient.getQueryData<CsrfData>(getResourceKey('csrf')) || {};
return React.useCallback(<Success, Error>(path: string, params?: Params): Promise<Success | ResourceError<Error>> => { return React.useCallback(<Success, Error>(path: string, params?: Params): Promise<Success | ResourceError<Error>> => {
const hasBody = params?.method && ![ 'GET', 'HEAD' ].includes(params.method); const _body = params?.body;
const isFormData = _body instanceof FormData;
const isBodyAllowed = params?.method && ![ 'GET', 'HEAD' ].includes(params.method);
const body: FormData | string | undefined = (() => {
if (!isBodyAllowed) {
return;
}
if (isFormData) {
return _body;
}
return JSON.stringify({ ..._body, _csrf_token: token });
})();
const reqParams = { const reqParams = {
...params, ...params,
body: hasBody ? JSON.stringify({ ...params.body, _csrf_token: token }) : undefined, body,
headers: { headers: {
...(hasBody ? { 'Content-type': 'application/json' } : undefined), ...(isBodyAllowed && !isFormData ? { 'Content-type': 'application/json' } : undefined),
...params?.headers, ...params?.headers,
}, },
}; };
......
// https://hexdocs.pm/phoenix/js/
import type { SocketConnectOption } from 'phoenix'; import type { SocketConnectOption } from 'phoenix';
import { Socket } from 'phoenix'; import { Socket } from 'phoenix';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
......
...@@ -19,6 +19,7 @@ SocketMessage.AddressTxs | ...@@ -19,6 +19,7 @@ SocketMessage.AddressTxs |
SocketMessage.AddressTxsPending | SocketMessage.AddressTxsPending |
SocketMessage.AddressTokenTransfer | SocketMessage.AddressTokenTransfer |
SocketMessage.TokenTransfers | SocketMessage.TokenTransfers |
SocketMessage.ContractVerification |
SocketMessage.Unknown; SocketMessage.Unknown;
interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> { interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> {
...@@ -43,6 +44,7 @@ export namespace SocketMessage { ...@@ -43,6 +44,7 @@ export namespace SocketMessage {
export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>; export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>;
export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>; export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>;
export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfer: TokenTransfer }>; export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfer: TokenTransfer }>;
export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }> export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }>;
export type ContractVerification = SocketMessageParamsGeneric<'verification', unknown>;
export type Unknown = SocketMessageParamsGeneric<undefined, unknown>; export type Unknown = SocketMessageParamsGeneric<undefined, unknown>;
} }
...@@ -60,7 +60,11 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on ...@@ -60,7 +60,11 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on
} else { } else {
ch = socket.channel(topic); ch = socket.channel(topic);
CHANNEL_REGISTRY[topic] = ch; CHANNEL_REGISTRY[topic] = ch;
ch.join().receive('ok', (message) => onJoinRef.current?.(ch, message)); ch.join()
.receive('ok', (message) => onJoinRef.current?.(ch, message))
.receive('error', () => {
onSocketError?.();
});
} }
setChannel(ch); setChannel(ch);
...@@ -70,7 +74,7 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on ...@@ -70,7 +74,7 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on
delete CHANNEL_REGISTRY[topic]; delete CHANNEL_REGISTRY[topic];
setChannel(undefined); setChannel(undefined);
}; };
}, [ socket, topic, params, isDisabled ]); }, [ socket, topic, params, isDisabled, onSocketError ]);
return channel; return channel;
} }
...@@ -116,7 +116,7 @@ export type SmartContractQueryMethodRead = SmartContractQueryMethodReadSuccess | ...@@ -116,7 +116,7 @@ export type SmartContractQueryMethodRead = SmartContractQueryMethodReadSuccess |
// VERIFICATION // VERIFICATION
export type SmartContractVerificationMethod = 'flattened_code' | 'standard_input' | 'sourcify' | 'multi_part' | 'vyper_multi_part'; export type SmartContractVerificationMethod = 'flattened_code' | 'standard_input' | 'sourcify' | 'multi_part' | 'vyper_code';
export interface SmartContractVerificationConfigRaw { export interface SmartContractVerificationConfigRaw {
solidity_compiler_versions: Array<string>; solidity_compiler_versions: Array<string>;
......
...@@ -13,6 +13,7 @@ const hooksConfig = { ...@@ -13,6 +13,7 @@ const hooksConfig = {
}, },
}; };
const hash = '0x2F99338637F027CFB7494E46B49987457beCC6E3';
const formConfig: SmartContractVerificationConfig = { const formConfig: SmartContractVerificationConfig = {
solidity_compiler_versions: [ solidity_compiler_versions: [
'v0.8.17+commit.8df45f5f', 'v0.8.17+commit.8df45f5f',
...@@ -32,7 +33,7 @@ const formConfig: SmartContractVerificationConfig = { ...@@ -32,7 +33,7 @@ const formConfig: SmartContractVerificationConfig = {
'standard_input', 'standard_input',
'sourcify', 'sourcify',
'multi_part', 'multi_part',
'vyper_multi_part', 'vyper_code',
], ],
vyper_compiler_versions: [ vyper_compiler_versions: [
'v0.3.7+commit.6020b8bb', 'v0.3.7+commit.6020b8bb',
...@@ -54,7 +55,7 @@ const formConfig: SmartContractVerificationConfig = { ...@@ -54,7 +55,7 @@ const formConfig: SmartContractVerificationConfig = {
test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) => { test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractVerificationForm config={ formConfig }/> <ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -70,7 +71,7 @@ test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) = ...@@ -70,7 +71,7 @@ test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) =
test('standard input json method', async({ mount, page }) => { test('standard input json method', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractVerificationForm config={ formConfig }/> <ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -83,7 +84,7 @@ test('standard input json method', async({ mount, page }) => { ...@@ -83,7 +84,7 @@ test('standard input json method', async({ mount, page }) => {
test('sourcify method +@dark-mode +@mobile', async({ mount, page }) => { test('sourcify method +@dark-mode +@mobile', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractVerificationForm config={ formConfig }/> <ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -102,7 +103,7 @@ test('sourcify method +@dark-mode +@mobile', async({ mount, page }) => { ...@@ -102,7 +103,7 @@ test('sourcify method +@dark-mode +@mobile', async({ mount, page }) => {
test('multi-part files method', async({ mount, page }) => { test('multi-part files method', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractVerificationForm config={ formConfig }/> <ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -115,7 +116,7 @@ test('multi-part files method', async({ mount, page }) => { ...@@ -115,7 +116,7 @@ test('multi-part files method', async({ mount, page }) => {
test('vyper contract method', async({ mount, page }) => { test('vyper contract method', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractVerificationForm config={ formConfig }/> <ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
......
...@@ -4,9 +4,12 @@ import type { SubmitHandler } from 'react-hook-form'; ...@@ -4,9 +4,12 @@ import type { SubmitHandler } from 'react-hook-form';
import { useForm, FormProvider } from 'react-hook-form'; import { useForm, FormProvider } from 'react-hook-form';
import type { FormFields } from './types'; import type { FormFields } from './types';
import type { SocketMessage } from 'lib/socket/types';
import type { SmartContractVerificationMethod, SmartContractVerificationConfig } from 'types/api/contract'; import type { SmartContractVerificationMethod, SmartContractVerificationConfig } from 'types/api/contract';
import delay from 'lib/delay'; import useApiFetch from 'lib/api/useApiFetch';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import ContractVerificationFieldMethod from './fields/ContractVerificationFieldMethod'; import ContractVerificationFieldMethod from './fields/ContractVerificationFieldMethod';
import ContractVerificationFlattenSourceCode from './methods/ContractVerificationFlattenSourceCode'; import ContractVerificationFlattenSourceCode from './methods/ContractVerificationFlattenSourceCode';
...@@ -14,21 +17,23 @@ import ContractVerificationMultiPartFile from './methods/ContractVerificationMul ...@@ -14,21 +17,23 @@ import ContractVerificationMultiPartFile from './methods/ContractVerificationMul
import ContractVerificationSourcify from './methods/ContractVerificationSourcify'; import ContractVerificationSourcify from './methods/ContractVerificationSourcify';
import ContractVerificationStandardInput from './methods/ContractVerificationStandardInput'; import ContractVerificationStandardInput from './methods/ContractVerificationStandardInput';
import ContractVerificationVyperContract from './methods/ContractVerificationVyperContract'; import ContractVerificationVyperContract from './methods/ContractVerificationVyperContract';
import { prepareRequestBody, METHOD_TO_ENDPOINT_MAP } from './utils';
const METHOD_COMPONENTS = { const METHOD_COMPONENTS = {
flattened_code: <ContractVerificationFlattenSourceCode/>, flattened_code: <ContractVerificationFlattenSourceCode/>,
standard_input: <ContractVerificationStandardInput/>, standard_input: <ContractVerificationStandardInput/>,
sourcify: <ContractVerificationSourcify/>, sourcify: <ContractVerificationSourcify/>,
multi_part: <ContractVerificationMultiPartFile/>, multi_part: <ContractVerificationMultiPartFile/>,
vyper_multi_part: <ContractVerificationVyperContract/>, vyper_code: <ContractVerificationVyperContract/>,
}; };
interface Props { interface Props {
method?: SmartContractVerificationMethod; method?: SmartContractVerificationMethod;
config: SmartContractVerificationConfig; config: SmartContractVerificationConfig;
hash: string;
} }
const ContractVerificationForm = ({ method: methodFromQuery, config }: Props) => { const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Props) => {
const formApi = useForm<FormFields>({ const formApi = useForm<FormFields>({
mode: 'onBlur', mode: 'onBlur',
defaultValues: { defaultValues: {
...@@ -36,15 +41,55 @@ const ContractVerificationForm = ({ method: methodFromQuery, config }: Props) => ...@@ -36,15 +41,55 @@ const ContractVerificationForm = ({ method: methodFromQuery, config }: Props) =>
}, },
}); });
const { control, handleSubmit, watch, formState } = formApi; const { control, handleSubmit, watch, formState } = formApi;
const submitPromiseResolver = React.useRef<(value: unknown) => void>();
const apiFetch = useApiFetch();
const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => { const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('__>__', data); console.log('__>__', data);
await delay(5_000);
const body = prepareRequestBody(data);
try {
await apiFetch('contract_verification_via', {
pathParams: { method: METHOD_TO_ENDPOINT_MAP[data.method], id: hash },
fetchParams: {
method: 'POST',
body,
},
});
} catch (error) {
return;
}
return new Promise((resolve) => {
submitPromiseResolver.current = resolve;
});
}, [ apiFetch, hash ]);
const handleNewSocketMessage: SocketMessage.ContractVerification['handler'] = React.useCallback((payload) => {
// eslint-disable-next-line no-console
console.log('__>__', payload);
}, []); }, []);
const method = watch('method'); const handleSocketError = React.useCallback(() => {
submitPromiseResolver.current?.(null);
}, []);
const channel = useSocketChannel({
topic: `address:${ hash }`,
onSocketClose: handleSocketError,
onSocketError: handleSocketError,
isDisabled: !formState.isSubmitting,
});
useSocketMessage({
channel,
event: 'verification',
handler: handleNewSocketMessage,
});
const method = watch('method');
const content = METHOD_COMPONENTS[method] || null; const content = METHOD_COMPONENTS[method] || null;
return ( return (
......
...@@ -87,7 +87,7 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props ...@@ -87,7 +87,7 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props
); );
case 'multi_part': case 'multi_part':
return 'Via multi-part files'; return 'Via multi-part files';
case 'vyper_multi_part': case 'vyper_code':
return 'Vyper contract'; return 'Vyper contract';
default: default:
......
...@@ -22,6 +22,7 @@ export interface FormFieldsStandardInput { ...@@ -22,6 +22,7 @@ export interface FormFieldsStandardInput {
name: string; name: string;
compiler: Option; compiler: Option;
sources: Array<File>; sources: Array<File>;
constructor_args: boolean;
} }
export interface FormFieldsSourcify { export interface FormFieldsSourcify {
...@@ -36,10 +37,11 @@ export interface FormFieldsMultiPartFile { ...@@ -36,10 +37,11 @@ export interface FormFieldsMultiPartFile {
is_optimization_enabled: boolean; is_optimization_enabled: boolean;
optimization_runs: string; optimization_runs: string;
sources: Array<File>; sources: Array<File>;
libraries: Array<ContractLibrary>;
} }
export interface FormFieldsVyperContract { export interface FormFieldsVyperContract {
method: 'vyper_multi_part'; method: 'vyper_code';
name: string; name: string;
compiler: Option; compiler: Option;
code: string; code: string;
......
import type { ContractLibrary, FormFields } from './types';
import type { SmartContractVerificationMethod } from 'types/api/contract'; import type { SmartContractVerificationMethod } from 'types/api/contract';
import type { Params as FetchParams } from 'lib/hooks/useFetch';
export const SUPPORTED_VERIFICATION_METHODS: Array<SmartContractVerificationMethod> = [ export const SUPPORTED_VERIFICATION_METHODS: Array<SmartContractVerificationMethod> = [
'flattened_code', 'flattened_code',
'standard_input', 'standard_input',
'sourcify', 'sourcify',
'multi_part', 'multi_part',
'vyper_multi_part', 'vyper_code',
]; ];
export const METHOD_TO_ENDPOINT_MAP: Record<SmartContractVerificationMethod, string> = {
flattened_code: 'flattened-code',
standard_input: 'standard-input',
sourcify: 'sourcify',
multi_part: 'multi-part',
vyper_code: 'vyper-code',
};
export function isValidVerificationMethod(method?: string): method is SmartContractVerificationMethod { export function isValidVerificationMethod(method?: string): method is SmartContractVerificationMethod {
return method && SUPPORTED_VERIFICATION_METHODS.includes(method as SmartContractVerificationMethod) ? true : false; return method && SUPPORTED_VERIFICATION_METHODS.includes(method as SmartContractVerificationMethod) ? true : false;
} }
export function sortVerificationMethods(methodA: SmartContractVerificationMethod, methodB: SmartContractVerificationMethod) {
const indexA = SUPPORTED_VERIFICATION_METHODS.indexOf(methodA);
const indexB = SUPPORTED_VERIFICATION_METHODS.indexOf(methodB);
if (indexA > indexB) {
return 1;
}
if (indexA < indexB) {
return -1;
}
return 0;
}
export function prepareRequestBody(data: FormFields): FetchParams['body'] {
switch (data.method) {
case 'flattened_code': {
return {
compiler_version: data.compiler.value,
source_code: data.code,
is_optimization_enabled: data.is_optimization_enabled,
optimization_runs: data.optimization_runs,
contract_name: data.name,
libraries: reduceLibrariesArray(data.libraries),
evm_version: data.evm_version.value,
autodetect_constructor_args: data.constructor_args,
};
}
case 'standard_input': {
const body = new FormData();
body.set('compiler_version', data.compiler.value);
body.set('contract_name', data.name);
body.set('autodetect_constructor_args', String(Boolean(data.constructor_args)));
addFilesToFormData(body, data.sources);
return body;
}
case 'sourcify': {
const body = new FormData();
addFilesToFormData(body, data.sources);
return body;
}
case 'multi_part': {
const body = new FormData();
body.set('compiler_version', data.compiler.value);
body.set('evm_version', data.evm_version.value);
body.set('is_optimization_enabled', String(Boolean(data.is_optimization_enabled)));
data.is_optimization_enabled && body.set('optimization_runs', data.optimization_runs);
const libraries = reduceLibrariesArray(data.libraries);
libraries && body.set('libraries', JSON.stringify(libraries));
addFilesToFormData(body, data.sources);
return body;
}
case 'vyper_code': {
return {
compiler_version: data.compiler.value,
source_code: data.code,
contract_name: data.name,
constructor_args: data.abi_encoded_args,
};
}
default: {
return {};
}
}
}
function reduceLibrariesArray(libraries: Array<ContractLibrary> | undefined) {
if (!libraries || libraries.length === 0) {
return;
}
return libraries.reduce<Record<string, string>>((result, item) => {
result[item.name] = item.address;
return result;
}, {});
}
function addFilesToFormData(body: FormData, files: Array<File>) {
for (let index = 0; index < files.length; index++) {
const file = files[index];
body.set(`files[${ index }]`, file, file.name);
}
}
...@@ -9,7 +9,7 @@ import { useAppContext } from 'lib/appContext'; ...@@ -9,7 +9,7 @@ import { useAppContext } from 'lib/appContext';
import isBrowser from 'lib/isBrowser'; import isBrowser from 'lib/isBrowser';
import link from 'lib/link/link'; import link from 'lib/link/link';
import ContractVerificationForm from 'ui/contractVerification/ContractVerificationForm'; import ContractVerificationForm from 'ui/contractVerification/ContractVerificationForm';
import { isValidVerificationMethod } from 'ui/contractVerification/utils'; import { isValidVerificationMethod, sortVerificationMethods } from 'ui/contractVerification/utils';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
...@@ -35,7 +35,7 @@ const ContractVerification = () => { ...@@ -35,7 +35,7 @@ const ContractVerification = () => {
const _data = data as SmartContractVerificationConfigRaw; const _data = data as SmartContractVerificationConfigRaw;
return { return {
..._data, ..._data,
verification_options: _data.verification_options.filter(isValidVerificationMethod), verification_options: _data.verification_options.filter(isValidVerificationMethod).sort(sortVerificationMethods),
}; };
}, },
enabled: Boolean(hash), enabled: Boolean(hash),
...@@ -51,7 +51,7 @@ const ContractVerification = () => { ...@@ -51,7 +51,7 @@ const ContractVerification = () => {
}, [ ]); }, [ ]);
const content = (() => { const content = (() => {
if (configQuery.isError) { if (configQuery.isError || !hash) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
...@@ -63,6 +63,7 @@ const ContractVerification = () => { ...@@ -63,6 +63,7 @@ const ContractVerification = () => {
<ContractVerificationForm <ContractVerificationForm
method={ method && configQuery.data.verification_options.includes(method) ? method : undefined } method={ method && configQuery.data.verification_options.includes(method) ? method : undefined }
config={ configQuery.data } config={ configQuery.data }
hash={ hash }
/> />
); );
})(); })();
......
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