Commit 961fb314 authored by tom's avatar tom

write contract and refactoring

parent 62afe258
...@@ -14,7 +14,7 @@ import type { ...@@ -14,7 +14,7 @@ import type {
} from 'types/api/address'; } from 'types/api/address';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } from 'types/api/block'; import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } from 'types/api/block';
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts'; import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { SmartContract, SmartContractReadMethod } from 'types/api/contract'; import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod } from 'types/api/contract';
import type { IndexingStatus } from 'types/api/indexingStatus'; import type { IndexingStatus } from 'types/api/indexingStatus';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction'; import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { JsonRpcUrlResponse } from 'types/api/jsonRpcUrl'; import type { JsonRpcUrlResponse } from 'types/api/jsonRpcUrl';
...@@ -174,6 +174,9 @@ export const RESOURCES = { ...@@ -174,6 +174,9 @@ export const RESOURCES = {
contract_method_query: { contract_method_query: {
path: '/api/v2/smart-contracts/:id/query-read-method', path: '/api/v2/smart-contracts/:id/query-read-method',
}, },
contract_methods_write: {
path: '/api/v2/smart-contracts/:id/methods-write',
},
// TOKEN // TOKEN
token: { token: {
...@@ -284,6 +287,7 @@ Q extends 'token_counters' ? TokenCounters : ...@@ -284,6 +287,7 @@ Q extends 'token_counters' ? TokenCounters :
Q extends 'config_json_rpc' ? JsonRpcUrlResponse : Q extends 'config_json_rpc' ? JsonRpcUrlResponse :
Q extends 'contract' ? SmartContract : Q extends 'contract' ? SmartContract :
Q extends 'contract_methods_read' ? Array<SmartContractReadMethod> : Q extends 'contract_methods_read' ? Array<SmartContractReadMethod> :
Q extends 'contract_methods_write' ? Array<SmartContractWriteMethod> :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
......
...@@ -14,15 +14,30 @@ export interface SmartContract { ...@@ -14,15 +14,30 @@ export interface SmartContract {
can_be_visualized_via_sol2uml: boolean | null; can_be_visualized_via_sol2uml: boolean | null;
} }
export interface SmartContractReadMethod { export interface SmartContractMethodBase {
inputs: Array<SmartContractMethodInput>; inputs: Array<SmartContractMethodInput>;
outputs: Array<SmartContractMethodOutput>; outputs: Array<SmartContractMethodOutput>;
method_id: string; constant: boolean;
name: string; name: string;
stateMutability: string; stateMutability: string;
type: string; type: 'function';
payable: boolean;
}
export interface SmartContractReadMethod extends SmartContractMethodBase {
method_id: string;
} }
export interface SmartContractWriteFallback {
payable: true;
stateMutability: 'payable';
type: 'fallback';
}
export type SmartContractWriteMethod = SmartContractMethodBase | SmartContractWriteFallback;
export type SmartContractMethod = SmartContractReadMethod | SmartContractWriteMethod;
export interface SmartContractMethodInput { export interface SmartContractMethodInput {
internalType: string; internalType: string;
name: string; name: string;
...@@ -30,5 +45,5 @@ export interface SmartContractMethodInput { ...@@ -30,5 +45,5 @@ export interface SmartContractMethodInput {
} }
export interface SmartContractMethodOutput extends SmartContractMethodInput { export interface SmartContractMethodOutput extends SmartContractMethodInput {
value: string; value?: string;
} }
...@@ -4,21 +4,17 @@ import React from 'react'; ...@@ -4,21 +4,17 @@ import React from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import type { MethodInputFields } from './types'; import type { MethodFormFields } from './types';
import type { SmartContract, SmartContractMethodInput, SmartContractMethodOutput } from 'types/api/contract'; import type { SmartContractMethodInput, SmartContractMethod } from 'types/api/contract';
import appConfig from 'configs/app/config';
import arrowIcon from 'icons/arrows/down-right.svg'; import arrowIcon from 'icons/arrows/down-right.svg';
import useApiFetch from 'lib/api/useApiFetch';
import ContractReadItemInputField from './ContractReadItemInputField'; import ContractMethodField from './ContractMethodField';
interface Props { interface Props<T extends SmartContractMethod> {
data: Array<SmartContractMethodInput>; data: T;
address?: string; caller: (data: T, args: Array<string>) => Promise<Array<Array<string>>>;
abi?: SmartContract['abi'];
methodName: string;
methodId: string;
outputs: Array<SmartContractMethodOutput>;
} }
const getFieldName = (name: string, index: number): string => name || String(index); const getFieldName = (name: string, index: number): string => name || String(index);
...@@ -39,36 +35,30 @@ const sortFields = (data: Array<SmartContractMethodInput>) => ([ a ]: [string, s ...@@ -39,36 +35,30 @@ const sortFields = (data: Array<SmartContractMethodInput>) => ([ a ]: [string, s
return 0; return 0;
}; };
const ContractReadItemInput = ({ data, address, methodId, methodName, outputs }: Props) => { const ContractMethodCallable = <T extends SmartContractMethod>({ data, caller }: Props<T>) => {
const { control, handleSubmit, setValue } = useForm<MethodInputFields>({
defaultValues: _fromPairs(data.map(({ name }, index) => [ getFieldName(name, index), '' ])), const inputs = React.useMemo(() => {
}); return data.payable && (!('inputs' in data) || data.inputs.length === 0) ? [ {
const apiFetch = useApiFetch(); name: 'value',
const [ result, setResult ] = React.useState<Array<[ string, string ]>>([ ]); type: appConfig.network.currency.symbol,
internalType: appConfig.network.currency.symbol,
} as SmartContractMethodInput ] : data.inputs;
}, [ data ]);
const onSubmit: SubmitHandler<MethodInputFields> = React.useCallback(async(formData) => { const { control, handleSubmit, setValue } = useForm<MethodFormFields>({
if (!address) { defaultValues: _fromPairs(inputs.map(({ name }, index) => [ getFieldName(name, index), '' ])),
return; });
} const [ result, setResult ] = React.useState<Array<Array<string>>>([ ]);
const onSubmit: SubmitHandler<MethodFormFields> = React.useCallback(async(formData) => {
const args = Object.entries(formData) const args = Object.entries(formData)
.sort(sortFields(data)) .sort(sortFields(inputs))
.map(([ , value ]) => value); .map(([ , value ]) => value);
// todo_tom delete mock const result = await caller(data, args);
setResult(outputs.map(({ type }, index) => ([ type, args[index] ]))); setResult(result);
await apiFetch('contract_method_query', { }, [ caller, data, inputs ]);
pathParams: { id: address },
fetchParams: {
method: 'POST',
body: {
args,
method_id: methodId,
},
},
});
}, [ address, apiFetch, data, methodId, outputs ]);
const resultBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); const resultBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
...@@ -84,10 +74,10 @@ const ContractReadItemInput = ({ data, address, methodId, methodName, outputs }: ...@@ -84,10 +74,10 @@ const ContractReadItemInput = ({ data, address, methodId, methodName, outputs }:
onSubmit={ handleSubmit(onSubmit) } onSubmit={ handleSubmit(onSubmit) }
flexWrap="wrap" flexWrap="wrap"
> >
{ data.map(({ type, name }, index) => { { inputs.map(({ type, name }, index) => {
const fieldName = getFieldName(name, index); const fieldName = getFieldName(name, index);
return ( return (
<ContractReadItemInputField <ContractMethodField
key={ fieldName } key={ fieldName }
name={ fieldName } name={ fieldName }
placeholder={ `${ name }(${ type })` } placeholder={ `${ name }(${ type })` }
...@@ -105,14 +95,16 @@ const ContractReadItemInput = ({ data, address, methodId, methodName, outputs }: ...@@ -105,14 +95,16 @@ const ContractReadItemInput = ({ data, address, methodId, methodName, outputs }:
Query Query
</Button> </Button>
</chakra.form> </chakra.form>
<Flex mt={ 3 }> { 'outputs' in data && data.outputs.length > 0 && (
<Icon as={ arrowIcon } boxSize={ 5 } mr={ 1 }/> <Flex mt={ 3 }>
<Text>{ outputs.map(({ type }) => type).join(', ') }</Text> <Icon as={ arrowIcon } boxSize={ 5 } mr={ 1 }/>
</Flex> <Text>{ data.outputs.map(({ type }) => type).join(', ') }</Text>
</Flex>
) }
{ result.length > 0 && ( { result.length > 0 && (
<Box mt={ 3 } p={ 4 } borderRadius="md" bgColor={ resultBgColor } fontSize="sm"> <Box mt={ 3 } p={ 4 } borderRadius="md" bgColor={ resultBgColor } fontSize="sm">
<p> <p>
[ <chakra.span fontWeight={ 600 }>{ methodName }</chakra.span> method Response ] [ <chakra.span fontWeight={ 600 }>{ 'name' in data ? data.name : '' }</chakra.span> method response ]
</p> </p>
<p>[</p> <p>[</p>
{ result.map(([ key, value ], index) => ( { result.map(([ key, value ], index) => (
...@@ -125,4 +117,4 @@ const ContractReadItemInput = ({ data, address, methodId, methodName, outputs }: ...@@ -125,4 +117,4 @@ const ContractReadItemInput = ({ data, address, methodId, methodName, outputs }:
); );
}; };
export default ContractReadItemInput; export default React.memo(ContractMethodCallable) as typeof ContractMethodCallable;
...@@ -12,12 +12,16 @@ interface Props { ...@@ -12,12 +12,16 @@ interface Props {
data: SmartContractMethodOutput; data: SmartContractMethodOutput;
} }
const ContractReadItemOutput = ({ data }: Props) => { const ContractMethodStatic = ({ data }: Props) => {
const isBigInt = data.type.includes('int256') || data.type.includes('int128'); const isBigInt = data.type.includes('int256') || data.type.includes('int128');
const [ value, setValue ] = React.useState(isBigInt ? BigNumber(data.value).toFixed() : data.value); const [ value, setValue ] = React.useState(isBigInt && data.value ? BigNumber(data.value).toFixed() : data.value);
const [ label, setLabel ] = React.useState('WEI'); const [ label, setLabel ] = React.useState('WEI');
const handleCheckboxChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => { const handleCheckboxChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
if (!data.value) {
return;
}
if (event.target.checked) { if (event.target.checked) {
setValue(BigNumber(data.value).div(WEI).toFixed()); setValue(BigNumber(data.value).div(WEI).toFixed());
setLabel(appConfig.network.currency.symbol || 'ETH'); setLabel(appConfig.network.currency.symbol || 'ETH');
...@@ -35,4 +39,4 @@ const ContractReadItemOutput = ({ data }: Props) => { ...@@ -35,4 +39,4 @@ const ContractReadItemOutput = ({ data }: Props) => {
); );
}; };
export default ContractReadItemOutput; export default ContractMethodStatic;
...@@ -3,18 +3,18 @@ import React from 'react'; ...@@ -3,18 +3,18 @@ import React from 'react';
import type { Control, ControllerRenderProps, UseFormSetValue } from 'react-hook-form'; import type { Control, ControllerRenderProps, UseFormSetValue } from 'react-hook-form';
import { Controller } from 'react-hook-form'; import { Controller } from 'react-hook-form';
import type { MethodInputFields } from './types'; import type { MethodFormFields } from './types';
import InputClearButton from 'ui/shared/InputClearButton'; import InputClearButton from 'ui/shared/InputClearButton';
interface Props { interface Props {
control: Control<MethodInputFields>; control: Control<MethodFormFields>;
setValue: UseFormSetValue<MethodInputFields>; setValue: UseFormSetValue<MethodFormFields>;
placeholder: string; placeholder: string;
name: string; name: string;
} }
const ContractReadItemInputField = ({ control, name, placeholder, setValue }: Props) => { const ContractMethodField = ({ control, name, placeholder, setValue }: Props) => {
const ref = React.useRef<HTMLInputElement>(null); const ref = React.useRef<HTMLInputElement>(null);
const handleClear = React.useCallback(() => { const handleClear = React.useCallback(() => {
...@@ -22,7 +22,7 @@ const ContractReadItemInputField = ({ control, name, placeholder, setValue }: Pr ...@@ -22,7 +22,7 @@ const ContractReadItemInputField = ({ control, name, placeholder, setValue }: Pr
ref.current?.focus(); ref.current?.focus();
}, [ name, setValue ]); }, [ name, setValue ]);
const renderInput = React.useCallback(({ field }: { field: ControllerRenderProps<MethodInputFields> }) => { const renderInput = React.useCallback(({ field }: { field: ControllerRenderProps<MethodFormFields> }) => {
return ( return (
<FormControl id={ name } maxW={{ base: '100%', lg: 'calc((100% - 24px) / 3)' }}> <FormControl id={ name } maxW={{ base: '100%', lg: 'calc((100% - 24px) / 3)' }}>
<InputGroup size="xs"> <InputGroup size="xs">
...@@ -51,4 +51,4 @@ const ContractReadItemInputField = ({ control, name, placeholder, setValue }: Pr ...@@ -51,4 +51,4 @@ const ContractReadItemInputField = ({ control, name, placeholder, setValue }: Pr
); );
}; };
export default ContractReadItemInputField; export default React.memo(ContractMethodField);
import { Accordion, AccordionButton, AccordionIcon, AccordionItem, AccordionPanel, Box, Flex, Link } from '@chakra-ui/react';
import _range from 'lodash/range';
import React from 'react';
import type { SmartContractMethod } from 'types/api/contract';
interface Props<T extends SmartContractMethod> {
data: Array<T>;
renderContent: (item: T, index: number, id: number) => React.ReactNode;
}
const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, renderContent }: Props<T>) => {
const [ expandedSections, setExpandedSections ] = React.useState<Array<number>>([]);
const [ id, setId ] = React.useState(0);
const handleAccordionStateChange = React.useCallback((newValue: Array<number>) => {
setExpandedSections(newValue);
}, []);
const handleExpandAll = React.useCallback(() => {
if (!data) {
return;
}
if (expandedSections.length < data.length) {
setExpandedSections(_range(0, data.length));
} else {
setExpandedSections([]);
}
}, [ data, expandedSections.length ]);
const handleReset = React.useCallback(() => {
setId((id) => id + 1);
}, []);
return (
<Accordion allowMultiple position="relative" onChange={ handleAccordionStateChange } index={ expandedSections }>
{ data.map((item, index) => {
return (
<AccordionItem key={ index } as="section">
<h2>
<AccordionButton px={ 0 } py={ 3 } _hover={{ bgColor: 'inherit' }}>
<Box as="span" fontFamily="heading" fontWeight={ 500 } fontSize="lg" mr={ 1 }>
{ index + 1 }. { item.type === 'fallback' ? 'fallback' : item.name }
</Box>
<AccordionIcon/>
</AccordionButton>
</h2>
<AccordionPanel pb={ 4 } px={ 0 }>
{ renderContent(item, index, id) }
</AccordionPanel>
</AccordionItem>
);
}) }
<Flex columnGap={ 3 } position="absolute" top={ 0 } right={ 0 } py={ 3 } lineHeight="27px">
<Link onClick={ handleExpandAll }>{ expandedSections.length === data.length ? 'Collapse' : 'Expand' } all</Link>
<Link onClick={ handleReset }>Reset</Link>
</Flex>
</Accordion>
);
};
export default React.memo(ContractMethodsAccordion) as typeof ContractMethodsAccordion;
import { Accordion, AccordionButton, AccordionIcon, AccordionItem, AccordionPanel, Box, Flex, Link } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import _range from 'lodash/range';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { SmartContractReadMethod } from 'types/api/contract';
import useApiFetch from 'lib/api/useApiFetch';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import ContractReadItemInput from './ContractReadItemInput'; import ContractMethodCallable from './ContractMethodCallable';
import ContractReadItemOutput from './ContractReadItemOutput'; import ContractMethodConstant from './ContractMethodConstant';
const ContractRead = () => { const ContractRead = () => {
const router = useRouter(); const router = useRouter();
const apiFetch = useApiFetch();
const [ expandedSections, setExpandedSections ] = React.useState<Array<number>>([]);
const [ id, setId ] = React.useState(0);
const addressHash = router.query.id?.toString(); const addressHash = router.query.id?.toString();
...@@ -24,80 +26,48 @@ const ContractRead = () => { ...@@ -24,80 +26,48 @@ const ContractRead = () => {
}, },
}); });
const contractInfo = useApiQuery('contract', { const contractCaller = React.useCallback(async(item: SmartContractReadMethod, args: Array<string>) => {
pathParams: { id: addressHash }, await apiFetch('contract_method_query', {
queryOptions: { pathParams: { id: addressHash },
enabled: Boolean(router.query.id), fetchParams: {
}, method: 'POST',
}); body: {
args,
method_id: item.method_id,
},
},
});
const handleAccordionStateChange = React.useCallback((newValue: Array<number>) => { return [ [ 'string', 'this is mock' ] ];
setExpandedSections(newValue); }, [ addressHash, apiFetch ]);
}, []);
const handleExpandAll = React.useCallback(() => {
if (!data) {
return;
}
if (expandedSections.length < data.length) { const renderContent = React.useCallback((item: SmartContractReadMethod, index: number, id: number) => {
setExpandedSections(_range(0, data.length)); if (item.inputs.length === 0) {
} else { return (
setExpandedSections([]); <Flex flexDir="column" rowGap={ 1 }>
{ item.outputs.map((output, index) => <ContractMethodConstant key={ index } data={ output }/>) }
</Flex>
);
} }
}, [ data, expandedSections.length ]);
const handleReset = React.useCallback(() => { return (
setId((id) => id + 1); <ContractMethodCallable
}, []); key={ id + '_' + index }
data={ item }
caller={ contractCaller }
/>
);
}, [ contractCaller ]);
if (isError) { if (isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
if (isLoading) { if (isLoading) {
return <span>loading...</span>; return <ContentLoader/>;
} }
return ( return <ContractMethodsAccordion data={ data } renderContent={ renderContent }/>;
<Accordion allowMultiple position="relative" onChange={ handleAccordionStateChange } index={ expandedSections }>
{ data.map((item, index) => {
return (
<AccordionItem key={ item.name + '_' + item.method_id } as="section">
<h2>
<AccordionButton px={ 0 } py={ 3 } _hover={{ bgColor: 'inherit' }}>
<Box as="span" fontFamily="heading" fontWeight={ 500 } fontSize="lg" mr={ 1 }>
{ index + 1 }. { item.name }
</Box>
<AccordionIcon/>
</AccordionButton>
</h2>
<AccordionPanel pb={ 4 } px={ 0 }>
{ item.inputs.length === 0 ? (
<Flex flexDir="column" rowGap={ 1 }>
{ item.outputs.map((output, index) => <ContractReadItemOutput key={ index } data={ output }/>) }
</Flex>
) : (
<ContractReadItemInput
key={ id + '_' + index }
data={ item.inputs }
address={ addressHash }
abi={ contractInfo.data?.abi }
methodName={ item.name }
methodId={ item.method_id }
outputs={ item.outputs }
/>
) }
</AccordionPanel>
</AccordionItem>
);
}) }
<Flex columnGap={ 3 } position="absolute" top={ 0 } right={ 0 } py={ 3 } lineHeight="27px">
<Link onClick={ handleExpandAll }>{ expandedSections.length === data.length ? 'Collapse' : 'Expand' } all</Link>
<Link onClick={ handleReset }>Reset</Link>
</Flex>
</Accordion>
);
}; };
export default ContractRead; export default ContractRead;
import { useRouter } from 'next/router';
import React from 'react';
import type { SmartContractWriteMethod } from 'types/api/contract';
import useApiQuery from 'lib/api/useApiQuery';
import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import ContractMethodCallable from './ContractMethodCallable';
const ContractWrite = () => {
const router = useRouter();
const addressHash = router.query.id?.toString();
const { data, isLoading, isError } = useApiQuery('contract_methods_write', {
pathParams: { id: addressHash },
queryOptions: {
enabled: Boolean(router.query.id),
},
});
const contractInfo = useApiQuery('contract', {
pathParams: { id: addressHash },
queryOptions: {
enabled: Boolean(router.query.id),
},
});
const contractCaller = React.useCallback(async() => {
// eslint-disable-next-line no-console
console.log('__>__', contractInfo);
return [ [ 'string', 'this is mock' ] ];
}, [ contractInfo ]);
const renderContent = React.useCallback((item: SmartContractWriteMethod, index: number, id: number) => {
return (
<ContractMethodCallable
key={ id + '_' + index }
data={ item }
caller={ contractCaller }
/>
);
}, [ contractCaller ]);
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return <ContentLoader/>;
}
return <ContractMethodsAccordion data={ data } renderContent={ renderContent }/>;
};
export default ContractWrite;
export type MethodInputFields = Record<string, string>; export type MethodFormFields = Record<string, string>;
...@@ -16,6 +16,7 @@ import AddressTokenTransfers from 'ui/address/AddressTokenTransfers'; ...@@ -16,6 +16,7 @@ import AddressTokenTransfers from 'ui/address/AddressTokenTransfers';
import AddressTxs from 'ui/address/AddressTxs'; import AddressTxs from 'ui/address/AddressTxs';
import ContractCode from 'ui/address/contract/ContractCode'; import ContractCode from 'ui/address/contract/ContractCode';
import ContractRead from 'ui/address/contract/ContractRead'; import ContractRead from 'ui/address/contract/ContractRead';
import ContractWrite from 'ui/address/contract/ContractWrite';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
...@@ -27,7 +28,7 @@ const CONTRACT_TABS = [ ...@@ -27,7 +28,7 @@ const CONTRACT_TABS = [
{ id: 'contact_decompiled_code', title: 'Decompiled code', component: <div>Decompiled code</div> }, { id: 'contact_decompiled_code', title: 'Decompiled code', component: <div>Decompiled code</div> },
{ id: 'read_contract', title: 'Read contract', component: <ContractRead/> }, { id: 'read_contract', title: 'Read contract', component: <ContractRead/> },
{ id: 'read_proxy', title: 'Read proxy', component: <div>Read proxy</div> }, { id: 'read_proxy', title: 'Read proxy', component: <div>Read proxy</div> },
{ id: 'write_contract', title: 'Write contract', component: <div>Write contract</div> }, { id: 'write_contract', title: 'Write contract', component: <ContractWrite/> },
{ id: 'write_proxy', title: 'Write proxy', component: <div>Write proxy</div> }, { id: 'write_proxy', title: 'Write proxy', component: <div>Write proxy</div> },
]; ];
......
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