Commit 8b63594b authored by tom's avatar tom

write to contract

parent c159bf91
......@@ -70,6 +70,11 @@ function makePolicyMap() {
// ad
'request-global.czilladx.com',
// walletconnect
'*.walletconnect.com',
'wss://*.bridge.walletconnect.org',
'wss://www.walletlink.org',
],
'script-src': [
......@@ -130,6 +135,9 @@ function makePolicyMap() {
// ad
'servedbyadbutler.com',
'cdn.coinzilla.io',
// walletconnect
'*.walletconnect.com',
],
'font-src': [
......
import * as Sentry from '@sentry/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import {
EthereumClient,
modalConnectors,
walletConnectProvider,
} from '@web3modal/ethereum';
import { Web3Modal } from '@web3modal/react';
import type { AppProps } from 'next/app';
import React, { useState } from 'react';
import type { Chain } from 'wagmi';
import { configureChains, createClient, WagmiConfig } from 'wagmi';
import appConfig from 'configs/app/config';
import type { ResourceError } from 'lib/api/resources';
......@@ -23,39 +15,6 @@ import theme from 'theme';
import AppError from 'ui/shared/AppError/AppError';
import ErrorBoundary from 'ui/shared/ErrorBoundary';
export const poa: Chain = {
id: 99,
name: 'POA',
network: 'poa',
nativeCurrency: {
decimals: 18,
name: 'POA',
symbol: 'POA',
},
rpcUrls: {
'default': { http: [ 'https://core.poa.network' ] },
},
blockExplorers: {
'default': { name: 'Blockscout', url: 'https://blockscout.com/poa/core' },
},
};
const chains = [ poa ];
const PROJECT_ID = 'b4ed81be141093911032944632465175';
// Wagmi client
const { provider } = configureChains(chains, [
walletConnectProvider({ projectId: PROJECT_ID }),
]);
const wagmiClient = createClient({
autoConnect: true,
connectors: modalConnectors({ appName: 'web3Modal', chains }),
provider,
});
// Web3Modal Ethereum Client
const ethereumClient = new EthereumClient(wagmiClient, chains);
function MyApp({ Component, pageProps }: AppProps) {
useConfigSentry();
const [ queryClient ] = useState(() => new QueryClient({
......@@ -102,14 +61,7 @@ function MyApp({ Component, pageProps }: AppProps) {
<QueryClientProvider client={ queryClient }>
<ScrollDirectionProvider>
<SocketProvider url={ `${ appConfig.api.socket }${ appConfig.api.basePath }/socket/v2` }>
<WagmiConfig client={ wagmiClient }>
<Component { ...pageProps }/>
</WagmiConfig>
<Web3Modal
projectId={ PROJECT_ID }
ethereumClient={ ethereumClient }
themeZIndex={ 1200 }
/>
<Component { ...pageProps }/>
</SocketProvider>
</ScrollDirectionProvider>
<ReactQueryDevtools/>
......
import type { Abi } from 'abitype';
export interface SmartContract {
deployed_bytecode: string | null;
creation_bytecode: string | null;
is_self_destructed: boolean;
abi: Array<Record<string, unknown>> | null;
abi: Abi | null;
compiler_version: string | null;
evm_version: string | null;
optimization_enabled: boolean | null;
......@@ -11,6 +13,7 @@ export interface SmartContract {
verified_at: string | null;
is_verified: boolean | null;
source_code: string | null;
constructor_args: string | null;
can_be_visualized_via_sol2uml: boolean | null;
}
......@@ -19,9 +22,10 @@ export interface SmartContractMethodBase {
outputs: Array<SmartContractMethodOutput>;
constant: boolean;
name: string;
stateMutability: string;
stateMutability: 'view' | 'nonpayable' | 'payable';
type: 'function';
payable: boolean;
error?: string;
}
export interface SmartContractReadMethod extends SmartContractMethodBase {
......@@ -29,19 +33,23 @@ export interface SmartContractReadMethod extends SmartContractMethodBase {
}
export interface SmartContractWriteFallback {
payable: true;
stateMutability: 'payable';
type: 'fallback';
}
export type SmartContractWriteMethod = SmartContractMethodBase | SmartContractWriteFallback;
export interface SmartContractWriteReceive {
stateMutability: 'payable';
type: 'receive';
}
export type SmartContractWriteMethod = SmartContractMethodBase | SmartContractWriteFallback | SmartContractWriteReceive;
export type SmartContractMethod = SmartContractReadMethod | SmartContractWriteMethod;
export interface SmartContractMethodInput {
internalType: string;
name: string;
type: string;
type: 'address' | 'uint256' | 'bool';
}
export interface SmartContractMethodOutput extends SmartContractMethodInput {
......
import { useColorModeValue } from '@chakra-ui/react';
import {
EthereumClient,
modalConnectors,
walletConnectProvider,
} from '@web3modal/ethereum';
import { Web3Modal } from '@web3modal/react';
import React from 'react';
import type { Chain } from 'wagmi';
import { configureChains, createClient, WagmiConfig } from 'wagmi';
import type { RoutedSubTab } from 'ui/shared/RoutedTabs/types';
......@@ -13,11 +22,53 @@ const TAB_LIST_PROPS = {
columnGap: 3,
};
export const poa: Chain = {
id: 99,
name: 'POA',
network: 'poa',
nativeCurrency: {
decimals: 18,
name: 'POA',
symbol: 'POA',
},
rpcUrls: {
'default': { http: [ 'https://core.poa.network' ] },
},
blockExplorers: {
'default': { name: 'Blockscout', url: 'https://blockscout.com/poa/core' },
},
};
const chains = [ poa ];
const PROJECT_ID = 'b4ed81be141093911032944632465175';
// Wagmi client
const { provider } = configureChains(chains, [
walletConnectProvider({ projectId: PROJECT_ID }),
]);
const wagmiClient = createClient({
autoConnect: true,
connectors: modalConnectors({ appName: 'web3Modal', chains }),
provider,
});
// Web3Modal Ethereum Client
const ethereumClient = new EthereumClient(wagmiClient, chains);
const AddressContract = ({ tabs }: Props) => {
return (
<ContractContextProvider>
<RoutedTabs tabs={ tabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS }/>
</ContractContextProvider>
<WagmiConfig client={ wagmiClient }>
<ContractContextProvider>
<RoutedTabs tabs={ tabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS }/>
</ContractContextProvider>
<Web3Modal
projectId={ PROJECT_ID }
ethereumClient={ ethereumClient }
themeZIndex={ 1200 }
themeMode={ useColorModeValue('light', 'dark') }
themeBackground="themeColor"
/>
</WagmiConfig>
);
};
......
......@@ -77,6 +77,12 @@ const ContractCode = () => {
</Grid>
) }
<Flex flexDir="column" rowGap={ 6 }>
{ data.constructor_args && (
<RawDataSnippet
data={ data.constructor_args }
title="Constructor Arguments"
/>
) }
{ data.source_code && (
<DynamicContractSourceCode
data={ data.source_code }
......
import { Alert, Button, chakra } from '@chakra-ui/react';
import { Alert, Button, Flex } from '@chakra-ui/react';
import { useWeb3Modal } from '@web3modal/react';
import React from 'react';
import { useAccount, useDisconnect } from 'wagmi';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
const ContractConnectWallet = () => {
const { open } = useWeb3Modal();
const { address, isDisconnected } = useAccount();
const { disconnect } = useDisconnect();
const isMobile = useIsMobile();
const handleConnect = React.useCallback(() => {
open();
......@@ -29,12 +32,14 @@ const ContractConnectWallet = () => {
}
return (
<>
<span>Connected to </span>
<AddressIcon address={{ hash: address, is_contract: false }} mx={ 2 }/>
<chakra.span fontWeight={ 600 }>{ address }</chakra.span>
<Button ml={ 3 } onClick={ handleDisconnect } size="sm" variant="outline">Disconnect</Button>
</>
<Flex columnGap={ 3 } rowGap={ 3 } alignItems={{ base: 'flex-start', lg: 'center' }} flexDir={{ base: 'column', lg: 'row' }}>
<Flex alignItems="center">
<span>Connected to </span>
<AddressIcon address={{ hash: address, is_contract: false }} mx={ 2 }/>
<AddressLink fontWeight={ 600 } hash={ address } truncation={ isMobile ? 'constant' : 'dynamic' }/>
</Flex>
<Button onClick={ handleDisconnect } size="sm" variant="outline">Disconnect</Button>
</Flex>
);
})();
......
......@@ -43,11 +43,14 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit,
const [ isLoading, setLoading ] = React.useState(false);
const inputs = React.useMemo(() => {
return data.payable && (!('inputs' in data) || data.inputs.length === 0) ? [ {
name: 'value',
type: appConfig.network.currency.symbol,
internalType: appConfig.network.currency.symbol,
} as SmartContractMethodInput ] : data.inputs;
return [
...('inputs' in data ? data.inputs : []),
...(data.stateMutability === 'payable' ? [ {
name: 'value',
type: appConfig.network.currency.symbol,
internalType: appConfig.network.currency.symbol,
} as SmartContractMethodInput ] : []),
];
}, [ data ]);
const { control, handleSubmit, setValue } = useForm<MethodFormFields>({
......@@ -67,7 +70,7 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit,
setLoading(false);
})
.catch((error) => {
setResult(error);
setResult('error' in error ? error.error : error);
setLoading(false);
});
}, [ onSubmit, data, inputs ]);
......
......@@ -7,6 +7,7 @@ import type { SmartContractMethodOutput } from 'types/api/contract';
import appConfig from 'configs/app/config';
import { WEI } from 'lib/consts';
import AddressLink from 'ui/shared/address/AddressLink';
interface Props {
data: SmartContractMethodOutput;
......@@ -31,9 +32,17 @@ const ContractMethodStatic = ({ data }: Props) => {
}
}, [ data.value ]);
const content = (() => {
if (data.type === 'address' && data.value) {
return <AddressLink hash={ data.value }/>;
}
return <chakra.span wordBreak="break-all">({ data.type }): { value }</chakra.span>;
})();
return (
<Flex flexDir={{ base: 'column', lg: 'row' }} columnGap={ 2 } rowGap={ 2 }>
<chakra.span wordBreak="break-all">({ data.type }): { value }</chakra.span>
{ content }
{ isBigInt && <Checkbox onChange={ handleCheckboxChange }>{ label }</Checkbox> }
</Flex>
);
......
......@@ -43,7 +43,7 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, renderC
<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 }
{ index + 1 }. { item.type === 'fallback' || item.type === 'receive' ? item.type : item.name }
</Box>
{ item.type === 'fallback' && (
<Tooltip
......@@ -58,6 +58,21 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, renderC
</Box>
</Tooltip>
) }
{ item.type === 'receive' && (
<Tooltip
label={ `The receive function is executed on a call to the contract with empty calldata.
This is the function that is executed on plain Ether transfers (e.g. via .send() or .transfer()).
If no such function exists, but a payable fallback function exists, the fallback function will be called on a plain Ether transfer.
If neither a receive Ether nor a payable fallback function is present,
the contract cannot receive Ether through regular transactions and throws an exception.` }
placement="top"
maxW="320px"
>
<Box cursor="pointer" display="inherit">
<Icon as={ infoIcon } boxSize={ 5 }/>
</Box>
</Tooltip>
) }
<AccordionIcon/>
</AccordionButton>
</h2>
......
......@@ -76,6 +76,10 @@ const ContractRead = ({ isProxy }: Props) => {
}, [ resultBgColor ]);
const renderContent = React.useCallback((item: SmartContractReadMethod, index: number, id: number) => {
if (item.error) {
return <Alert status="error" fontSize="sm">{ item.error }</Alert>;
}
if (item.outputs.some(({ value }) => value)) {
return (
<Flex flexDir="column" rowGap={ 1 }>
......
import { Alert } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { ContractMethodWriteResult } from './types';
import type { SmartContractWriteMethod } from 'types/api/contract';
import useApiQuery from 'lib/api/useApiQuery';
......@@ -11,6 +13,7 @@ import DataFetchAlert from 'ui/shared/DataFetchAlert';
import { useContractContext } from './context';
import ContractConnectWallet from './ContractConnectWallet';
import ContractMethodCallable from './ContractMethodCallable';
import ContractWriteResult from './ContractWriteResult';
interface Props {
isProxy?: boolean;
......@@ -35,7 +38,7 @@ const ContractWrite = ({ isProxy }: Props) => {
return;
}
if (item.type === 'fallback') {
if (item.type === 'fallback' || item.type === 'receive') {
return;
}
......@@ -45,13 +48,21 @@ const ContractWrite = ({ isProxy }: Props) => {
});
// eslint-disable-next-line no-console
console.log('__>__', { result });
console.log('__>__', { result, args });
return [ [ 'string', 'this is mock' ] ];
return { hash: result.hash as string };
}, [ contract ]);
const renderResult = React.useCallback(() => {
return <span>result</span>;
const renderResult = React.useCallback((item: SmartContractWriteMethod, result: ContractMethodWriteResult) => {
if (!result || 'message' in result) {
return (
<Alert status="error" mt={ 3 } p={ 4 } borderRadius="md" fontSize="sm" wordBreak="break-all">
{ result ? result.message : 'No result' }
</Alert>
);
}
return <ContractWriteResult hash={ result.hash as `0x${ string }` }/>;
}, []);
const renderContent = React.useCallback((item: SmartContractWriteMethod, index: number, id: number) => {
......
import { Alert, Box, Link } from '@chakra-ui/react';
import React from 'react';
import { useWaitForTransaction } from 'wagmi';
import link from 'lib/link/link';
interface Props {
hash: `0x${ string }`;
}
const ContractWriteResult = ({ hash }: Props) => {
const txInfo = useWaitForTransaction({
hash,
});
// eslint-disable-next-line no-console
console.log('__>__ txInfo', txInfo);
return (
<>
<Alert status="info" mt={ 3 } p={ 4 } borderRadius="md" fontSize="sm" flexDir="column" alignItems="flex-start">
<Box>Tx hash: <Link href={ link('tx', { id: hash }) }>{ hash }</Link></Box>
<Box>Status: { txInfo.status }</Box>
</Alert>
{ txInfo.error && <Alert status="error" mt={ 3 } p={ 4 } borderRadius="md" fontSize="sm" wordBreak="break-all">{ txInfo.error.message }</Alert> }
</>
);
};
export default React.memo(ContractWriteResult);
import type { ExternalProvider } from '@ethersproject/providers';
import type { Contract } from 'ethers';
import { ethers } from 'ethers';
import { useRouter } from 'next/router';
import React from 'react';
import { useContract, useProvider, useSigner } from 'wagmi';
import useApiQuery from 'lib/api/useApiQuery';
......@@ -15,11 +14,12 @@ type TContractContext = Contract;
const ContractContext = React.createContext<TContractContext | null>(null);
export function ContractContextProvider({ children }: ProviderProps) {
const [ contract, setContract ] = React.useState<TContractContext | null>(null);
const router = useRouter();
const addressHash = router.query.id?.toString();
const provider = useProvider();
const { data: signer } = useSigner();
const { data } = useApiQuery('contract', {
const addressHash = router.query.id?.toString();
const { data: contractInfo } = useApiQuery('contract', {
pathParams: { id: addressHash },
queryOptions: {
enabled: Boolean(addressHash),
......@@ -27,18 +27,11 @@ export function ContractContextProvider({ children }: ProviderProps) {
},
});
React.useEffect(() => {
if (!addressHash || !data?.abi) {
return;
}
const provider = new ethers.providers.Web3Provider(window.ethereum as unknown as ExternalProvider);
const signer = provider.getSigner();
const contract = new ethers.Contract(addressHash, data.abi, provider);
const contractWithSigner = contract.connect(signer);
setContract(contractWithSigner);
}, [ data, addressHash ]);
const contract = useContract({
address: addressHash,
abi: contractInfo?.abi || undefined,
signerOrProvider: signer || provider,
});
return (
<ContractContext.Provider value={ contract }>
......
......@@ -5,7 +5,8 @@ import type { ResourceError } from 'lib/api/resources';
export type MethodFormFields = Record<string, string>;
export type ContractMethodReadResult = SmartContractQueryMethodRead | ResourceError;
export type ContractMethodWriteResult = unknown;
export type ContractMethodWriteResult = Error | { hash: string } | undefined;
export type ContractMethodCallResult<T extends SmartContractMethod> =
T extends { method_id: string } ? ContractMethodReadResult : ContractMethodWriteResult;
......@@ -12,6 +12,7 @@ const TxRevertReason = (props: Props) => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
if ('raw' in props) {
const decoded = hexToUtf8(props.raw);
return (
<Grid
bgColor={ bgColor }
......@@ -25,8 +26,12 @@ const TxRevertReason = (props: Props) => {
>
<GridItem fontWeight={ 500 }>Raw:</GridItem>
<GridItem>{ props.raw }</GridItem>
<GridItem fontWeight={ 500 }>Decoded:</GridItem>
<GridItem>{ hexToUtf8(props.raw) }</GridItem>
{ decoded.replace(/\s|\0/g, '') && (
<>
<GridItem fontWeight={ 500 }>Decoded:</GridItem>
<GridItem>{ decoded }</GridItem>
</>
) }
</Grid>
);
}
......
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