Call contract read methods from the client (#2032)
* display methods accordion * add method_id to methods * query read method via public client * query read methods without inputs at accordion section open * add custom abi tabs * add proxy tabs * custom wagmi config if wc is not configured * remove ContractAbiItemConstant component * display tuple properties in method outputs * display read result * clean up * support nested arrays in the read result * re-write test for contract methods * change result view for address output * refactoring * fix tests * remove web3 provider fallback * fix tab loading * refactoring * add implementation contract selector * add "copy to clipboard" for address, remove top-level square brackets * add preview mode to contract method result * tests for read result * fix tests * change styles for result preview * review fix
@@ -18,7 +18,7 @@ NEXT_PUBLIC_NETWORK_RPC_URL=https://eth-sepolia.public.blastapi.io
|
||||
NEXT_PUBLIC_IS_TESTNET=true
|
||||
|
||||
# api configuration
|
||||
NEXT_PUBLIC_API_HOST=eth-sepolia.k8s-dev.blockscout.com
|
||||
NEXT_PUBLIC_API_HOST=eth-sepolia.blockscout.com
|
||||
NEXT_PUBLIC_API_BASE_PATH=/
|
||||
|
||||
# ui config
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.16 3A2.16 2.16 0 0 0 3 5.16v10.18a2.16 2.16 0 0 0 2.16 2.16h10.18a2.16 2.16 0 0 0 2.16-2.16v-3.272h-1.614v3.114c0 .389-.315.704-.704.704H5.318a.705.705 0 0 1-.704-.704V5.318c0-.389.315-.704.704-.704h3.114V3H5.159Zm6.135 0a.41.41 0 0 0-.41.41v.793c0 .226.184.409.41.409h3.453l-4.244 4.245a.409.409 0 0 0 0 .578l.56.561c.16.16.42.16.58 0l4.244-4.245v3.454c0 .226.183.41.41.41h.793a.41.41 0 0 0 .409-.41V3h-6.205Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 568 B |
@@ -46,8 +46,6 @@ import type { ChartMarketResponse, ChartSecondaryCoinPriceResponse, ChartTransac
|
||||
import type { BackendVersionConfig } from 'types/api/configs';
|
||||
import type {
|
||||
SmartContract,
|
||||
SmartContractReadMethod,
|
||||
SmartContractWriteMethod,
|
||||
SmartContractVerificationConfig,
|
||||
SolidityscanReport,
|
||||
SmartContractSecurityAudits,
|
||||
@@ -465,26 +463,6 @@ export const RESOURCES = {
|
||||
path: '/api/v2/smart-contracts/:hash',
|
||||
pathParams: [ 'hash' as const ],
|
||||
},
|
||||
contract_methods_read: {
|
||||
path: '/api/v2/smart-contracts/:hash/methods-read',
|
||||
pathParams: [ 'hash' as const ],
|
||||
},
|
||||
contract_methods_read_proxy: {
|
||||
path: '/api/v2/smart-contracts/:hash/methods-read-proxy',
|
||||
pathParams: [ 'hash' as const ],
|
||||
},
|
||||
contract_method_query: {
|
||||
path: '/api/v2/smart-contracts/:hash/query-read-method',
|
||||
pathParams: [ 'hash' as const ],
|
||||
},
|
||||
contract_methods_write: {
|
||||
path: '/api/v2/smart-contracts/:hash/methods-write',
|
||||
pathParams: [ 'hash' as const ],
|
||||
},
|
||||
contract_methods_write_proxy: {
|
||||
path: '/api/v2/smart-contracts/:hash/methods-write-proxy',
|
||||
pathParams: [ 'hash' as const ],
|
||||
},
|
||||
contract_verification_config: {
|
||||
path: '/api/v2/smart-contracts/verification/config',
|
||||
},
|
||||
@@ -1000,10 +978,6 @@ Q extends 'quick_search' ? Array<SearchResultItem> :
|
||||
Q extends 'search' ? SearchResult :
|
||||
Q extends 'search_check_redirect' ? SearchRedirectResult :
|
||||
Q extends 'contract' ? SmartContract :
|
||||
Q extends 'contract_methods_read' ? Array<SmartContractReadMethod> :
|
||||
Q extends 'contract_methods_read_proxy' ? Array<SmartContractReadMethod> :
|
||||
Q extends 'contract_methods_write' ? Array<SmartContractWriteMethod> :
|
||||
Q extends 'contract_methods_write_proxy' ? Array<SmartContractWriteMethod> :
|
||||
Q extends 'contract_solidityscan_report' ? SolidityscanReport :
|
||||
Q extends 'verified_contracts' ? VerifiedContractsResponse :
|
||||
Q extends 'verified_contracts_counters' ? VerifiedContractsCounters :
|
||||
|
||||
@@ -4,19 +4,24 @@ import React from 'react';
|
||||
import type { Address } from 'types/api/address';
|
||||
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
import * as cookies from 'lib/cookies';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import useSocketChannel from 'lib/socket/useSocketChannel';
|
||||
import * as stubs from 'stubs/contract';
|
||||
import ContractCode from 'ui/address/contract/ContractCode';
|
||||
import ContractRead from 'ui/address/contract/ContractRead';
|
||||
import ContractWrite from 'ui/address/contract/ContractWrite';
|
||||
import ContractMethodsCustom from 'ui/address/contract/methods/ContractMethodsCustom';
|
||||
import ContractMethodsProxy from 'ui/address/contract/methods/ContractMethodsProxy';
|
||||
import ContractMethodsRegular from 'ui/address/contract/methods/ContractMethodsRegular';
|
||||
import { divideAbiIntoMethodTypes } from 'ui/address/contract/methods/utils';
|
||||
|
||||
const CONTRACT_TAB_IDS = [
|
||||
'contract_code',
|
||||
'read_contract',
|
||||
'read_contract_rpc',
|
||||
'read_proxy',
|
||||
'read_custom_methods',
|
||||
'write_contract',
|
||||
'write_contract_rpc',
|
||||
'write_proxy',
|
||||
'write_custom_methods',
|
||||
] as const;
|
||||
@@ -38,7 +43,7 @@ export default function useContractTabs(data: Address | undefined, isPlaceholder
|
||||
const router = useRouter();
|
||||
const tab = getQueryParamString(router.query.tab);
|
||||
|
||||
const isEnabled = Boolean(data?.hash) && !isPlaceholderData && CONTRACT_TAB_IDS.concat('contract' as never).includes(tab);
|
||||
const isEnabled = Boolean(data?.hash) && data?.is_contract && !isPlaceholderData && CONTRACT_TAB_IDS.concat('contract' as never).includes(tab);
|
||||
|
||||
const enableQuery = React.useCallback(() => {
|
||||
setIsQueryEnabled(true);
|
||||
@@ -53,6 +58,13 @@ export default function useContractTabs(data: Address | undefined, isPlaceholder
|
||||
},
|
||||
});
|
||||
|
||||
const customAbiQuery = useApiQuery('custom_abi', {
|
||||
queryOptions: {
|
||||
enabled: isEnabled && isQueryEnabled && Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
|
||||
refetchOnMount: false,
|
||||
},
|
||||
});
|
||||
|
||||
const channel = useSocketChannel({
|
||||
topic: `addresses:${ data?.hash?.toLowerCase() }`,
|
||||
isDisabled: !isEnabled,
|
||||
@@ -60,6 +72,20 @@ export default function useContractTabs(data: Address | undefined, isPlaceholder
|
||||
onSocketError: enableQuery,
|
||||
});
|
||||
|
||||
const methods = React.useMemo(() => divideAbiIntoMethodTypes(contractQuery.data?.abi ?? []), [ contractQuery.data?.abi ]);
|
||||
const methodsCustomAbi = React.useMemo(() => {
|
||||
return divideAbiIntoMethodTypes(
|
||||
customAbiQuery.data
|
||||
?.find((item) => data && item.contract_address_hash.toLowerCase() === data.hash.toLowerCase())
|
||||
?.abi ??
|
||||
[],
|
||||
);
|
||||
}, [ customAbiQuery.data, data ]);
|
||||
|
||||
const verifiedImplementations = React.useMemo(() => {
|
||||
return data?.implementations?.filter(({ name, address }) => name && address && address !== data?.hash) || [];
|
||||
}, [ data?.hash, data?.implementations ]);
|
||||
|
||||
return React.useMemo(() => {
|
||||
return {
|
||||
tabs: [
|
||||
@@ -68,26 +94,50 @@ export default function useContractTabs(data: Address | undefined, isPlaceholder
|
||||
title: 'Code',
|
||||
component: <ContractCode contractQuery={ contractQuery } channel={ channel } addressHash={ data?.hash }/>,
|
||||
},
|
||||
contractQuery.data?.has_methods_read ?
|
||||
{ id: 'read_contract' as const, title: 'Read contract', component: <ContractRead isLoading={ contractQuery.isPlaceholderData }/> } :
|
||||
undefined,
|
||||
contractQuery.data?.has_methods_read_proxy ?
|
||||
{ id: 'read_proxy' as const, title: 'Read proxy', component: <ContractRead isLoading={ contractQuery.isPlaceholderData }/> } :
|
||||
undefined,
|
||||
contractQuery.data?.has_custom_methods_read ?
|
||||
{ id: 'read_custom_methods' as const, title: 'Read custom', component: <ContractRead isLoading={ contractQuery.isPlaceholderData }/> } :
|
||||
undefined,
|
||||
contractQuery.data?.has_methods_write ?
|
||||
{ id: 'write_contract' as const, title: 'Write contract', component: <ContractWrite isLoading={ contractQuery.isPlaceholderData }/> } :
|
||||
undefined,
|
||||
contractQuery.data?.has_methods_write_proxy ?
|
||||
{ id: 'write_proxy' as const, title: 'Write proxy', component: <ContractWrite isLoading={ contractQuery.isPlaceholderData }/> } :
|
||||
undefined,
|
||||
contractQuery.data?.has_custom_methods_write ?
|
||||
{ id: 'write_custom_methods' as const, title: 'Write custom', component: <ContractWrite isLoading={ contractQuery.isPlaceholderData }/> } :
|
||||
undefined,
|
||||
methods.read.length > 0 && {
|
||||
id: 'read_contract' as const,
|
||||
title: 'Read contract',
|
||||
component: <ContractMethodsRegular type="read" abi={ methods.read } isLoading={ contractQuery.isPlaceholderData }/>,
|
||||
},
|
||||
methodsCustomAbi.read.length > 0 && {
|
||||
id: 'read_custom_methods' as const,
|
||||
title: 'Read custom',
|
||||
component: <ContractMethodsCustom type="read" abi={ methodsCustomAbi.read } isLoading={ contractQuery.isPlaceholderData }/>,
|
||||
},
|
||||
verifiedImplementations.length > 0 && {
|
||||
id: 'read_proxy' as const,
|
||||
title: 'Read proxy',
|
||||
component: (
|
||||
<ContractMethodsProxy
|
||||
type="read"
|
||||
implementations={ verifiedImplementations }
|
||||
isLoading={ contractQuery.isPlaceholderData }
|
||||
/>
|
||||
),
|
||||
},
|
||||
methods.write.length > 0 && {
|
||||
id: 'write_contract' as const,
|
||||
title: 'Write contract',
|
||||
component: <ContractMethodsRegular type="write" abi={ methods.write } isLoading={ contractQuery.isPlaceholderData }/>,
|
||||
},
|
||||
methodsCustomAbi.write.length > 0 && {
|
||||
id: 'write_custom_methods' as const,
|
||||
title: 'Write custom',
|
||||
component: <ContractMethodsCustom type="write" abi={ methodsCustomAbi.write } isLoading={ contractQuery.isPlaceholderData }/>,
|
||||
},
|
||||
verifiedImplementations.length > 0 && {
|
||||
id: 'write_proxy' as const,
|
||||
title: 'Write proxy',
|
||||
component: (
|
||||
<ContractMethodsProxy
|
||||
type="write"
|
||||
implementations={ verifiedImplementations }
|
||||
isLoading={ contractQuery.isPlaceholderData }
|
||||
/>
|
||||
),
|
||||
},
|
||||
].filter(Boolean),
|
||||
isLoading: contractQuery.isPlaceholderData,
|
||||
};
|
||||
}, [ contractQuery, channel, data?.hash ]);
|
||||
}, [ contractQuery, channel, data?.hash, verifiedImplementations, methods.read, methods.write, methodsCustomAbi.read, methodsCustomAbi.write ]);
|
||||
}
|
||||
|
||||
@@ -1,38 +1,46 @@
|
||||
import { defaultWagmiConfig } from '@web3modal/wagmi/react/config';
|
||||
import { http } from 'viem';
|
||||
import type { CreateConfigParameters } from 'wagmi';
|
||||
import { createConfig, type CreateConfigParameters } from 'wagmi';
|
||||
|
||||
import config from 'configs/app';
|
||||
import currentChain from 'lib/web3/currentChain';
|
||||
const feature = config.features.blockchainInteraction;
|
||||
|
||||
const wagmiConfig = (() => {
|
||||
try {
|
||||
if (!feature.isEnabled) {
|
||||
throw new Error();
|
||||
}
|
||||
const chains: CreateConfigParameters['chains'] = [ currentChain ];
|
||||
|
||||
const chains: CreateConfigParameters['chains'] = [ currentChain ];
|
||||
|
||||
const wagmiConfig = defaultWagmiConfig({
|
||||
chains,
|
||||
multiInjectedProviderDiscovery: true,
|
||||
if (!feature.isEnabled) {
|
||||
const wagmiConfig = createConfig({
|
||||
chains: [ currentChain ],
|
||||
transports: {
|
||||
[currentChain.id]: http(),
|
||||
[currentChain.id]: http(config.chain.rpcUrl || `${ config.api.endpoint }/api/eth-rpc`),
|
||||
},
|
||||
projectId: feature.walletConnect.projectId,
|
||||
metadata: {
|
||||
name: `${ config.chain.name } explorer`,
|
||||
description: `${ config.chain.name } explorer`,
|
||||
url: config.app.baseUrl,
|
||||
icons: [ config.UI.navigation.icon.default ].filter(Boolean),
|
||||
},
|
||||
enableEmail: true,
|
||||
ssr: true,
|
||||
batch: { multicall: { wait: 100 } },
|
||||
});
|
||||
|
||||
return wagmiConfig;
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
const wagmiConfig = defaultWagmiConfig({
|
||||
chains,
|
||||
multiInjectedProviderDiscovery: true,
|
||||
transports: {
|
||||
[currentChain.id]: http(),
|
||||
},
|
||||
projectId: feature.walletConnect.projectId,
|
||||
metadata: {
|
||||
name: `${ config.chain.name } explorer`,
|
||||
description: `${ config.chain.name } explorer`,
|
||||
url: config.app.baseUrl,
|
||||
icons: [ config.UI.navigation.icon.default ].filter(Boolean),
|
||||
},
|
||||
enableEmail: true,
|
||||
ssr: true,
|
||||
batch: { multicall: { wait: 100 } },
|
||||
});
|
||||
|
||||
return wagmiConfig;
|
||||
})();
|
||||
|
||||
export default wagmiConfig;
|
||||
|
||||
@@ -33,12 +33,6 @@ export const verified: SmartContract = {
|
||||
],
|
||||
language: 'solidity',
|
||||
license_type: 'gnu_gpl_v3',
|
||||
has_methods_read: true,
|
||||
has_methods_read_proxy: false,
|
||||
has_methods_write: true,
|
||||
has_methods_write_proxy: false,
|
||||
has_custom_methods_read: false,
|
||||
has_custom_methods_write: false,
|
||||
is_self_destructed: false,
|
||||
is_verified_via_eth_bytecode_db: null,
|
||||
is_changed_bytecode: null,
|
||||
@@ -120,12 +114,6 @@ export const nonVerified: SmartContract = {
|
||||
verified_at: null,
|
||||
is_verified_via_eth_bytecode_db: null,
|
||||
is_changed_bytecode: null,
|
||||
has_methods_read: false,
|
||||
has_methods_read_proxy: false,
|
||||
has_methods_write: false,
|
||||
has_methods_write_proxy: false,
|
||||
has_custom_methods_read: false,
|
||||
has_custom_methods_write: false,
|
||||
is_verified_via_sourcify: null,
|
||||
is_fully_verified: null,
|
||||
is_partially_verified: null,
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import type {
|
||||
SmartContractQueryMethodError,
|
||||
SmartContractQueryMethodSuccess,
|
||||
SmartContractReadMethod,
|
||||
SmartContractWriteMethod,
|
||||
} from 'types/api/contract';
|
||||
import type { SmartContractMethodRead, SmartContractMethodWrite } from 'ui/address/contract/methods/types';
|
||||
|
||||
export const read: Array<SmartContractReadMethod> = [
|
||||
export const read: Array<SmartContractMethodRead> = [
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
@@ -26,94 +21,15 @@ export const read: Array<SmartContractReadMethod> = [
|
||||
method_id: '06fdde03',
|
||||
name: 'name',
|
||||
outputs: [
|
||||
{ internalType: 'string', name: '', type: 'string', value: 'Wrapped POA' },
|
||||
{ internalType: 'string', name: '', type: 'string' },
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
method_id: '18160ddd',
|
||||
name: 'totalSupply',
|
||||
outputs: [
|
||||
{ internalType: 'uint256', name: '', type: 'uint256', value: '139905710421584994690047413' },
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
error: '(-32015) VM execution error. (revert)',
|
||||
inputs: [],
|
||||
method_id: 'df0ad3de',
|
||||
name: 'upgradeabilityAdmin',
|
||||
outputs: [
|
||||
{ name: '', type: 'address', value: '' },
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
method_id: '165ec2e2',
|
||||
name: 'arianeeWhitelist',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address',
|
||||
value: '0xd3eee7f8e8021db24825c3457d5479f2b57f40ef',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
method_id: '69598efe',
|
||||
name: 'totalPartitions',
|
||||
constant: true,
|
||||
payable: false,
|
||||
outputs: [
|
||||
{
|
||||
type: 'bytes32[]',
|
||||
name: 'bytes32[]',
|
||||
value: [
|
||||
'0x7265736572766564000000000000000000000000000000000000000000000000',
|
||||
'0x6973737565640000000000000000000000000000000000000000000000000000',
|
||||
],
|
||||
},
|
||||
],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
];
|
||||
|
||||
export const readResultSuccess: SmartContractQueryMethodSuccess = {
|
||||
is_error: false,
|
||||
result: {
|
||||
names: [ 'amount' ],
|
||||
output: [
|
||||
{ type: 'uint256', value: '42' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const readResultError: SmartContractQueryMethodError = {
|
||||
is_error: true,
|
||||
result: {
|
||||
message: 'Some shit happened',
|
||||
code: -32017,
|
||||
raw: '49276d20616c7761797320726576657274696e67207769746820616e206572726f72',
|
||||
},
|
||||
};
|
||||
|
||||
export const write: Array<SmartContractWriteMethod> = [
|
||||
export const write: Array<SmartContractMethodWrite> = [
|
||||
{
|
||||
payable: true,
|
||||
stateMutability: 'payable',
|
||||
|
||||
@@ -4,7 +4,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
import { http } from 'viem';
|
||||
import { WagmiProvider, createConfig } from 'wagmi';
|
||||
import { sepolia } from 'wagmi/chains';
|
||||
import { mock } from 'wagmi/connectors';
|
||||
|
||||
import type { Props as PageProps } from 'nextjs/getServerSideProps';
|
||||
@@ -12,6 +11,7 @@ import type { Props as PageProps } from 'nextjs/getServerSideProps';
|
||||
import config from 'configs/app';
|
||||
import { AppContextProvider } from 'lib/contexts/app';
|
||||
import { SocketProvider } from 'lib/socket/context';
|
||||
import currentChain from 'lib/web3/currentChain';
|
||||
import theme from 'theme';
|
||||
|
||||
import { port as socketPort } from './utils/socket';
|
||||
@@ -36,7 +36,7 @@ const defaultAppContext = {
|
||||
};
|
||||
|
||||
const wagmiConfig = createConfig({
|
||||
chains: [ sepolia ],
|
||||
chains: [ currentChain ],
|
||||
connectors: [
|
||||
mock({
|
||||
accounts: [
|
||||
@@ -45,24 +45,11 @@ const wagmiConfig = createConfig({
|
||||
}),
|
||||
],
|
||||
transports: {
|
||||
[sepolia.id]: http(),
|
||||
[currentChain.id]: http(),
|
||||
},
|
||||
});
|
||||
|
||||
const WalletClientProvider = ({ children, withWalletClient }: { children: React.ReactNode; withWalletClient?: boolean }) => {
|
||||
if (withWalletClient) {
|
||||
return (
|
||||
<WagmiProvider config={ wagmiConfig }>
|
||||
{ children }
|
||||
</WagmiProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{ children }</>;
|
||||
};
|
||||
|
||||
const TestApp = ({ children, withSocket, withWalletClient = true, appContext = defaultAppContext }: Props) => {
|
||||
const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props) => {
|
||||
const [ queryClient ] = React.useState(() => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
@@ -78,9 +65,9 @@ const TestApp = ({ children, withSocket, withWalletClient = true, appContext = d
|
||||
<SocketProvider url={ withSocket ? `ws://${ config.app.host }:${ socketPort }` : undefined }>
|
||||
<AppContextProvider { ...appContext }>
|
||||
<GrowthBookProvider>
|
||||
<WalletClientProvider withWalletClient={ withWalletClient }>
|
||||
<WagmiProvider config={ wagmiConfig }>
|
||||
{ children }
|
||||
</WalletClientProvider>
|
||||
</WagmiProvider>
|
||||
</GrowthBookProvider>
|
||||
</AppContextProvider>
|
||||
</SocketProvider>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { TestFixture, Page } from '@playwright/test';
|
||||
import _isEqual from 'lodash/isEqual';
|
||||
import { encodeFunctionData, encodeFunctionResult, type AbiFunction } from 'viem';
|
||||
|
||||
import { getEnvValue } from 'configs/app/utils';
|
||||
|
||||
interface Params {
|
||||
abiItem: AbiFunction;
|
||||
args?: Array<unknown>;
|
||||
address: string;
|
||||
result: Array<unknown>;
|
||||
}
|
||||
|
||||
export type MockContractReadResponseFixture = (params: Params) => Promise<void>;
|
||||
|
||||
const fixture: TestFixture<MockContractReadResponseFixture, { page: Page }> = async({ page }, use) => {
|
||||
await use(async({ abiItem, args = [], address, result }) => {
|
||||
const rpcUrl = getEnvValue('NEXT_PUBLIC_NETWORK_RPC_URL');
|
||||
|
||||
if (!rpcUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
await page.route(rpcUrl, (route) => {
|
||||
const method = route.request().method();
|
||||
|
||||
if (method !== 'POST') {
|
||||
route.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
const json = route.request().postDataJSON();
|
||||
const params = json?.params?.[0];
|
||||
const id = json?.id;
|
||||
|
||||
const callParams = {
|
||||
data: encodeFunctionData({
|
||||
abi: [ abiItem ],
|
||||
functionName: abiItem.name,
|
||||
args,
|
||||
}),
|
||||
to: address,
|
||||
};
|
||||
|
||||
if (_isEqual(params, callParams) && id) {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
jsonrpc: '2.0',
|
||||
result: encodeFunctionResult({
|
||||
abi: [ abiItem ],
|
||||
functionName: abiItem.name,
|
||||
result,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default fixture;
|
||||
@@ -5,6 +5,7 @@ import * as injectMetaMaskProvider from './fixtures/injectMetaMaskProvider';
|
||||
import * as mockApiResponse from './fixtures/mockApiResponse';
|
||||
import * as mockAssetResponse from './fixtures/mockAssetResponse';
|
||||
import * as mockConfigResponse from './fixtures/mockConfigResponse';
|
||||
import * as mockContractReadResponse from './fixtures/mockContractReadResponse';
|
||||
import * as mockEnvs from './fixtures/mockEnvs';
|
||||
import * as mockFeatures from './fixtures/mockFeatures';
|
||||
import * as mockTextAd from './fixtures/mockTextAd';
|
||||
@@ -16,6 +17,7 @@ interface Fixtures {
|
||||
mockApiResponse: mockApiResponse.MockApiResponseFixture;
|
||||
mockAssetResponse: mockAssetResponse.MockAssetResponseFixture;
|
||||
mockConfigResponse: mockConfigResponse.MockConfigResponseFixture;
|
||||
mockContractReadResponse: mockContractReadResponse.MockContractReadResponseFixture;
|
||||
mockEnvs: mockEnvs.MockEnvsFixture;
|
||||
mockFeatures: mockFeatures.MockFeaturesFixture;
|
||||
createSocket: socketServer.CreateSocketFixture;
|
||||
@@ -28,6 +30,7 @@ const test = base.extend<Fixtures>({
|
||||
mockApiResponse: mockApiResponse.default,
|
||||
mockAssetResponse: mockAssetResponse.default,
|
||||
mockConfigResponse: mockConfigResponse.default,
|
||||
mockContractReadResponse: mockContractReadResponse.default,
|
||||
mockEnvs: mockEnvs.default,
|
||||
mockFeatures: mockFeatures.default,
|
||||
// FIXME: for some reason Playwright does not intercept requests to text ad provider when running multiple tests in parallel
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
| "networks/icon-placeholder"
|
||||
| "networks/logo-placeholder"
|
||||
| "nft_shield"
|
||||
| "open-link"
|
||||
| "output_roots"
|
||||
| "payment_link"
|
||||
| "plus"
|
||||
|
||||
@@ -25,7 +25,7 @@ export const ADDRESS_INFO: Address = {
|
||||
has_tokens: false,
|
||||
has_validated_blocks: false,
|
||||
hash: ADDRESS_HASH,
|
||||
implementations: null,
|
||||
implementations: [ { address: ADDRESS_HASH, name: 'Proxy' } ],
|
||||
is_contract: true,
|
||||
is_verified: true,
|
||||
name: 'ChainLink Token (goerli)',
|
||||
|
||||
@@ -7,16 +7,25 @@ export const CONTRACT_CODE_UNVERIFIED = {
|
||||
creation_bytecode: '0x60806040526e',
|
||||
deployed_bytecode: '0x608060405233',
|
||||
is_self_destructed: false,
|
||||
has_methods_read: true,
|
||||
has_methods_read_proxy: true,
|
||||
has_methods_write: true,
|
||||
has_methods_write_proxy: true,
|
||||
has_custom_methods_read: true,
|
||||
has_custom_methods_write: true,
|
||||
} as SmartContract;
|
||||
|
||||
export const CONTRACT_CODE_VERIFIED = {
|
||||
abi: [],
|
||||
abi: [
|
||||
{
|
||||
inputs: [],
|
||||
name: 'symbol',
|
||||
outputs: [ { internalType: 'string', name: '', type: 'string' } ],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [ { internalType: 'address', name: 'newOwner', type: 'address' } ],
|
||||
name: 'transferOwnership',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
],
|
||||
additional_sources: [],
|
||||
can_be_visualized_via_sol2uml: true,
|
||||
compiler_settings: {
|
||||
@@ -47,12 +56,6 @@ export const CONTRACT_CODE_VERIFIED = {
|
||||
source_code: 'source_code',
|
||||
verified_at: '2023-02-21T14:39:16.906760Z',
|
||||
license_type: 'mit',
|
||||
has_methods_read: true,
|
||||
has_methods_read_proxy: true,
|
||||
has_methods_write: true,
|
||||
has_methods_write_proxy: true,
|
||||
has_custom_methods_read: true,
|
||||
has_custom_methods_write: true,
|
||||
} as unknown as SmartContract;
|
||||
|
||||
export const VERIFIED_CONTRACT_INFO: VerifiedContract = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Abi, AbiType, AbiFallback, AbiFunction, AbiReceive } from 'abitype';
|
||||
import type { Abi, AbiType } from 'abitype';
|
||||
|
||||
export type SmartContractMethodArgType = AbiType;
|
||||
export type SmartContractMethodStateMutability = 'view' | 'nonpayable' | 'payable';
|
||||
@@ -35,13 +35,6 @@ export interface SmartContract {
|
||||
is_verified_via_eth_bytecode_db: boolean | null;
|
||||
is_changed_bytecode: boolean | null;
|
||||
|
||||
has_methods_read: boolean;
|
||||
has_methods_read_proxy: boolean;
|
||||
has_methods_write: boolean;
|
||||
has_methods_write_proxy: boolean;
|
||||
has_custom_methods_read: boolean;
|
||||
has_custom_methods_write: boolean;
|
||||
|
||||
// sourcify info >>>
|
||||
is_verified_via_sourcify: boolean | null;
|
||||
is_fully_verified: boolean | null;
|
||||
@@ -80,47 +73,6 @@ export interface SmartContractExternalLibrary {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type SmartContractMethodOutputValue = string | boolean | object;
|
||||
export type SmartContractMethodOutput = AbiFunction['outputs'][number] & { value?: SmartContractMethodOutputValue };
|
||||
export type SmartContractMethodBase = Omit<AbiFunction, 'outputs'> & {
|
||||
method_id: string;
|
||||
outputs: Array<SmartContractMethodOutput>;
|
||||
constant?: boolean;
|
||||
error?: string;
|
||||
};
|
||||
export type SmartContractReadMethod = SmartContractMethodBase;
|
||||
export type SmartContractWriteMethod = SmartContractMethodBase | AbiFallback | AbiReceive;
|
||||
export type SmartContractMethod = SmartContractReadMethod | SmartContractWriteMethod;
|
||||
|
||||
export interface SmartContractQueryMethodSuccess {
|
||||
is_error: false;
|
||||
result: {
|
||||
names: Array<string | [ string, Array<string> ]>;
|
||||
output: Array<{
|
||||
type: string;
|
||||
value: string | Array<unknown>;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SmartContractQueryMethodError {
|
||||
is_error: true;
|
||||
result: {
|
||||
code: number;
|
||||
message: string;
|
||||
} | {
|
||||
error: string;
|
||||
} | {
|
||||
raw: string;
|
||||
} | {
|
||||
method_call: string;
|
||||
method_id: string;
|
||||
parameters: Array<{ 'name': string; 'type': string; 'value': string }>;
|
||||
};
|
||||
}
|
||||
|
||||
export type SmartContractQueryMethod = SmartContractQueryMethodSuccess | SmartContractQueryMethodError;
|
||||
|
||||
// VERIFICATION
|
||||
|
||||
export type SmartContractVerificationMethod = 'flattened-code' | 'standard-input' | 'sourcify' | 'multi-part'
|
||||
|
||||
@@ -13,9 +13,11 @@ const hash = addressMock.contract.hash;
|
||||
|
||||
test.beforeEach(async({ mockApiResponse }) => {
|
||||
await mockApiResponse('address', addressMock.contract, { pathParams: { hash } });
|
||||
await mockApiResponse('contract', contractInfoMock.verified, { pathParams: { hash } });
|
||||
await mockApiResponse('contract_methods_read', contractMethodsMock.read, { pathParams: { hash }, queryParams: { is_custom_abi: 'false' } });
|
||||
await mockApiResponse('contract_methods_write', contractMethodsMock.write, { pathParams: { hash }, queryParams: { is_custom_abi: 'false' } });
|
||||
await mockApiResponse(
|
||||
'contract',
|
||||
{ ...contractInfoMock.verified, abi: [ ...contractMethodsMock.read, ...contractMethodsMock.write ] },
|
||||
{ pathParams: { hash } },
|
||||
);
|
||||
});
|
||||
|
||||
test.describe('ABI functionality', () => {
|
||||
@@ -41,7 +43,7 @@ test.describe('ABI functionality', () => {
|
||||
},
|
||||
};
|
||||
await mockEnvs(ENVS_MAP.noWalletClient);
|
||||
const component = await render(<AddressContract/>, { hooksConfig }, { withSocket: true, withWalletClient: false });
|
||||
const component = await render(<AddressContract/>, { hooksConfig }, { withSocket: true });
|
||||
const socket = await createSocket();
|
||||
await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase());
|
||||
|
||||
@@ -78,7 +80,7 @@ test.describe('ABI functionality', () => {
|
||||
};
|
||||
await mockEnvs(ENVS_MAP.noWalletClient);
|
||||
|
||||
const component = await render(<AddressContract/>, { hooksConfig }, { withSocket: true, withWalletClient: false });
|
||||
const component = await render(<AddressContract/>, { hooksConfig }, { withSocket: true });
|
||||
const socket = await createSocket();
|
||||
await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase());
|
||||
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import { Checkbox, Flex, chakra } from '@chakra-ui/react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import React from 'react';
|
||||
import { getAddress } from 'viem';
|
||||
|
||||
import type { ContractAbiItemOutput } from './types';
|
||||
|
||||
import { WEI } from 'lib/consts';
|
||||
import { currencyUnits } from 'lib/units';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
|
||||
import { matchInt } from './form/utils';
|
||||
|
||||
function castValueToString(value: number | string | boolean | object | bigint | undefined): string {
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
return value;
|
||||
case 'boolean':
|
||||
return String(value);
|
||||
case 'undefined':
|
||||
return '';
|
||||
case 'number':
|
||||
return value.toLocaleString(undefined, { useGrouping: false });
|
||||
case 'bigint':
|
||||
return value.toString();
|
||||
case 'object':
|
||||
return JSON.stringify(value, undefined, 2);
|
||||
}
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: ContractAbiItemOutput;
|
||||
}
|
||||
|
||||
const ContractAbiItemConstant = ({ data }: Props) => {
|
||||
const [ value, setValue ] = React.useState<string>(castValueToString(data.value));
|
||||
const [ label, setLabel ] = React.useState(currencyUnits.wei.toUpperCase());
|
||||
|
||||
const intMatch = matchInt(data.type);
|
||||
|
||||
const handleCheckboxChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
const initialValue = castValueToString(data.value);
|
||||
|
||||
if (event.target.checked) {
|
||||
setValue(BigNumber(initialValue).div(WEI).toFixed());
|
||||
setLabel(currencyUnits.ether.toUpperCase());
|
||||
} else {
|
||||
setValue(BigNumber(initialValue).toFixed());
|
||||
setLabel(currencyUnits.wei.toUpperCase());
|
||||
}
|
||||
}, [ data.value ]);
|
||||
|
||||
const content = (() => {
|
||||
if (typeof data.value === 'string' && data.type === 'address' && data.value) {
|
||||
return (
|
||||
<AddressEntity
|
||||
address={{ hash: getAddress(data.value) }}
|
||||
noIcon
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <chakra.span wordBreak="break-all" whiteSpace="pre-wrap">({ data.type }): { String(value) }</chakra.span>;
|
||||
})();
|
||||
|
||||
return (
|
||||
<Flex flexDir={{ base: 'column', lg: 'row' }} columnGap={ 2 } rowGap={ 2 }>
|
||||
{ content }
|
||||
{ Number(intMatch?.power) >= 128 && <Checkbox onChange={ handleCheckboxChange }>{ label }</Checkbox> }
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContractAbiItemConstant;
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Flex, chakra } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import type { AbiFunction } from 'viem';
|
||||
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
interface Props {
|
||||
data: AbiFunction['outputs'];
|
||||
}
|
||||
|
||||
const ContractMethodOutputs = ({ data }: Props) => {
|
||||
if (data.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex mt={ 3 } fontSize="sm">
|
||||
<IconSvg name="arrows/down-right" boxSize={ 5 } mr={ 1 }/>
|
||||
<p>
|
||||
{ data.map(({ type, name }, index) => {
|
||||
return (
|
||||
<React.Fragment key={ index }>
|
||||
<chakra.span fontWeight={ 500 }>{ name } </chakra.span>
|
||||
<span>{ name ? `(${ type })` : type }</span>
|
||||
{ index < data.length - 1 && <span>, </span> }
|
||||
</React.Fragment>
|
||||
);
|
||||
}) }
|
||||
</p>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ContractMethodOutputs);
|
||||
@@ -1,29 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { FormSubmitResult, ContractAbiItem } from '../types';
|
||||
|
||||
import ContractMethodResultApi from './ContractMethodResultApi';
|
||||
import ContractMethodResultWalletClient from './ContractMethodResultWalletClient';
|
||||
|
||||
interface Props {
|
||||
abiItem: ContractAbiItem;
|
||||
result: FormSubmitResult;
|
||||
onSettle: () => void;
|
||||
}
|
||||
|
||||
const ContractMethodResult = ({ result, abiItem, onSettle }: Props) => {
|
||||
|
||||
switch (result.source) {
|
||||
case 'api':
|
||||
return <ContractMethodResultApi item={ abiItem } result={ result.result } onSettle={ onSettle }/>;
|
||||
|
||||
case 'wallet_client':
|
||||
return <ContractMethodResultWalletClient result={ result.result } onSettle={ onSettle }/>;
|
||||
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default React.memo(ContractMethodResult);
|
||||
@@ -1,107 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { FormSubmitResultApi } from '../types';
|
||||
|
||||
import * as contractMethodsMock from 'mocks/contract/methods';
|
||||
import { test, expect } from 'playwright/lib';
|
||||
|
||||
import ContractMethodResultApi from './ContractMethodResultApi';
|
||||
|
||||
const item = contractMethodsMock.read[0];
|
||||
const onSettle = () => Promise.resolve();
|
||||
|
||||
test.use({ viewport: { width: 500, height: 500 } });
|
||||
|
||||
test('default error', async({ render }) => {
|
||||
const result: FormSubmitResultApi['result'] = {
|
||||
is_error: true,
|
||||
result: {
|
||||
error: 'I am an error',
|
||||
},
|
||||
};
|
||||
const component = await render(<ContractMethodResultApi item={ item } onSettle={ onSettle } result={ result }/>);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('error with code', async({ render }) => {
|
||||
const result: FormSubmitResultApi['result'] = {
|
||||
is_error: true,
|
||||
result: {
|
||||
message: 'I am an error',
|
||||
code: -32017,
|
||||
},
|
||||
};
|
||||
const component = await render(<ContractMethodResultApi item={ item } onSettle={ onSettle } result={ result }/>);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('raw error', async({ render }) => {
|
||||
const result: FormSubmitResultApi['result'] = {
|
||||
is_error: true,
|
||||
result: {
|
||||
raw: '49276d20616c7761797320726576657274696e67207769746820616e206572726f72',
|
||||
},
|
||||
};
|
||||
const component = await render(<ContractMethodResultApi item={ item } onSettle={ onSettle } result={ result }/>);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('complex error', async({ render }) => {
|
||||
const result: FormSubmitResultApi['result'] = {
|
||||
is_error: true,
|
||||
result: {
|
||||
method_call: 'SomeCustomError(address addr, uint256 balance)',
|
||||
method_id: '50289a9f',
|
||||
parameters: [
|
||||
{ name: 'addr', type: 'address', value: '0x850e73b42f48e91ebaedf8f00a74f6147e485c5a' },
|
||||
{ name: 'balance', type: 'uint256', value: '14' },
|
||||
],
|
||||
},
|
||||
};
|
||||
const component = await render(<ContractMethodResultApi item={ item } onSettle={ onSettle } result={ result }/>);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('success', async({ render }) => {
|
||||
const result: FormSubmitResultApi['result'] = {
|
||||
is_error: false,
|
||||
result: {
|
||||
names: [ 'address' ],
|
||||
output: [ { type: 'address', value: '0x0000000000000000000000000000000000000000' } ],
|
||||
},
|
||||
};
|
||||
const component = await render(<ContractMethodResultApi item={ item } onSettle={ onSettle } result={ result }/>);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('complex success', async({ render }) => {
|
||||
const result: FormSubmitResultApi['result'] = {
|
||||
is_error: false,
|
||||
result: {
|
||||
names: [
|
||||
[
|
||||
'data',
|
||||
[ 'totalSupply', 'owner', 'symbol' ],
|
||||
],
|
||||
'supports721',
|
||||
'page',
|
||||
],
|
||||
output: [
|
||||
{
|
||||
type: 'tuple[uint256,address,string]',
|
||||
value: [ 1000, '0xe150519ae293922cfe6217feba3add4726f5e851', 'AOC_INCUBATORS' ],
|
||||
},
|
||||
{ type: 'bool', value: 'true' },
|
||||
{ type: 'uint256[]', value: [ 1, 2, 3, 4, 5 ] },
|
||||
],
|
||||
},
|
||||
};
|
||||
const component = await render(<ContractMethodResultApi item={ item } onSettle={ onSettle } result={ result }/>);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -1,64 +0,0 @@
|
||||
import { Box, chakra, useColorModeValue } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { ContractAbiItem, FormSubmitResultApi } from '../types';
|
||||
|
||||
import hexToUtf8 from 'lib/hexToUtf8';
|
||||
|
||||
import ContractMethodResultApiError from './ContractMethodResultApiError';
|
||||
import ContractMethodResultApiItem from './ContractMethodResultApiItem';
|
||||
|
||||
interface Props {
|
||||
item: ContractAbiItem;
|
||||
result: FormSubmitResultApi['result'];
|
||||
onSettle: () => void;
|
||||
}
|
||||
|
||||
const ContractMethodResultApi = ({ item, result, onSettle }: Props) => {
|
||||
const resultBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
|
||||
|
||||
React.useEffect(() => {
|
||||
onSettle();
|
||||
}, [ onSettle ]);
|
||||
|
||||
if ('status' in result) {
|
||||
return <ContractMethodResultApiError>{ result.statusText }</ContractMethodResultApiError>;
|
||||
}
|
||||
|
||||
if (result instanceof Error) {
|
||||
return <ContractMethodResultApiError>{ result.message }</ContractMethodResultApiError>;
|
||||
}
|
||||
|
||||
if (result.is_error) {
|
||||
if ('error' in result.result) {
|
||||
return <ContractMethodResultApiError>{ result.result.error }</ContractMethodResultApiError>;
|
||||
}
|
||||
|
||||
if ('message' in result.result) {
|
||||
return <ContractMethodResultApiError>[{ result.result.code }] { result.result.message }</ContractMethodResultApiError>;
|
||||
}
|
||||
|
||||
if ('raw' in result.result) {
|
||||
return <ContractMethodResultApiError>{ `Revert reason: ${ hexToUtf8(result.result.raw) }` }</ContractMethodResultApiError>;
|
||||
}
|
||||
|
||||
if ('method_id' in result.result) {
|
||||
return <ContractMethodResultApiError>{ JSON.stringify(result.result, undefined, 2) }</ContractMethodResultApiError>;
|
||||
}
|
||||
|
||||
return <ContractMethodResultApiError>Something went wrong.</ContractMethodResultApiError>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box mt={ 3 } p={ 4 } borderRadius="md" bgColor={ resultBgColor } fontSize="sm" whiteSpace="break-spaces" wordBreak="break-all">
|
||||
<p>
|
||||
[ <chakra.span fontWeight={ 500 }>{ 'name' in item ? item.name : '' }</chakra.span> method response ]
|
||||
</p>
|
||||
<p>[</p>
|
||||
{ result.result.output.map((output, index) => <ContractMethodResultApiItem key={ index } output={ output } name={ result.result.names[index] }/>) }
|
||||
<p>]</p>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ContractMethodResultApi);
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Alert } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const ContractMethodResultApiError = ({ children }: Props) => {
|
||||
return (
|
||||
<Alert status="error" mt={ 3 } p={ 4 } borderRadius="md" fontSize="sm" wordBreak="break-word" whiteSpace="pre-wrap">
|
||||
{ children }
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ContractMethodResultApiError);
|
||||
@@ -1,45 +0,0 @@
|
||||
import { chakra } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { SmartContractQueryMethodSuccess } from 'types/api/contract';
|
||||
|
||||
const TUPLE_TYPE_REGEX = /\[(.+)\]/;
|
||||
|
||||
interface Props {
|
||||
output: SmartContractQueryMethodSuccess['result']['output'][0];
|
||||
name: SmartContractQueryMethodSuccess['result']['names'][0];
|
||||
}
|
||||
|
||||
const ContractMethodResultApiItem = ({ output, name }: Props) => {
|
||||
if (Array.isArray(name)) {
|
||||
const [ structName, argNames ] = name;
|
||||
const argTypes = output.type.match(TUPLE_TYPE_REGEX)?.[1].split(',');
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
<chakra.span fontWeight={ 500 }> { structName }</chakra.span>
|
||||
<span> ({ output.type }) :</span>
|
||||
</p>
|
||||
{ argNames.map((argName, argIndex) => {
|
||||
return (
|
||||
<p key={ argName }>
|
||||
<chakra.span fontWeight={ 500 }> { argName }</chakra.span>
|
||||
<span>{ argTypes?.[argIndex] ? ` (${ argTypes[argIndex] })` : '' } : { String(output.value[argIndex]) }</span>
|
||||
</p>
|
||||
);
|
||||
}) }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<p>
|
||||
<span> </span>
|
||||
{ name && <chakra.span fontWeight={ 500 }>{ name } </chakra.span> }
|
||||
<span>({ output.type }) : { String(output.value) }</span>
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ContractMethodResultApiItem);
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
@@ -1,25 +0,0 @@
|
||||
import type { AbiFunction } from 'abitype';
|
||||
|
||||
import type { SmartContractMethod, SmartContractMethodOutput, SmartContractQueryMethod } from 'types/api/contract';
|
||||
|
||||
import type { ResourceError } from 'lib/api/resources';
|
||||
|
||||
export type ContractAbiItemInput = AbiFunction['inputs'][number] & { fieldType?: 'native_coin' };
|
||||
export type ContractAbiItemOutput = SmartContractMethodOutput;
|
||||
export type ContractAbiItem = SmartContractMethod;
|
||||
export type ContractAbi = Array<ContractAbiItem>;
|
||||
|
||||
export type MethodType = 'read' | 'write';
|
||||
export type MethodCallStrategy = 'api' | 'wallet_client';
|
||||
|
||||
export interface FormSubmitResultApi {
|
||||
source: 'api';
|
||||
result: SmartContractQueryMethod | ResourceError | Error;
|
||||
}
|
||||
export interface FormSubmitResultWalletClient {
|
||||
source: 'wallet_client';
|
||||
result: Error | { hash: `0x${ string }` | undefined } | undefined;
|
||||
}
|
||||
export type FormSubmitResult = FormSubmitResultApi | FormSubmitResultWalletClient;
|
||||
|
||||
export type FormSubmitHandler = (item: ContractAbiItem, args: Array<unknown>, submitType: MethodCallStrategy | undefined) => Promise<FormSubmitResult>;
|
||||
@@ -1,51 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { FormSubmitResult } from './types';
|
||||
import type { SmartContractQueryMethod } from 'types/api/contract';
|
||||
|
||||
import type { ResourceError } from 'lib/api/resources';
|
||||
import useApiFetch from 'lib/api/useApiFetch';
|
||||
import useAccount from 'lib/web3/useAccount';
|
||||
|
||||
interface Params {
|
||||
methodId: string;
|
||||
args: Array<unknown>;
|
||||
isProxy: boolean;
|
||||
isCustomAbi: boolean;
|
||||
addressHash: string;
|
||||
}
|
||||
|
||||
export default function useCallMethodApi(): (params: Params) => Promise<FormSubmitResult> {
|
||||
const apiFetch = useApiFetch();
|
||||
const { address } = useAccount();
|
||||
|
||||
return React.useCallback(async({ addressHash, isCustomAbi, isProxy, args, methodId }) => {
|
||||
try {
|
||||
const response = await apiFetch<'contract_method_query', SmartContractQueryMethod>('contract_method_query', {
|
||||
pathParams: { hash: addressHash },
|
||||
queryParams: {
|
||||
is_custom_abi: isCustomAbi ? 'true' : 'false',
|
||||
},
|
||||
fetchParams: {
|
||||
method: 'POST',
|
||||
body: {
|
||||
args,
|
||||
method_id: methodId,
|
||||
contract_type: isProxy ? 'proxy' : 'regular',
|
||||
from: address,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
source: 'api',
|
||||
result: response,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
source: 'api',
|
||||
result: error as (Error | ResourceError),
|
||||
};
|
||||
}
|
||||
}, [ address, apiFetch ]);
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { FormSubmitHandler } from './types';
|
||||
|
||||
import config from 'configs/app';
|
||||
|
||||
import useCallMethodApi from './useCallMethodApi';
|
||||
import useCallMethodWalletClient from './useCallMethodWalletClient';
|
||||
|
||||
interface Params {
|
||||
tab: string;
|
||||
addressHash: string;
|
||||
}
|
||||
|
||||
function useFormSubmit({ tab, addressHash }: Params): FormSubmitHandler {
|
||||
const callMethodApi = useCallMethodApi();
|
||||
const callMethodWalletClient = useCallMethodWalletClient();
|
||||
|
||||
return React.useCallback(async(item, args, strategy) => {
|
||||
switch (strategy) {
|
||||
case 'api': {
|
||||
if (!('method_id' in item)) {
|
||||
throw new Error('Method ID is not defined');
|
||||
}
|
||||
return callMethodApi({
|
||||
args,
|
||||
methodId: item.method_id,
|
||||
addressHash,
|
||||
isCustomAbi: tab === 'read_custom_methods' || tab === 'write_custom_methods',
|
||||
isProxy: tab === 'read_proxy' || tab === 'write_proxy',
|
||||
});
|
||||
}
|
||||
case 'wallet_client': {
|
||||
return callMethodWalletClient({ args, item, addressHash });
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new Error(`Unknown call strategy "${ strategy }"`);
|
||||
}
|
||||
}
|
||||
}, [ addressHash, callMethodApi, callMethodWalletClient, tab ]);
|
||||
}
|
||||
|
||||
function useFormSubmitFallback({ tab, addressHash }: Params): FormSubmitHandler {
|
||||
const callMethodApi = useCallMethodApi();
|
||||
|
||||
return React.useCallback(async(item, args, strategy) => {
|
||||
switch (strategy) {
|
||||
case 'api': {
|
||||
if (!('method_id' in item)) {
|
||||
throw new Error('Method ID is not defined');
|
||||
}
|
||||
return callMethodApi({
|
||||
args,
|
||||
methodId: item.method_id,
|
||||
addressHash,
|
||||
isCustomAbi: tab === 'read_custom_methods' || tab === 'write_custom_methods',
|
||||
isProxy: tab === 'read_proxy' || tab === 'write_proxy',
|
||||
});
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new Error(`Unknown call strategy "${ strategy }"`);
|
||||
}
|
||||
}
|
||||
|
||||
}, [ addressHash, callMethodApi, tab ]);
|
||||
}
|
||||
|
||||
const hook = config.features.blockchainInteraction.isEnabled ? useFormSubmit : useFormSubmitFallback;
|
||||
|
||||
export default hook;
|
||||
@@ -1,7 +0,0 @@
|
||||
export const getNativeCoinValue = (value: unknown) => {
|
||||
if (typeof value !== 'string') {
|
||||
return BigInt(0);
|
||||
}
|
||||
|
||||
return BigInt(value);
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
|
||||
import type { Address as TAddress } from 'types/api/address';
|
||||
|
||||
import { getResourceKey } from 'lib/api/useApiQuery';
|
||||
import ContainerWithScrollY from 'ui/shared/ContainerWithScrollY';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
|
||||
interface Props {
|
||||
hash: string | undefined;
|
||||
}
|
||||
|
||||
const ContractImplementationAddress = ({ hash }: Props) => {
|
||||
const queryClient = useQueryClient();
|
||||
const data = queryClient.getQueryData<TAddress>(getResourceKey('address', {
|
||||
pathParams: { hash },
|
||||
}));
|
||||
|
||||
if (!data?.implementations || data.implementations.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = data.implementations.length > 1 ? 'Implementation addresses:' : 'Implementation address:';
|
||||
|
||||
return (
|
||||
<Flex mb={ 6 } flexWrap="wrap" columnGap={ 2 } rowGap={ 2 }>
|
||||
<span>{ label }</span>
|
||||
<Box position="relative" maxW="100%">
|
||||
<ContainerWithScrollY
|
||||
gradientHeight={ 24 }
|
||||
rowGap={ 2 }
|
||||
maxH="150px"
|
||||
>
|
||||
{ data.implementations.map((item) => (
|
||||
<AddressEntity
|
||||
key={ item.address }
|
||||
address={{ hash: item.address, is_contract: true }}
|
||||
noIcon
|
||||
noCopy
|
||||
/>
|
||||
)) }
|
||||
</ContainerWithScrollY>
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ContractImplementationAddress);
|
||||
@@ -1,39 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import buildUrl from 'lib/api/buildUrl';
|
||||
import * as contractMethodsMock from 'mocks/contract/methods';
|
||||
import { test, expect } from 'playwright/lib';
|
||||
|
||||
import ContractRead from './ContractRead';
|
||||
|
||||
const addressHash = 'hash';
|
||||
const hooksConfig = {
|
||||
router: {
|
||||
query: { hash: addressHash },
|
||||
},
|
||||
};
|
||||
|
||||
test('base view +@mobile +@dark-mode', async({ render, mockApiResponse, page }) => {
|
||||
await mockApiResponse(
|
||||
'contract_methods_read',
|
||||
contractMethodsMock.read,
|
||||
{ pathParams: { hash: addressHash }, queryParams: { is_custom_abi: false } },
|
||||
);
|
||||
const CONTRACT_QUERY_METHOD_API_URL = buildUrl('contract_method_query', { hash: addressHash }, { is_custom_abi: false });
|
||||
await page.route(CONTRACT_QUERY_METHOD_API_URL, (route) => route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify(contractMethodsMock.readResultSuccess),
|
||||
}));
|
||||
const component = await render(<ContractRead/>, { hooksConfig });
|
||||
|
||||
await component.getByText(/expand all/i).click();
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
|
||||
await component.getByPlaceholder(/address/i).fill('0xa113Ce24919C08a26C952E81681dAc861d6a2466');
|
||||
await component.getByText(/read/i).click();
|
||||
|
||||
await component.getByText(/wei/i).click();
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -1,62 +0,0 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import config from 'configs/app';
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import useAccount from 'lib/web3/useAccount';
|
||||
import ContentLoader from 'ui/shared/ContentLoader';
|
||||
import DataFetchAlert from 'ui/shared/DataFetchAlert';
|
||||
|
||||
import ContractAbi from './ABI/ContractAbi';
|
||||
import ContractConnectWallet from './ContractConnectWallet';
|
||||
import ContractCustomAbiAlert from './ContractCustomAbiAlert';
|
||||
import ContractImplementationAddress from './ContractImplementationAddress';
|
||||
|
||||
interface Props {
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const ContractRead = ({ isLoading }: Props) => {
|
||||
const { address } = useAccount();
|
||||
const router = useRouter();
|
||||
|
||||
const tab = getQueryParamString(router.query.tab);
|
||||
const addressHash = getQueryParamString(router.query.hash);
|
||||
const isProxy = tab === 'read_proxy';
|
||||
const isCustomAbi = tab === 'read_custom_methods';
|
||||
|
||||
const { data, isPending, isError } = useApiQuery(isProxy ? 'contract_methods_read_proxy' : 'contract_methods_read', {
|
||||
pathParams: { hash: addressHash },
|
||||
queryParams: {
|
||||
is_custom_abi: isCustomAbi ? 'true' : 'false',
|
||||
from: address,
|
||||
},
|
||||
queryOptions: {
|
||||
enabled: !isLoading,
|
||||
},
|
||||
});
|
||||
|
||||
if (isError) {
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
|
||||
if (isPending) {
|
||||
return <ContentLoader/>;
|
||||
}
|
||||
|
||||
if (data.length === 0 && !isProxy) {
|
||||
return <span>No public read functions were found for this contract.</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{ isCustomAbi && <ContractCustomAbiAlert/> }
|
||||
{ config.features.blockchainInteraction.isEnabled && <ContractConnectWallet/> }
|
||||
{ isProxy && <ContractImplementationAddress hash={ addressHash }/> }
|
||||
<ContractAbi data={ data } addressHash={ addressHash } tab={ tab } methodType="read"/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ContractRead);
|
||||
@@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import * as contractMethodsMock from 'mocks/contract/methods';
|
||||
import { test, expect } from 'playwright/lib';
|
||||
|
||||
import ContractWrite from './ContractWrite';
|
||||
|
||||
const addressHash = 'hash';
|
||||
const hooksConfig = {
|
||||
router: {
|
||||
query: { hash: addressHash },
|
||||
},
|
||||
};
|
||||
|
||||
test('base view +@mobile', async({ render, mockApiResponse }) => {
|
||||
await mockApiResponse(
|
||||
'contract_methods_write',
|
||||
contractMethodsMock.write,
|
||||
{ pathParams: { hash: addressHash }, queryParams: { is_custom_abi: false } },
|
||||
);
|
||||
const component = await render(<ContractWrite/>, { hooksConfig });
|
||||
await component.getByText(/expand all/i).click();
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -1,60 +0,0 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import config from 'configs/app';
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import ContentLoader from 'ui/shared/ContentLoader';
|
||||
import DataFetchAlert from 'ui/shared/DataFetchAlert';
|
||||
|
||||
import ContractAbi from './ABI/ContractAbi';
|
||||
import ContractConnectWallet from './ContractConnectWallet';
|
||||
import ContractCustomAbiAlert from './ContractCustomAbiAlert';
|
||||
import ContractImplementationAddress from './ContractImplementationAddress';
|
||||
|
||||
interface Props {
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const ContractWrite = ({ isLoading }: Props) => {
|
||||
const router = useRouter();
|
||||
|
||||
const tab = getQueryParamString(router.query.tab);
|
||||
const addressHash = getQueryParamString(router.query.hash);
|
||||
const isProxy = tab === 'write_proxy';
|
||||
const isCustomAbi = tab === 'write_custom_methods';
|
||||
|
||||
const { data, isPending, isError } = useApiQuery(isProxy ? 'contract_methods_write_proxy' : 'contract_methods_write', {
|
||||
pathParams: { hash: addressHash },
|
||||
queryParams: {
|
||||
is_custom_abi: isCustomAbi ? 'true' : 'false',
|
||||
},
|
||||
queryOptions: {
|
||||
enabled: !isLoading,
|
||||
refetchOnMount: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (isError) {
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
|
||||
if (isPending) {
|
||||
return <ContentLoader/>;
|
||||
}
|
||||
|
||||
if (data.length === 0 && !isProxy) {
|
||||
return <span>No public write functions were found for this contract.</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{ isCustomAbi && <ContractCustomAbiAlert/> }
|
||||
{ config.features.blockchainInteraction.isEnabled && <ContractConnectWallet/> }
|
||||
{ isProxy && <ContractImplementationAddress hash={ addressHash }/> }
|
||||
<ContractAbi data={ data } addressHash={ addressHash } tab={ tab } methodType="write"/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ContractWrite);
|
||||
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 150 KiB |
@@ -2,64 +2,59 @@ import { Accordion, Box, Flex, Link } from '@chakra-ui/react';
|
||||
import _range from 'lodash/range';
|
||||
import React from 'react';
|
||||
|
||||
import type { MethodType, ContractAbi as TContractAbi } from './types';
|
||||
import type { SmartContractMethod } from './types';
|
||||
|
||||
import ContractAbiItem from './ContractAbiItem';
|
||||
import useFormSubmit from './useFormSubmit';
|
||||
import useScrollToMethod from './useScrollToMethod';
|
||||
|
||||
interface Props {
|
||||
data: TContractAbi;
|
||||
abi: Array<SmartContractMethod>;
|
||||
addressHash: string;
|
||||
tab: string;
|
||||
methodType: MethodType;
|
||||
}
|
||||
|
||||
const ContractAbi = ({ data, addressHash, tab, methodType }: Props) => {
|
||||
const [ expandedSections, setExpandedSections ] = React.useState<Array<number>>(data.length === 1 ? [ 0 ] : []);
|
||||
const ContractAbi = ({ abi, addressHash, tab }: Props) => {
|
||||
const [ expandedSections, setExpandedSections ] = React.useState<Array<number>>(abi.length === 1 ? [ 0 ] : []);
|
||||
const [ id, setId ] = React.useState(0);
|
||||
|
||||
useScrollToMethod(data, setExpandedSections);
|
||||
useScrollToMethod(abi, setExpandedSections);
|
||||
|
||||
const handleFormSubmit = useFormSubmit({ addressHash, tab });
|
||||
const handleFormSubmit = useFormSubmit({ addressHash });
|
||||
|
||||
const handleAccordionStateChange = React.useCallback((newValue: Array<number>) => {
|
||||
setExpandedSections(newValue);
|
||||
}, []);
|
||||
|
||||
const handleExpandAll = React.useCallback(() => {
|
||||
if (!data) {
|
||||
if (!abi) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (expandedSections.length < data.length) {
|
||||
setExpandedSections(_range(0, data.length));
|
||||
if (expandedSections.length < abi.length) {
|
||||
setExpandedSections(_range(0, abi.length));
|
||||
} else {
|
||||
setExpandedSections([]);
|
||||
}
|
||||
}, [ data, expandedSections.length ]);
|
||||
}, [ abi, expandedSections.length ]);
|
||||
|
||||
const handleReset = React.useCallback(() => {
|
||||
setId((id) => id + 1);
|
||||
}, []);
|
||||
|
||||
if (data.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex mb={ 3 }>
|
||||
<Box fontWeight={ 500 } mr="auto">Contract information</Box>
|
||||
{ data.length > 1 && (
|
||||
{ abi.length > 1 && (
|
||||
<Link onClick={ handleExpandAll }>
|
||||
{ expandedSections.length === data.length ? 'Collapse' : 'Expand' } all
|
||||
{ expandedSections.length === abi.length ? 'Collapse' : 'Expand' } all
|
||||
</Link>
|
||||
) }
|
||||
<Link onClick={ handleReset } ml={ 3 }>Reset</Link>
|
||||
</Flex>
|
||||
<Accordion allowMultiple position="relative" onChange={ handleAccordionStateChange } index={ expandedSections }>
|
||||
{ data.map((item, index) => (
|
||||
{ abi.map((item, index) => (
|
||||
<ContractAbiItem
|
||||
key={ index }
|
||||
data={ item }
|
||||
@@ -68,7 +63,6 @@ const ContractAbi = ({ data, addressHash, tab, methodType }: Props) => {
|
||||
addressHash={ addressHash }
|
||||
tab={ tab }
|
||||
onSubmit={ handleFormSubmit }
|
||||
methodType={ methodType }
|
||||
/>
|
||||
)) }
|
||||
</Accordion>
|
||||
@@ -1,8 +1,8 @@
|
||||
import { AccordionButton, AccordionIcon, AccordionItem, AccordionPanel, Alert, Box, Flex, Tooltip, useClipboard, useDisclosure } from '@chakra-ui/react';
|
||||
import { AccordionButton, AccordionIcon, AccordionItem, AccordionPanel, Box, Tooltip, useClipboard, useDisclosure } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { Element } from 'react-scroll';
|
||||
|
||||
import type { FormSubmitHandler, MethodType, ContractAbiItem as TContractAbiItem } from './types';
|
||||
import type { FormSubmitHandler, SmartContractMethod } from './types';
|
||||
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
@@ -12,21 +12,21 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard';
|
||||
import Hint from 'ui/shared/Hint';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
import ContractAbiItemConstant from './ContractAbiItemConstant';
|
||||
import ContractMethodForm from './form/ContractMethodForm';
|
||||
import { getElementName } from './useScrollToMethod';
|
||||
|
||||
interface Props {
|
||||
data: TContractAbiItem;
|
||||
data: SmartContractMethod;
|
||||
index: number;
|
||||
id: number;
|
||||
addressHash: string;
|
||||
tab: string;
|
||||
onSubmit: FormSubmitHandler;
|
||||
methodType: MethodType;
|
||||
}
|
||||
|
||||
const ContractAbiItem = ({ data, index, id, addressHash, tab, onSubmit, methodType }: Props) => {
|
||||
const ContractAbiItem = ({ data, index, id, addressHash, tab, onSubmit }: Props) => {
|
||||
const [ attempt, setAttempt ] = React.useState(0);
|
||||
|
||||
const url = React.useMemo(() => {
|
||||
if (!('method_id' in data)) {
|
||||
return '';
|
||||
@@ -43,7 +43,7 @@ const ContractAbiItem = ({ data, index, id, addressHash, tab, onSubmit, methodTy
|
||||
}, [ addressHash, data, tab ]);
|
||||
|
||||
const { hasCopied, onCopy } = useClipboard(url, 1000);
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const methodIdTooltip = useDisclosure();
|
||||
|
||||
const handleCopyLinkClick = React.useCallback((event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
@@ -54,30 +54,9 @@ const ContractAbiItem = ({ data, index, id, addressHash, tab, onSubmit, methodTy
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const content = (() => {
|
||||
if ('error' in data && data.error) {
|
||||
return <Alert status="error" fontSize="sm" wordBreak="break-word">{ data.error }</Alert>;
|
||||
}
|
||||
|
||||
const hasConstantOutputs = 'outputs' in data && data.outputs.some(({ value }) => value !== undefined && value !== null);
|
||||
|
||||
if (hasConstantOutputs) {
|
||||
return (
|
||||
<Flex flexDir="column" rowGap={ 1 }>
|
||||
{ data.outputs.map((output, index) => <ContractAbiItemConstant key={ index } data={ output }/>) }
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ContractMethodForm
|
||||
key={ id + '_' + index }
|
||||
data={ data }
|
||||
onSubmit={ onSubmit }
|
||||
methodType={ methodType }
|
||||
/>
|
||||
);
|
||||
})();
|
||||
const handleReset = React.useCallback(() => {
|
||||
setAttempt((prev) => prev + 1);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AccordionItem as="section" _first={{ borderTopWidth: 0 }} _last={{ borderBottomWidth: 0 }}>
|
||||
@@ -86,15 +65,15 @@ const ContractAbiItem = ({ data, index, id, addressHash, tab, onSubmit, methodTy
|
||||
<Element as="h2" name={ 'method_id' in data ? getElementName(data.method_id) : '' }>
|
||||
<AccordionButton px={ 0 } py={ 3 } _hover={{ bgColor: 'inherit' }} wordBreak="break-all" textAlign="left" as="div" cursor="pointer">
|
||||
{ 'method_id' in data && (
|
||||
<Tooltip label={ hasCopied ? 'Copied!' : 'Copy link' } isOpen={ isOpen || hasCopied } onClose={ onClose }>
|
||||
<Tooltip label={ hasCopied ? 'Copied!' : 'Copy link' } isOpen={ methodIdTooltip.isOpen || hasCopied } onClose={ methodIdTooltip.onClose }>
|
||||
<Box
|
||||
boxSize={ 5 }
|
||||
color="text_secondary"
|
||||
_hover={{ color: 'link_hovered' }}
|
||||
mr={ 2 }
|
||||
onClick={ handleCopyLinkClick }
|
||||
onMouseEnter={ onOpen }
|
||||
onMouseLeave={ onClose }
|
||||
onMouseEnter={ methodIdTooltip.onOpen }
|
||||
onMouseLeave={ methodIdTooltip.onClose }
|
||||
>
|
||||
<IconSvg name="link" boxSize={ 5 }/>
|
||||
</Box>
|
||||
@@ -131,7 +110,14 @@ const ContractAbiItem = ({ data, index, id, addressHash, tab, onSubmit, methodTy
|
||||
</AccordionButton>
|
||||
</Element>
|
||||
<AccordionPanel pb={ 4 } pr={ 0 } pl="28px" w="calc(100% - 6px)">
|
||||
{ content }
|
||||
<ContractMethodForm
|
||||
key={ id + '_' + index + '_' + attempt }
|
||||
data={ data }
|
||||
attempt={ attempt }
|
||||
onSubmit={ onSubmit }
|
||||
onReset={ handleReset }
|
||||
isOpen={ isExpanded }
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</>
|
||||
) }
|
||||
@@ -1,11 +1,16 @@
|
||||
import { Alert, Button, Flex } from '@chakra-ui/react';
|
||||
import { Alert, Button, Flex, Skeleton } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import config from 'configs/app';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import useWallet from 'ui/snippets/walletMenu/useWallet';
|
||||
|
||||
const ContractConnectWallet = () => {
|
||||
interface Props {
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const ContractConnectWallet = ({ isLoading }: Props) => {
|
||||
const { isModalOpening, isModalOpen, connect, disconnect, address, isWalletConnected } = useWallet({ source: 'Smart contracts' });
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
@@ -44,7 +49,15 @@ const ContractConnectWallet = () => {
|
||||
);
|
||||
})();
|
||||
|
||||
return <Alert mb={ 6 } status={ address ? 'success' : 'warning' }>{ content }</Alert>;
|
||||
return (
|
||||
<Skeleton isLoaded={ !isLoading } mb={ 6 }>
|
||||
<Alert status={ address ? 'success' : 'warning' }>
|
||||
{ content }
|
||||
</Alert>
|
||||
</Skeleton>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContractConnectWallet;
|
||||
const Fallback = () => null;
|
||||
|
||||
export default config.features.blockchainInteraction.isEnabled ? ContractConnectWallet : Fallback;
|
||||
@@ -1,13 +1,19 @@
|
||||
import { Alert } from '@chakra-ui/react';
|
||||
import { Alert, Skeleton } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
const ContractCustomAbiAlert = () => {
|
||||
interface Props {
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const ContractCustomAbiAlert = ({ isLoading }: Props) => {
|
||||
return (
|
||||
<Alert status="warning" mb={ 4 }>
|
||||
<Skeleton isLoaded={ !isLoading }>
|
||||
<Alert status="warning" mb={ 6 }>
|
||||
Note: Contract with custom ABI is only meant for debugging purpose and it is the user’s responsibility to ensure that the provided ABI
|
||||
matches the contract, otherwise errors may occur or results returned may be incorrect.
|
||||
Blockscout is not responsible for any losses that arise from the use of Read & Write contract.
|
||||
</Alert>
|
||||
</Alert>
|
||||
</Skeleton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Flex, Select, Skeleton } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { AddressImplementation } from 'types/api/addressParams';
|
||||
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
import CopyToClipboard from 'ui/shared/CopyToClipboard';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import LinkNewTab from 'ui/shared/links/LinkNewTab';
|
||||
|
||||
interface Props {
|
||||
selectedItem: AddressImplementation;
|
||||
onItemSelect: (event: React.ChangeEvent<HTMLSelectElement>) => void;
|
||||
implementations: Array<AddressImplementation>;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const ContractImplementationAddress = ({ selectedItem, onItemSelect, implementations, isLoading }: Props) => {
|
||||
|
||||
if (isLoading) {
|
||||
return <Skeleton mb={ 6 } h={ 6 } w={{ base: '300px', lg: '500px' }}/>;
|
||||
}
|
||||
|
||||
if (implementations.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (implementations.length === 1) {
|
||||
return (
|
||||
<Flex mb={ 6 } flexWrap="wrap" columnGap={ 2 } rowGap={ 2 }>
|
||||
<span>Implementation address:</span>
|
||||
<AddressEntity
|
||||
address={{ hash: implementations[0].address, is_contract: true, is_verified: true }}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex mb={ 6 } flexWrap="wrap" columnGap={ 2 } rowGap={ 2 } alignItems="center">
|
||||
<span>Implementation address:</span>
|
||||
<Select
|
||||
size="xs"
|
||||
value={ selectedItem.address }
|
||||
onChange={ onItemSelect }
|
||||
w="auto"
|
||||
fontWeight={ 600 }
|
||||
borderRadius="base"
|
||||
>
|
||||
{ implementations.map((implementation) => (
|
||||
<option key={ implementation.address } value={ implementation.address }>
|
||||
{ implementation.name }
|
||||
</option>
|
||||
)) }
|
||||
</Select>
|
||||
<CopyToClipboard text={ selectedItem.address } ml={ 1 }/>
|
||||
<LinkNewTab
|
||||
label="Open contract details page in new tab"
|
||||
href={ route({ pathname: '/address/[hash]', query: { hash: selectedItem.address, tab: 'contract' } }) }
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ContractImplementationAddress);
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { MethodType, SmartContractMethod } from './types';
|
||||
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import ContentLoader from 'ui/shared/ContentLoader';
|
||||
import DataFetchAlert from 'ui/shared/DataFetchAlert';
|
||||
|
||||
import ContractAbi from './ContractAbi';
|
||||
|
||||
interface Props {
|
||||
abi: Array<SmartContractMethod>;
|
||||
isLoading?: boolean;
|
||||
isError?: boolean;
|
||||
type: MethodType;
|
||||
}
|
||||
|
||||
const ContractMethods = ({ abi, isLoading, isError, type }: Props) => {
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const tab = getQueryParamString(router.query.tab);
|
||||
const addressHash = getQueryParamString(router.query.hash);
|
||||
|
||||
if (isLoading) {
|
||||
return <ContentLoader/>;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
|
||||
if (abi.length === 0) {
|
||||
return <span>No public { type } functions were found for this contract.</span>;
|
||||
}
|
||||
|
||||
return <ContractAbi abi={ abi } tab={ tab } addressHash={ addressHash }/>;
|
||||
};
|
||||
|
||||
export default React.memo(ContractMethods);
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { MethodType, SmartContractMethod } from './types';
|
||||
|
||||
import ContractConnectWallet from './ContractConnectWallet';
|
||||
import ContractCustomAbiAlert from './ContractCustomAbiAlert';
|
||||
import ContractMethods from './ContractMethods';
|
||||
|
||||
interface Props {
|
||||
abi: Array<SmartContractMethod>;
|
||||
isLoading?: boolean;
|
||||
type: MethodType;
|
||||
}
|
||||
|
||||
const ContractMethodsCustom = ({ abi, isLoading, type }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContractConnectWallet isLoading={ isLoading }/>
|
||||
<ContractCustomAbiAlert isLoading={ isLoading }/>
|
||||
<ContractMethods abi={ abi } isLoading={ isLoading } type={ type }/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ContractMethodsCustom);
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
|
||||
import * as addressMock from 'mocks/address/address';
|
||||
import * as contractMock from 'mocks/contract/info';
|
||||
import * as methodsMock from 'mocks/contract/methods';
|
||||
import { test, expect } from 'playwright/lib';
|
||||
|
||||
import ContractMethodsProxy from './ContractMethodsProxy';
|
||||
|
||||
const addressHash = addressMock.hash;
|
||||
|
||||
test('with one implementation +@mobile', async({ render, mockApiResponse }) => {
|
||||
const hooksConfig = {
|
||||
router: {
|
||||
query: { hash: addressHash, tab: 'read_proxy' },
|
||||
},
|
||||
};
|
||||
const implementations = [
|
||||
{ address: '0x2F4F4A52295940C576417d29F22EEb92B440eC89', name: 'HomeBridge' },
|
||||
];
|
||||
await mockApiResponse('contract', { ...contractMock.verified, abi: methodsMock.read }, { pathParams: { hash: implementations[0].address } });
|
||||
|
||||
const component = await render(<ContractMethodsProxy implementations={ implementations } type="read"/>, { hooksConfig });
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('with multiple implementations +@mobile', async({ render, mockApiResponse }) => {
|
||||
const hooksConfig = {
|
||||
router: {
|
||||
query: { hash: addressHash, tab: 'read_proxy' },
|
||||
},
|
||||
};
|
||||
const implementations = [
|
||||
{ address: '0x2F4F4A52295940C576417d29F22EEb92B440eC89', name: 'HomeBridge' },
|
||||
{ address: '0xc9e91eDeA9DC16604022e4E5b437Df9c64EdB05A', name: 'Diamond' },
|
||||
];
|
||||
await mockApiResponse('contract', { ...contractMock.verified, abi: methodsMock.read }, { pathParams: { hash: implementations[0].address } });
|
||||
|
||||
const component = await render(<ContractMethodsProxy implementations={ implementations } type="read"/>, { hooksConfig });
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { MethodType } from './types';
|
||||
import type { AddressImplementation } from 'types/api/addressParams';
|
||||
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
|
||||
import ContractConnectWallet from './ContractConnectWallet';
|
||||
import ContractImplementationAddress from './ContractImplementationAddress';
|
||||
import ContractMethods from './ContractMethods';
|
||||
import { isReadMethod, isWriteMethod } from './utils';
|
||||
|
||||
interface Props {
|
||||
type: MethodType;
|
||||
implementations: Array<AddressImplementation>;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const ContractMethodsProxy = ({ type, implementations, isLoading: isInitialLoading }: Props) => {
|
||||
|
||||
const [ selectedItem, setSelectedItem ] = React.useState(implementations[0]);
|
||||
|
||||
const contractQuery = useApiQuery('contract', {
|
||||
pathParams: { hash: selectedItem.address },
|
||||
queryOptions: {
|
||||
enabled: Boolean(selectedItem.address),
|
||||
refetchOnMount: false,
|
||||
},
|
||||
});
|
||||
|
||||
const handleItemSelect = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const nextOption = implementations.find(({ address }) => address === event.target.value);
|
||||
if (nextOption) {
|
||||
setSelectedItem(nextOption);
|
||||
}
|
||||
}, [ implementations ]);
|
||||
|
||||
const abi = contractQuery.data?.abi?.filter(type === 'read' ? isReadMethod : isWriteMethod) || [];
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ContractConnectWallet isLoading={ isInitialLoading }/>
|
||||
<ContractImplementationAddress
|
||||
implementations={ implementations }
|
||||
selectedItem={ selectedItem }
|
||||
onItemSelect={ handleItemSelect }
|
||||
isLoading={ isInitialLoading }
|
||||
/>
|
||||
<ContractMethods
|
||||
key={ selectedItem.address }
|
||||
abi={ abi }
|
||||
isLoading={ isInitialLoading || contractQuery.isPending }
|
||||
isError={ contractQuery.isError }
|
||||
type={ type }
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ContractMethodsProxy);
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
|
||||
import * as addressMock from 'mocks/address/address';
|
||||
import * as methodsMock from 'mocks/contract/methods';
|
||||
import { test, expect } from 'playwright/lib';
|
||||
|
||||
import ContractMethodsRegular from './ContractMethodsRegular';
|
||||
|
||||
const addressHash = addressMock.hash;
|
||||
|
||||
test('read methods', async({ render, mockContractReadResponse }) => {
|
||||
// for some reason it takes a long time for "wagmi" library to parse response result in the test environment
|
||||
// so I had to increase the test timeout
|
||||
test.slow();
|
||||
|
||||
const hooksConfig = {
|
||||
router: {
|
||||
query: { hash: addressHash, tab: 'read_contract' },
|
||||
},
|
||||
};
|
||||
|
||||
await mockContractReadResponse({
|
||||
abiItem: methodsMock.read[1],
|
||||
address: addressHash,
|
||||
result: [ 'USDC' ],
|
||||
});
|
||||
|
||||
const component = await render(<ContractMethodsRegular abi={ methodsMock.read } type="read"/>, { hooksConfig });
|
||||
await component.getByText(/expand all/i).click();
|
||||
|
||||
await expect(component.getByText('USDC')).toBeVisible({ timeout: 20_000 });
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('write methods +@dark-mode +@mobile', async({ render }) => {
|
||||
const hooksConfig = {
|
||||
router: {
|
||||
query: { hash: addressHash, tab: 'write_contract' },
|
||||
},
|
||||
};
|
||||
|
||||
const component = await render(<ContractMethodsRegular abi={ methodsMock.write } type="write"/>, { hooksConfig });
|
||||
await component.getByText(/expand all/i).click();
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { MethodType, SmartContractMethod } from './types';
|
||||
|
||||
import ContractConnectWallet from './ContractConnectWallet';
|
||||
import ContractMethods from './ContractMethods';
|
||||
|
||||
interface Props {
|
||||
abi: Array<SmartContractMethod>;
|
||||
isLoading?: boolean;
|
||||
type: MethodType;
|
||||
}
|
||||
|
||||
const ContractMethodsRegular = ({ abi, isLoading, type }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContractConnectWallet isLoading={ isLoading }/>
|
||||
<ContractMethods abi={ abi } isLoading={ isLoading } type={ type }/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ContractMethodsRegular);
|
||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 153 KiB |
@@ -1,14 +1,15 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { ContractAbiItem } from '../types';
|
||||
import type { FormSubmitHandler, SmartContractMethod } from '../types';
|
||||
|
||||
import { test, expect } from 'playwright/lib';
|
||||
|
||||
import ContractMethodForm from './ContractMethodForm';
|
||||
|
||||
const onSubmit = () => Promise.resolve({ source: 'wallet_client' as const, result: { hash: '0x0000' as `0x${ string }` } });
|
||||
const onSubmit: FormSubmitHandler = () => Promise.resolve({ source: 'wallet_client' as const, data: { hash: '0x0000' as `0x${ string }` } });
|
||||
const onReset = () => {};
|
||||
|
||||
const data: ContractAbiItem = {
|
||||
const data: SmartContractMethod = {
|
||||
inputs: [
|
||||
// TUPLE
|
||||
{
|
||||
@@ -102,7 +103,9 @@ test('base view +@mobile +@dark-mode', async({ render }) => {
|
||||
<ContractMethodForm
|
||||
data={ data }
|
||||
onSubmit={ onSubmit }
|
||||
methodType="write"
|
||||
onReset={ onReset }
|
||||
isOpen
|
||||
attempt={ 0 }
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -1,31 +1,37 @@
|
||||
import { Box, Button, Flex, Tooltip, chakra } from '@chakra-ui/react';
|
||||
import _mapValues from 'lodash/mapValues';
|
||||
import React from 'react';
|
||||
import type { SubmitHandler } from 'react-hook-form';
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
import type { AbiFunction } from 'viem';
|
||||
|
||||
import type { FormSubmitHandler, FormSubmitResult, MethodCallStrategy, MethodType, ContractAbiItem } from '../types';
|
||||
import type { FormSubmitHandler, FormSubmitResult, MethodCallStrategy, SmartContractMethod } from '../types';
|
||||
|
||||
import config from 'configs/app';
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
import { isReadMethod } from '../utils';
|
||||
import ContractMethodFieldAccordion from './ContractMethodFieldAccordion';
|
||||
import ContractMethodFieldInput from './ContractMethodFieldInput';
|
||||
import ContractMethodFieldInputArray from './ContractMethodFieldInputArray';
|
||||
import ContractMethodFieldInputTuple from './ContractMethodFieldInputTuple';
|
||||
import ContractMethodOutputs from './ContractMethodOutputs';
|
||||
import ContractMethodResult from './ContractMethodResult';
|
||||
import ContractMethodResultPublicClient from './ContractMethodResultPublicClient';
|
||||
import ContractMethodResultWalletClient from './ContractMethodResultWalletClient';
|
||||
import { getFieldLabel, matchArray, transformFormDataToMethodArgs } from './utils';
|
||||
import type { ContractMethodFormFields } from './utils';
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
const NO_WALLET_CLIENT_TEXT = 'Blockchain interaction is not available at the moment since WalletConnect is not configured for this application. Please contact the service maintainer to make necessary changes in the service configuration.';
|
||||
|
||||
interface Props {
|
||||
data: ContractAbiItem;
|
||||
data: SmartContractMethod;
|
||||
attempt: number;
|
||||
onSubmit: FormSubmitHandler;
|
||||
methodType: MethodType;
|
||||
isOpen: boolean;
|
||||
onReset: () => void;
|
||||
}
|
||||
|
||||
const ContractMethodForm = ({ data, onSubmit, methodType }: Props) => {
|
||||
const ContractMethodForm = ({ data, attempt, onSubmit, onReset, isOpen }: Props) => {
|
||||
|
||||
const [ result, setResult ] = React.useState<FormSubmitResult>();
|
||||
const [ isLoading, setLoading ] = React.useState(false);
|
||||
@@ -43,12 +49,10 @@ const ContractMethodForm = ({ data, onSubmit, methodType }: Props) => {
|
||||
callStrategyRef.current = callStrategy as MethodCallStrategy;
|
||||
}, []);
|
||||
|
||||
const methodType = isReadMethod(data) ? 'read' : 'write';
|
||||
|
||||
const onFormSubmit: SubmitHandler<ContractMethodFormFields> = React.useCallback(async(formData) => {
|
||||
// The API used for reading from contracts expects all values to be strings.
|
||||
const formattedData = callStrategyRef.current === 'api' ?
|
||||
_mapValues(formData, (value) => value !== undefined ? String(value) : undefined) :
|
||||
formData;
|
||||
const args = transformFormDataToMethodArgs(formattedData);
|
||||
const args = transformFormDataToMethodArgs(formData);
|
||||
|
||||
setResult(undefined);
|
||||
setLoading(true);
|
||||
@@ -57,10 +61,10 @@ const ContractMethodForm = ({ data, onSubmit, methodType }: Props) => {
|
||||
.then((result) => {
|
||||
setResult(result);
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch((error: Error) => {
|
||||
setResult({
|
||||
source: callStrategyRef.current ?? 'wallet_client',
|
||||
result: error?.error || error?.data || (error?.reason && { message: error.reason }) || error,
|
||||
source: callStrategyRef.current === 'write' ? 'wallet_client' : 'public_client',
|
||||
data: error,
|
||||
});
|
||||
setLoading(false);
|
||||
})
|
||||
@@ -72,7 +76,18 @@ const ContractMethodForm = ({ data, onSubmit, methodType }: Props) => {
|
||||
});
|
||||
}, [ data, methodType, onSubmit ]);
|
||||
|
||||
const handleTxSettle = React.useCallback(() => {
|
||||
React.useEffect(() => {
|
||||
if (isOpen && !callStrategyRef.current && attempt === 0) {
|
||||
const hasConstantOutputs = isReadMethod(data) && data.inputs.length === 0;
|
||||
if (hasConstantOutputs) {
|
||||
setCallStrategy('read');
|
||||
callStrategyRef.current = 'read';
|
||||
onFormSubmit({});
|
||||
}
|
||||
}
|
||||
}, [ data, isOpen, onFormSubmit, attempt ]);
|
||||
|
||||
const handleResultSettle = React.useCallback(() => {
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
@@ -92,29 +107,64 @@ const ContractMethodForm = ({ data, onSubmit, methodType }: Props) => {
|
||||
];
|
||||
}, [ data ]);
|
||||
|
||||
const outputs = 'outputs' in data && data.outputs ? data.outputs : [];
|
||||
const primaryButton = (() => {
|
||||
const isDisabled = !config.features.blockchainInteraction.isEnabled && methodType === 'write';
|
||||
const text = methodType === 'write' ? 'Write' : 'Read';
|
||||
const buttonCallStrategy = methodType === 'write' ? 'write' : 'read';
|
||||
|
||||
const callStrategies = (() => {
|
||||
switch (methodType) {
|
||||
case 'read': {
|
||||
return { primary: 'api', secondary: undefined };
|
||||
}
|
||||
|
||||
case 'write': {
|
||||
return {
|
||||
primary: config.features.blockchainInteraction.isEnabled ? 'wallet_client' : undefined,
|
||||
secondary: 'outputs' in data && Boolean(data.outputs?.length) ? 'api' : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
default: {
|
||||
return { primary: undefined, secondary: undefined };
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Tooltip label={ isDisabled ? NO_WALLET_CLIENT_TEXT : undefined } maxW="300px">
|
||||
<Button
|
||||
isLoading={ callStrategy === buttonCallStrategy && isLoading }
|
||||
isDisabled={ isLoading || isDisabled }
|
||||
onClick={ handleButtonClick }
|
||||
loadingText={ text }
|
||||
variant="outline"
|
||||
size="sm"
|
||||
flexShrink={ 0 }
|
||||
width="min-content"
|
||||
px={ 4 }
|
||||
type="submit"
|
||||
data-call-strategy={ buttonCallStrategy }
|
||||
>
|
||||
{ text }
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
})();
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
const noWalletClientText = 'Blockchain interaction is not available at the moment since WalletConnect is not configured for this application. Please contact the service maintainer to make necessary changes in the service configuration.';
|
||||
const secondaryButton = (() => {
|
||||
if (methodType === 'read') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasOutputs = 'outputs' in data && data.outputs.length > 0;
|
||||
if (!hasOutputs) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = 'Simulate';
|
||||
const buttonCallStrategy = 'simulate';
|
||||
|
||||
return (
|
||||
<Button
|
||||
isLoading={ callStrategy === buttonCallStrategy && isLoading }
|
||||
isDisabled={ isLoading }
|
||||
onClick={ handleButtonClick }
|
||||
loadingText={ text }
|
||||
variant="outline"
|
||||
size="sm"
|
||||
flexShrink={ 0 }
|
||||
width="min-content"
|
||||
px={ 4 }
|
||||
mr={ 3 }
|
||||
type="submit"
|
||||
data-call-strategy={ buttonCallStrategy }
|
||||
>
|
||||
{ text }
|
||||
</Button>
|
||||
);
|
||||
})();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
@@ -162,45 +212,36 @@ const ContractMethodForm = ({ data, onSubmit, methodType }: Props) => {
|
||||
return <ContractMethodFieldInput key={ index } { ...props } path={ `${ index }` }/>;
|
||||
}) }
|
||||
</Flex>
|
||||
{ callStrategies.secondary && (
|
||||
{ secondaryButton }
|
||||
{ primaryButton }
|
||||
{ result && !isLoading && (
|
||||
<Button
|
||||
isLoading={ callStrategy === callStrategies.secondary && isLoading }
|
||||
isDisabled={ isLoading }
|
||||
onClick={ handleButtonClick }
|
||||
loadingText="Simulate"
|
||||
variant="outline"
|
||||
variant="simple"
|
||||
colorScheme="blue"
|
||||
size="sm"
|
||||
flexShrink={ 0 }
|
||||
width="min-content"
|
||||
px={ 4 }
|
||||
mr={ 3 }
|
||||
type="submit"
|
||||
data-call-strategy={ callStrategies.secondary }
|
||||
onClick={ onReset }
|
||||
ml={ 1 }
|
||||
>
|
||||
Simulate
|
||||
<IconSvg name="repeat_arrow" boxSize={ 5 } mr={ 1 }/>
|
||||
Reset
|
||||
</Button>
|
||||
) }
|
||||
<Tooltip label={ !callStrategies.primary ? noWalletClientText : undefined } maxW="300px">
|
||||
<Button
|
||||
isLoading={ callStrategy === callStrategies.primary && isLoading }
|
||||
isDisabled={ isLoading || !callStrategies.primary }
|
||||
onClick={ handleButtonClick }
|
||||
loadingText={ methodType === 'write' ? 'Write' : 'Read' }
|
||||
variant="outline"
|
||||
size="sm"
|
||||
flexShrink={ 0 }
|
||||
width="min-content"
|
||||
px={ 4 }
|
||||
type="submit"
|
||||
data-call-strategy={ callStrategies.primary }
|
||||
>
|
||||
{ methodType === 'write' ? 'Write' : 'Read' }
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</chakra.form>
|
||||
</FormProvider>
|
||||
{ 'outputs' in data && Boolean(data.outputs?.length) && <ContractMethodOutputs data={ outputs }/> }
|
||||
{ result && <ContractMethodResult abiItem={ data } result={ result } onSettle={ handleTxSettle }/> }
|
||||
{ result && result.source === 'wallet_client' && (
|
||||
<ContractMethodResultWalletClient
|
||||
data={ result.data }
|
||||
onSettle={ handleResultSettle }
|
||||
/>
|
||||
) }
|
||||
{ 'outputs' in data && data.outputs.length > 0 && (
|
||||
<ContractMethodResultPublicClient
|
||||
data={ result && result.source === 'public_client' ? result.data : undefined }
|
||||
onSettle={ handleResultSettle }
|
||||
abiItem={ data }
|
||||
mode={ result && result.source === 'public_client' ? 'result' : 'preview' }
|
||||
/>
|
||||
) }
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,167 @@
|
||||
import React from 'react';
|
||||
import type { AbiFunction } from 'viem';
|
||||
|
||||
import { test, expect } from 'playwright/lib';
|
||||
|
||||
import ContractMethodResultPublicClient from './ContractMethodResultPublicClient';
|
||||
import { ErrorStory } from './ContractMethodResultPublicClient.pwstory';
|
||||
|
||||
const abiItem: AbiFunction = {
|
||||
inputs: [],
|
||||
name: 'testAbiMethod',
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
outputs: [
|
||||
// PRIMITIVES
|
||||
{ internalType: 'address', name: 'owner', type: 'address' },
|
||||
{ internalType: 'uint256', name: 'validatorsCount', type: 'uint256' },
|
||||
{ internalType: 'bool', name: '', type: 'bool' },
|
||||
|
||||
// ARRAY OF PRIMITIVES
|
||||
{ internalType: 'address[]', name: 'interChainClients', type: 'address[]' },
|
||||
|
||||
// NESTED ARRAY
|
||||
{ internalType: 'uint256[2][]', name: 'chainIds', type: 'uint256[2][]' },
|
||||
|
||||
// TUPLE
|
||||
{
|
||||
components: [
|
||||
{
|
||||
components: [
|
||||
{ internalType: 'bool', name: 'executed', type: 'bool' },
|
||||
{ internalType: 'uint56', name: 'snapshotId', type: 'uint56' },
|
||||
{ internalType: 'string', name: 'descriptionURL', type: 'string' },
|
||||
],
|
||||
internalType: 'struct IGovValidators.ProposalCore',
|
||||
name: 'core',
|
||||
type: 'tuple',
|
||||
},
|
||||
],
|
||||
internalType: 'struct IGovValidators.ExternalProposal',
|
||||
name: '',
|
||||
type: 'tuple',
|
||||
},
|
||||
|
||||
// ARRAY OF TUPLE
|
||||
{
|
||||
components: [
|
||||
{
|
||||
components: [
|
||||
{ internalType: 'enum IGovValidators.ProposalType', name: 'proposalType', type: 'uint8' },
|
||||
{ internalType: 'bytes', name: 'data', type: 'bytes' },
|
||||
{
|
||||
components: [
|
||||
{ internalType: 'bool', name: 'executed', type: 'bool' },
|
||||
{ internalType: 'uint56', name: 'snapshotId', type: 'uint56' },
|
||||
],
|
||||
internalType: 'struct IGovValidators.ProposalCore',
|
||||
name: 'core',
|
||||
type: 'tuple',
|
||||
},
|
||||
],
|
||||
internalType: 'struct IGovValidators.InternalProposal',
|
||||
name: 'proposal',
|
||||
type: 'tuple',
|
||||
},
|
||||
{ internalType: 'enum IGovValidators.ProposalState', name: 'proposalState', type: 'uint8' },
|
||||
],
|
||||
internalType: 'struct IGovValidators.InternalProposalView[]',
|
||||
name: 'internalProposals',
|
||||
type: 'tuple[]',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = [
|
||||
'0x0000000000000000000000000000000000000000',
|
||||
BigInt(42),
|
||||
false,
|
||||
[
|
||||
'0x92a309C640c3f6AF4F84FE40120fD02b58E3Aa96',
|
||||
'0x588c7Bda9366EEf83EdF67049a1C45f737aFFe0F',
|
||||
],
|
||||
[
|
||||
[ BigInt(11_000), BigInt(12_000), BigInt(13_000) ],
|
||||
[ BigInt(21_000), BigInt(22_000) ],
|
||||
],
|
||||
{
|
||||
core: {
|
||||
executed: true,
|
||||
snapshotId: BigInt(77),
|
||||
descriptionURL: '',
|
||||
},
|
||||
},
|
||||
[
|
||||
{
|
||||
proposalState: 1,
|
||||
proposal: {
|
||||
proposalType: 100,
|
||||
data: '0x000100',
|
||||
core: {
|
||||
executed: true,
|
||||
snapshotId: 111,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
proposalState: 3,
|
||||
proposal: {
|
||||
proposalType: 300,
|
||||
data: '0x000300',
|
||||
core: {
|
||||
executed: true,
|
||||
snapshotId: 333,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
const onSettle = () => {};
|
||||
|
||||
test('preview mode', async({ render }) => {
|
||||
const component = await render(
|
||||
<ContractMethodResultPublicClient
|
||||
abiItem={ abiItem }
|
||||
data={ undefined }
|
||||
mode="preview"
|
||||
onSettle={ onSettle }
|
||||
/>,
|
||||
);
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('result mode', async({ render }) => {
|
||||
const component = await render(
|
||||
<ContractMethodResultPublicClient
|
||||
abiItem={ abiItem }
|
||||
data={ result }
|
||||
mode="result"
|
||||
onSettle={ onSettle }
|
||||
/>,
|
||||
);
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('error', async({ render }) => {
|
||||
const component = await render(
|
||||
<ErrorStory
|
||||
abiItem={ abiItem }
|
||||
mode="result"
|
||||
onSettle={ onSettle }
|
||||
/>,
|
||||
);
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('single output', async({ render }) => {
|
||||
const component = await render(
|
||||
<ContractMethodResultPublicClient
|
||||
abiItem={{ ...abiItem, outputs: abiItem.outputs.slice(3, 4) }}
|
||||
data={ result[3] }
|
||||
mode="result"
|
||||
onSettle={ onSettle }
|
||||
/>,
|
||||
);
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { Props } from './ContractMethodResultPublicClient';
|
||||
import ContractMethodResultPublicClient from './ContractMethodResultPublicClient';
|
||||
|
||||
export const ErrorStory = (props: Omit<Props, 'data'>) => {
|
||||
const result = new Error('Something went wrong');
|
||||
|
||||
return <ContractMethodResultPublicClient { ...props } data={ result }/>;
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Alert, Flex, useColorModeValue } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import type { AbiFunction } from 'viem';
|
||||
|
||||
import type { FormSubmitResultPublicClient, ResultViewMode } from '../types';
|
||||
|
||||
import ResultItem from './resultPublicClient/Item';
|
||||
|
||||
export interface Props {
|
||||
data: FormSubmitResultPublicClient['data'];
|
||||
abiItem: AbiFunction;
|
||||
onSettle: () => void;
|
||||
mode: ResultViewMode;
|
||||
}
|
||||
|
||||
const ContractMethodResultPublicClient = ({ data, abiItem, onSettle, mode: modeProps }: Props) => {
|
||||
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
|
||||
|
||||
React.useEffect(() => {
|
||||
if (modeProps === 'result') {
|
||||
onSettle();
|
||||
}
|
||||
}, [ onSettle, modeProps ]);
|
||||
|
||||
const formattedData = (() => {
|
||||
return abiItem.outputs.length > 1 && Array.isArray(data) ? data : [ data ];
|
||||
})();
|
||||
|
||||
const isError = data instanceof Error;
|
||||
const mode = isError ? 'preview' : modeProps;
|
||||
|
||||
return (
|
||||
<>
|
||||
{ isError && (
|
||||
<Alert status="error" mt={ 3 } p={ 4 } borderRadius="md" fontSize="sm" wordBreak="break-word" whiteSpace="pre-wrap">
|
||||
{ 'shortMessage' in data && typeof data.shortMessage === 'string' ? data.shortMessage : data.message }
|
||||
</Alert>
|
||||
) }
|
||||
<Flex
|
||||
flexDir="column"
|
||||
rowGap={ 2 }
|
||||
mt={ 3 }
|
||||
p={ 4 }
|
||||
borderRadius="md"
|
||||
bgColor={ bgColor }
|
||||
color={ mode === 'preview' ? 'gray.500' : undefined }
|
||||
fontSize="sm"
|
||||
lineHeight="20px"
|
||||
whiteSpace="break-spaces"
|
||||
wordBreak="break-all"
|
||||
>
|
||||
{ abiItem.outputs.map((output, index) => (
|
||||
<ResultItem
|
||||
key={ index }
|
||||
abiParameter={ output }
|
||||
data={ isError ? undefined : formattedData[index] }
|
||||
mode={ mode }
|
||||
/>
|
||||
)) }
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ContractMethodResultPublicClient);
|
||||
@@ -11,7 +11,7 @@ test('loading', async({ render }) => {
|
||||
status: 'pending' as const,
|
||||
error: null,
|
||||
} as PropsDumb['txInfo'],
|
||||
result: {
|
||||
data: {
|
||||
hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29' as `0x${ string }`,
|
||||
},
|
||||
onSettle: () => {},
|
||||
@@ -27,7 +27,7 @@ test('success', async({ render }) => {
|
||||
status: 'success' as const,
|
||||
error: null,
|
||||
} as PropsDumb['txInfo'],
|
||||
result: {
|
||||
data: {
|
||||
hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29' as `0x${ string }`,
|
||||
},
|
||||
onSettle: () => {},
|
||||
@@ -46,7 +46,7 @@ test('error +@mobile', async({ render }) => {
|
||||
message: 'missing revert data in call exception; Transaction reverted without a reason string [ See: https://links.ethers.org/v5-errors-CALL_EXCEPTION ]',
|
||||
} as Error,
|
||||
} as PropsDumb['txInfo'],
|
||||
result: {
|
||||
data: {
|
||||
hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29' as `0x${ string }`,
|
||||
},
|
||||
onSettle: () => {},
|
||||
@@ -62,7 +62,7 @@ test('error in result', async({ render }) => {
|
||||
status: 'idle' as const,
|
||||
error: null,
|
||||
} as unknown as PropsDumb['txInfo'],
|
||||
result: {
|
||||
data: {
|
||||
message: 'wallet is not connected',
|
||||
} as Error,
|
||||
onSettle: () => {},
|
||||
@@ -1,4 +1,4 @@
|
||||
import { chakra, Spinner, Box } from '@chakra-ui/react';
|
||||
import { chakra, Spinner, Box, Alert } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import type { UseWaitForTransactionReceiptReturnType } from 'wagmi';
|
||||
import { useWaitForTransactionReceipt } from 'wagmi';
|
||||
@@ -10,27 +10,27 @@ import { route } from 'nextjs-routes';
|
||||
import LinkInternal from 'ui/shared/links/LinkInternal';
|
||||
|
||||
interface Props {
|
||||
result: FormSubmitResultWalletClient['result'];
|
||||
data: FormSubmitResultWalletClient['data'];
|
||||
onSettle: () => void;
|
||||
}
|
||||
|
||||
const ContractMethodResultWalletClient = ({ result, onSettle }: Props) => {
|
||||
const txHash = result && 'hash' in result ? result.hash as `0x${ string }` : undefined;
|
||||
const ContractMethodResultWalletClient = ({ data, onSettle }: Props) => {
|
||||
const txHash = data && 'hash' in data ? data.hash as `0x${ string }` : undefined;
|
||||
const txInfo = useWaitForTransactionReceipt({
|
||||
hash: txHash,
|
||||
});
|
||||
|
||||
return <ContractMethodResultWalletClientDumb result={ result } onSettle={ onSettle } txInfo={ txInfo }/>;
|
||||
return <ContractMethodResultWalletClientDumb data={ data } onSettle={ onSettle } txInfo={ txInfo }/>;
|
||||
};
|
||||
|
||||
export interface PropsDumb {
|
||||
result: FormSubmitResultWalletClient['result'];
|
||||
data: FormSubmitResultWalletClient['data'];
|
||||
onSettle: () => void;
|
||||
txInfo: UseWaitForTransactionReceiptReturnType;
|
||||
}
|
||||
|
||||
export const ContractMethodResultWalletClientDumb = ({ result, onSettle, txInfo }: PropsDumb) => {
|
||||
const txHash = result && 'hash' in result ? result.hash : undefined;
|
||||
export const ContractMethodResultWalletClientDumb = ({ data, onSettle, txInfo }: PropsDumb) => {
|
||||
const txHash = data && 'hash' in data ? data.hash : undefined;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (txInfo.status !== 'pending') {
|
||||
@@ -38,11 +38,11 @@ export const ContractMethodResultWalletClientDumb = ({ result, onSettle, txInfo
|
||||
}
|
||||
}, [ onSettle, txInfo.status ]);
|
||||
|
||||
if (!result) {
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isErrorResult = 'message' in result;
|
||||
const isErrorResult = 'message' in data;
|
||||
|
||||
const txLink = txHash ? (
|
||||
<LinkInternal href={ route({ pathname: '/tx/[hash]', query: { hash: txHash } }) }>View transaction details</LinkInternal>
|
||||
@@ -51,10 +51,9 @@ export const ContractMethodResultWalletClientDumb = ({ result, onSettle, txInfo
|
||||
const content = (() => {
|
||||
if (isErrorResult) {
|
||||
return (
|
||||
<>
|
||||
<span>Error: </span>
|
||||
<span>{ result.message }</span>
|
||||
</>
|
||||
<Alert status="error">
|
||||
{ data.message }
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,11 +81,9 @@ export const ContractMethodResultWalletClientDumb = ({ result, onSettle, txInfo
|
||||
|
||||
case 'error': {
|
||||
return (
|
||||
<>
|
||||
<span>Error: </span>
|
||||
<span>{ txInfo.error ? txInfo.error.message : 'Something went wrong' } </span>
|
||||
{ txLink }
|
||||
</>
|
||||
<Alert status="error" flexDir="column" alignItems="flex-start" rowGap={ 1 }>
|
||||
Error: { txInfo.error ? txInfo.error.message : 'Something went wrong' } { txLink }
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import type { AbiParameter } from 'viem';
|
||||
|
||||
import type { ResultViewMode } from '../../types';
|
||||
|
||||
import { matchArray } from '../utils';
|
||||
import ItemArray from './ItemArray';
|
||||
import ItemPrimitive from './ItemPrimitive';
|
||||
import ItemTuple from './ItemTuple';
|
||||
|
||||
interface Props {
|
||||
abiParameter: AbiParameter;
|
||||
data: unknown;
|
||||
mode: ResultViewMode;
|
||||
level?: number;
|
||||
}
|
||||
|
||||
const Item = ({ abiParameter, data, mode, level = 0 }: Props) => {
|
||||
|
||||
const arrayMatch = React.useMemo(() => matchArray(abiParameter.type), [ abiParameter.type ]);
|
||||
if (arrayMatch) {
|
||||
if (!Array.isArray(data)) {
|
||||
if (arrayMatch.itemType === 'tuple' && mode === 'preview' && 'components' in abiParameter) {
|
||||
const previewData = Object.fromEntries(abiParameter.components.map(({ name }) => ([ name ?? '', undefined ])));
|
||||
return <ItemArray abiParameter={ abiParameter } data={ [ previewData ] } mode={ mode } level={ level } arrayMatch={ arrayMatch }/>;
|
||||
}
|
||||
return <ItemPrimitive abiParameter={ abiParameter } data={ data || (mode === 'preview' ? undefined : '[ ]') } level={ level }/>;
|
||||
}
|
||||
return <ItemArray abiParameter={ abiParameter } data={ data } level={ level } mode={ mode } arrayMatch={ arrayMatch }/>;
|
||||
}
|
||||
|
||||
const tupleMatch = abiParameter.type.includes('tuple');
|
||||
if (tupleMatch) {
|
||||
return <ItemTuple abiParameter={ abiParameter } data={ data } mode={ mode } level={ level }/>;
|
||||
}
|
||||
|
||||
return <ItemPrimitive abiParameter={ abiParameter } data={ data } level={ level }/>;
|
||||
};
|
||||
|
||||
export default React.memo(Item);
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import type { AbiParameter } from 'viem';
|
||||
|
||||
import type { ResultViewMode } from '../../types';
|
||||
|
||||
import { matchArray, type MatchArray } from '../utils';
|
||||
import ItemLabel from './ItemLabel';
|
||||
import ItemPrimitive from './ItemPrimitive';
|
||||
import ItemTuple from './ItemTuple';
|
||||
import { printRowOffset } from './utils';
|
||||
|
||||
interface Props {
|
||||
abiParameter: AbiParameter;
|
||||
data: Array<unknown>;
|
||||
mode: ResultViewMode;
|
||||
level: number;
|
||||
arrayMatch: MatchArray;
|
||||
}
|
||||
|
||||
const ItemArray = ({ abiParameter, data, level, arrayMatch, mode }: Props) => {
|
||||
|
||||
const itemAbiParameter = React.useMemo(() => {
|
||||
const type = arrayMatch.itemType;
|
||||
const internalType = matchArray(abiParameter.internalType || '')?.itemType;
|
||||
return { ...abiParameter, type, internalType };
|
||||
}, [ abiParameter, arrayMatch.itemType ]);
|
||||
|
||||
const content = (() => {
|
||||
if (arrayMatch.isNested && data.every(Array.isArray)) {
|
||||
const itemArrayMatch = matchArray(arrayMatch.itemType);
|
||||
|
||||
if (itemArrayMatch) {
|
||||
return data.map((item, index) => (
|
||||
<ItemArray
|
||||
key={ index }
|
||||
abiParameter={ itemAbiParameter }
|
||||
data={ item }
|
||||
mode={ mode }
|
||||
arrayMatch={ itemArrayMatch }
|
||||
level={ level + 1 }
|
||||
/>
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (arrayMatch.itemType === 'tuple') {
|
||||
return data.map((item, index) => (
|
||||
<ItemTuple
|
||||
key={ index }
|
||||
abiParameter={ itemAbiParameter }
|
||||
data={ item }
|
||||
mode={ mode }
|
||||
level={ level + 1 }
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
return data.map((item, index) => (
|
||||
<ItemPrimitive
|
||||
key={ index }
|
||||
abiParameter={ itemAbiParameter }
|
||||
data={ item }
|
||||
level={ level + 1 }
|
||||
hideLabel
|
||||
/>
|
||||
));
|
||||
})();
|
||||
|
||||
return (
|
||||
<p>
|
||||
<p>
|
||||
<span>{ printRowOffset(level) }</span>
|
||||
<ItemLabel abiParameter={ abiParameter }/>
|
||||
<span>[{ data.length === 0 ? ' ]' : '' }</span>
|
||||
</p>
|
||||
{ content }
|
||||
{ data.length > 0 && <p>{ printRowOffset(level) }]</p> }
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ItemArray);
|
||||
@@ -0,0 +1,18 @@
|
||||
import { chakra } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import type { AbiParameter } from 'viem';
|
||||
|
||||
interface Props {
|
||||
abiParameter: AbiParameter;
|
||||
}
|
||||
|
||||
const ItemLabel = ({ abiParameter }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{ abiParameter.name && <chakra.span fontWeight={ 500 }>{ abiParameter.name } </chakra.span> }
|
||||
<span>({ abiParameter.type }) : </span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ItemLabel);
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import React from 'react';
|
||||
import type { AbiParameter } from 'viem';
|
||||
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
import CopyToClipboard from 'ui/shared/CopyToClipboard';
|
||||
import LinkInternal from 'ui/shared/links/LinkInternal';
|
||||
|
||||
import { matchInt } from '../utils';
|
||||
import ItemLabel from './ItemLabel';
|
||||
import { printRowOffset } from './utils';
|
||||
|
||||
function castValueToString(value: unknown): string {
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
return value === '' ? `""` : value;
|
||||
case 'undefined':
|
||||
return '';
|
||||
case 'number':
|
||||
return value.toLocaleString(undefined, { useGrouping: false });
|
||||
case 'bigint':
|
||||
return value.toString();
|
||||
case 'boolean':
|
||||
default:
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
const INT_TOOLTIP_THRESHOLD = 10 ** 9;
|
||||
|
||||
interface Props {
|
||||
abiParameter: AbiParameter;
|
||||
data: unknown;
|
||||
level: number;
|
||||
hideLabel?: boolean;
|
||||
}
|
||||
|
||||
const ItemPrimitive = ({ abiParameter, data, level, hideLabel }: Props) => {
|
||||
|
||||
const value = (() => {
|
||||
if (abiParameter.type === 'address' && typeof data === 'string') {
|
||||
return (
|
||||
<>
|
||||
<LinkInternal href={ route({ pathname: '/address/[hash]', query: { hash: data } }) }>{ data }</LinkInternal>
|
||||
<CopyToClipboard text={ data } size={ 4 } verticalAlign="sub"/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const intMatch = matchInt(abiParameter.type);
|
||||
if (intMatch && typeof data === 'bigint' && intMatch.max > INT_TOOLTIP_THRESHOLD && data > INT_TOOLTIP_THRESHOLD) {
|
||||
const dividedValue = BigNumber(data.toString()).div(BigNumber(INT_TOOLTIP_THRESHOLD));
|
||||
return (
|
||||
<Tooltip label={ dividedValue.toLocaleString() + ' ETH' }>
|
||||
<span>{ castValueToString(data) }</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return <span>{ castValueToString(data) }</span>;
|
||||
})();
|
||||
|
||||
return (
|
||||
<p>
|
||||
<span>{ printRowOffset(level) }</span>
|
||||
{ !hideLabel && <ItemLabel abiParameter={ abiParameter }/> }
|
||||
{ value }
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ItemPrimitive);
|
||||
@@ -0,0 +1,43 @@
|
||||
import { chakra } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import type { AbiParameter } from 'viem';
|
||||
|
||||
import type { ResultViewMode } from '../../types';
|
||||
|
||||
import Item from './Item';
|
||||
import { printRowOffset } from './utils';
|
||||
|
||||
interface Props {
|
||||
abiParameter: AbiParameter;
|
||||
data: unknown;
|
||||
level: number;
|
||||
mode: ResultViewMode;
|
||||
}
|
||||
|
||||
const ItemTuple = ({ abiParameter, data, mode, level }: Props) => {
|
||||
return (
|
||||
<p>
|
||||
<p>
|
||||
<span>{ printRowOffset(level) }</span>
|
||||
<chakra.span fontWeight={ 500 }>{ abiParameter.name || abiParameter.internalType }</chakra.span>
|
||||
<span> { '{' }</span>
|
||||
</p>
|
||||
{ 'components' in abiParameter && abiParameter.components.map((component, index) => {
|
||||
const dataObj = typeof data === 'object' && data !== null ? data : undefined;
|
||||
const itemData = dataObj && component.name && component.name in dataObj ? dataObj[component.name as keyof typeof dataObj] : undefined;
|
||||
return (
|
||||
<Item
|
||||
key={ index }
|
||||
abiParameter={ component }
|
||||
data={ itemData }
|
||||
mode={ mode }
|
||||
level={ level + 1 }
|
||||
/>
|
||||
);
|
||||
}) }
|
||||
<p>{ printRowOffset(level) }{ '}' }</p>
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ItemTuple);
|
||||
@@ -0,0 +1,2 @@
|
||||
const TAB_SIZE = 2;
|
||||
export const printRowOffset = (level: number) => ' '.repeat(level * TAB_SIZE);
|
||||