Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Contract interaction improvements #1875

Merged
merged 12 commits into from
May 6, 2024
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
Loading