Skip to content

Commit

Permalink
Contract interaction improvements (blockscout#1875)
Browse files Browse the repository at this point in the history
* add contract method id tag

* add form submit type

* refactor types for SmartConstract and make new component for Contract ABI methods

* move contract method form inside ABI folder

* handle form submit and display result

* clean-up

* change copied text

* handle case when blockchain interaction is not configured

* tests and screenshots update

* fix bug wit WEI checkbox
  • Loading branch information
tom2drum authored and DaMandal0rian committed May 6, 2024
1 parent 7862859 commit f812b89
Show file tree
Hide file tree
Showing 99 changed files with 996 additions and 933 deletions.
23 changes: 23 additions & 0 deletions lib/web3/useAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { UseAccountReturnType } from 'wagmi';
import { useAccount } from 'wagmi';

import config from 'configs/app';

function useAccountFallback(): UseAccountReturnType {
return {
address: undefined,
addresses: undefined,
chain: undefined,
chainId: undefined,
connector: undefined,
isConnected: false,
isConnecting: false,
isDisconnected: true,
isReconnecting: false,
status: 'disconnected',
};
}

const hook = config.features.blockchainInteraction.isEnabled ? useAccount : useAccountFallback;

export default hook;
8 changes: 4 additions & 4 deletions mocks/contract/methods.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {
SmartContractQueryMethodReadError,
SmartContractQueryMethodReadSuccess,
SmartContractQueryMethodError,
SmartContractQueryMethodSuccess,
SmartContractReadMethod,
SmartContractWriteMethod,
} from 'types/api/contract';
Expand Down Expand Up @@ -94,7 +94,7 @@ export const read: Array<SmartContractReadMethod> = [
},
];

export const readResultSuccess: SmartContractQueryMethodReadSuccess = {
export const readResultSuccess: SmartContractQueryMethodSuccess = {
is_error: false,
result: {
names: [ 'amount' ],
Expand All @@ -104,7 +104,7 @@ export const readResultSuccess: SmartContractQueryMethodReadSuccess = {
},
};

export const readResultError: SmartContractQueryMethodReadError = {
export const readResultError: SmartContractQueryMethodError = {
is_error: true,
result: {
message: 'Some shit happened',
Expand Down
20 changes: 17 additions & 3 deletions playwright/TestApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import theme from 'theme';
export type Props = {
children: React.ReactNode;
withSocket?: boolean;
withWalletClient?: boolean;
appContext?: {
pageProps: PageProps;
};
Expand Down Expand Up @@ -47,7 +48,20 @@ const wagmiConfig = createConfig({
},
});

const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props) => {
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 [ queryClient ] = React.useState(() => new QueryClient({
defaultOptions: {
queries: {
Expand All @@ -63,9 +77,9 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props
<SocketProvider url={ withSocket ? `ws://${ config.app.host }:${ app.socketPort }` : undefined }>
<AppContextProvider { ...appContext }>
<GrowthBookProvider>
<WagmiProvider config={ wagmiConfig }>
<WalletClientProvider withWalletClient={ withWalletClient }>
{ children }
</WagmiProvider>
</WalletClientProvider>
</GrowthBookProvider>
</AppContextProvider>
</SocketProvider>
Expand Down
3 changes: 3 additions & 0 deletions playwright/fixtures/mockEnvs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,7 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
blockHiddenFields: [
[ 'NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS', '["burnt_fees", "total_reward", "nonce"]' ],
],
noWalletClient: [
[ 'NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID', '' ],
],
};
54 changes: 12 additions & 42 deletions types/api/contract.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Abi, AbiType } from 'abitype';
import type { Abi, AbiType, AbiFallback, AbiFunction, AbiReceive } from 'abitype';

export type SmartContractMethodArgType = AbiType;
export type SmartContractMethodStateMutability = 'view' | 'nonpayable' | 'payable';
Expand Down Expand Up @@ -78,49 +78,19 @@ export interface SmartContractExternalLibrary {
name: string;
}

export interface SmartContractMethodBase {
inputs: Array<SmartContractMethodInput>;
outputs?: Array<SmartContractMethodOutput>;
constant: boolean;
name: string;
stateMutability: SmartContractMethodStateMutability;
type: 'function';
payable: boolean;
error?: 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 interface SmartContractWriteFallback {
payable?: true;
stateMutability: 'payable';
type: 'fallback';
}

export interface SmartContractWriteReceive {
payable?: true;
stateMutability: 'payable';
type: 'receive';
}

export type SmartContractWriteMethod = SmartContractMethodBase | SmartContractWriteFallback | SmartContractWriteReceive;

export type SmartContractWriteMethod = SmartContractMethodBase | AbiFallback | AbiReceive;
export type SmartContractMethod = SmartContractReadMethod | SmartContractWriteMethod;

export interface SmartContractMethodInput {
internalType?: string; // there could be any string, e.g "enum MyEnum"
name: string;
type: SmartContractMethodArgType;
components?: Array<SmartContractMethodInput>;
fieldType?: 'native_coin';
}

export interface SmartContractMethodOutput extends SmartContractMethodInput {
value?: string | boolean | object;
}

export interface SmartContractQueryMethodReadSuccess {
export interface SmartContractQueryMethodSuccess {
is_error: false;
result: {
names: Array<string | [ string, Array<string> ]>;
Expand All @@ -131,7 +101,7 @@ export interface SmartContractQueryMethodReadSuccess {
};
}

export interface SmartContractQueryMethodReadError {
export interface SmartContractQueryMethodError {
is_error: true;
result: {
code: number;
Expand All @@ -147,7 +117,7 @@ export interface SmartContractQueryMethodReadError {
};
}

export type SmartContractQueryMethodRead = SmartContractQueryMethodReadSuccess | SmartContractQueryMethodReadError;
export type SmartContractQueryMethod = SmartContractQueryMethodSuccess | SmartContractQueryMethodError;

// VERIFICATION

Expand Down
94 changes: 94 additions & 0 deletions ui/address/AddressContract.pw.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React from 'react';

import * as addressMock from 'mocks/address/address';
import * as contractInfoMock from 'mocks/contract/info';
import * as contractMethodsMock from 'mocks/contract/methods';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import * as socketServer from 'playwright/fixtures/socketServer';
import { test, expect } from 'playwright/lib';

import AddressContract from './AddressContract.pwstory';

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' } });
});

test.describe('ABI functionality', () => {
test('read', async({ render, createSocket }) => {
const hooksConfig = {
router: {
query: { hash, tab: 'read_contract' },
},
};
const component = await render(<AddressContract/>, { hooksConfig }, { withSocket: true });
const socket = await createSocket();
await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase());

await expect(component.getByRole('button', { name: 'Connect wallet' })).toBeVisible();
await component.getByText('FLASHLOAN_PREMIUM_TOTAL').click();
await expect(component.getByRole('button', { name: 'Read' })).toBeVisible();
});

test('read, no wallet client', async({ render, createSocket, mockEnvs }) => {
const hooksConfig = {
router: {
query: { hash, tab: 'read_contract' },
},
};
await mockEnvs(ENVS_MAP.noWalletClient);
const component = await render(<AddressContract/>, { hooksConfig }, { withSocket: true, withWalletClient: false });
const socket = await createSocket();
await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase());

await expect(component.getByRole('button', { name: 'Connect wallet' })).toBeHidden();
await component.getByText('FLASHLOAN_PREMIUM_TOTAL').click();
await expect(component.getByRole('button', { name: 'Read' })).toBeVisible();
});

test('write', async({ render, createSocket }) => {
const hooksConfig = {
router: {
query: { hash, tab: 'write_contract' },
},
};
const component = await render(<AddressContract/>, { hooksConfig }, { withSocket: true });
const socket = await createSocket();
await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase());

await expect(component.getByRole('button', { name: 'Connect wallet' })).toBeVisible();
await component.getByText('setReserveInterestRateStrategyAddress').click();
await expect(component.getByLabel('2.').getByRole('button', { name: 'Simulate' })).toBeEnabled();
await expect(component.getByLabel('2.').getByRole('button', { name: 'Write' })).toBeEnabled();

await component.getByText('pause').click();
await expect(component.getByLabel('5.').getByRole('button', { name: 'Simulate' })).toBeHidden();
await expect(component.getByLabel('5.').getByRole('button', { name: 'Write' })).toBeEnabled();
});

test('write, no wallet client', async({ render, createSocket, mockEnvs }) => {
const hooksConfig = {
router: {
query: { hash, tab: 'write_contract' },
},
};
await mockEnvs(ENVS_MAP.noWalletClient);

const component = await render(<AddressContract/>, { hooksConfig }, { withSocket: true, withWalletClient: false });
const socket = await createSocket();
await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase());

await expect(component.getByRole('button', { name: 'Connect wallet' })).toBeHidden();
await component.getByText('setReserveInterestRateStrategyAddress').click();
await expect(component.getByLabel('2.').getByRole('button', { name: 'Simulate' })).toBeEnabled();
await expect(component.getByLabel('2.').getByRole('button', { name: 'Write' })).toBeDisabled();

await component.getByText('pause').click();
await expect(component.getByLabel('5.').getByRole('button', { name: 'Simulate' })).toBeHidden();
await expect(component.getByLabel('5.').getByRole('button', { name: 'Write' })).toBeDisabled();
});
});
18 changes: 18 additions & 0 deletions ui/address/AddressContract.pwstory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useRouter } from 'next/router';
import React from 'react';

import useApiQuery from 'lib/api/useApiQuery';
import useContractTabs from 'lib/hooks/useContractTabs';
import getQueryParamString from 'lib/router/getQueryParamString';

import AddressContract from './AddressContract';

const AddressContractPwStory = () => {
const router = useRouter();
const hash = getQueryParamString(router.query.hash);
const addressQuery = useApiQuery('address', { pathParams: { hash } });
const { tabs } = useContractTabs(addressQuery.data, false);
return <AddressContract tabs={ tabs } shouldRender={ true } isLoading={ false }/>;
};

export default AddressContractPwStory;
12 changes: 1 addition & 11 deletions ui/address/AddressContract.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import React from 'react';
import type { RoutedSubTab } from 'ui/shared/Tabs/types';

import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import Web3ModalProvider from 'ui/shared/Web3ModalProvider';

interface Props {
tabs: Array<RoutedSubTab>;
Expand All @@ -16,21 +15,12 @@ const TAB_LIST_PROPS = {
};

const AddressContract = ({ tabs, isLoading, shouldRender }: Props) => {
const fallback = React.useCallback(() => {
const noProviderTabs = tabs.filter(({ id }) => id === 'contract_code' || id.startsWith('read_'));
return (
<RoutedTabs tabs={ noProviderTabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS } isLoading={ isLoading }/>
);
}, [ isLoading, tabs ]);

if (!shouldRender) {
return null;
}

return (
<Web3ModalProvider fallback={ fallback }>
<RoutedTabs tabs={ tabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS } isLoading={ isLoading }/>
</Web3ModalProvider>
<RoutedTabs tabs={ tabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS } isLoading={ isLoading }/>
);
};

Expand Down
Loading

0 comments on commit f812b89

Please sign in to comment.