Skip to content

Commit

Permalink
Combine Read/Write for Contract page (#2343)
Browse files Browse the repository at this point in the history
* combine tabs and add method type filter

* add badge with contract method type

* add name filter

* add filters to mud system tab

* support array of tabs ids and route old tabs to the new one

* fix tests

* support adding the custom ABI from the contract page

* update margins and add test

* fix bugs and update screenshots

* some design updates

* update preset for garnet

* [skip ci] fix garnet preset

* improve links to contract methods
  • Loading branch information
tom2drum authored Nov 4, 2024
1 parent 2c35fb1 commit f93310d
Show file tree
Hide file tree
Showing 63 changed files with 760 additions and 346 deletions.
75 changes: 39 additions & 36 deletions configs/envs/.env.garnet
Original file line number Diff line number Diff line change
@@ -1,50 +1,53 @@
# Set of ENVs for Garnet (dev only)
# https://https://explorer.garnetchain.com//
# Set of ENVs for Garnet Testnet network explorer
# https://explorer.garnetchain.com
# This is an auto-generated file. To update all values, run "yarn preset:sync --name=garnet"

# app configuration
# Local ENVs
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws

# blockchain parameters
NEXT_PUBLIC_NETWORK_NAME="Garnet Testnet"
NEXT_PUBLIC_NETWORK_ID=17069
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_RPC_URL=https://partner-rpc.garnetchain.com/tireless-strand-dreamt-overcome

# api configuration
NEXT_PUBLIC_API_HOST=explorer.garnetchain.com
# Instance ENVs
NEXT_PUBLIC_AD_BANNER_PROVIDER=none
NEXT_PUBLIC_AD_TEXT_PROVIDER=none
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/

# ui config
## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
## views
NEXT_PUBLIC_API_HOST=explorer.garnetchain.com
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
# app features
NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3000/login
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/redstone-testnet.json
NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/redstone.json
NEXT_PUBLIC_AD_BANNER_PROVIDER=none
## sidebar
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x5b0ba69f2cf5fbc6da96b6cf475c5521f7a385efd9d68673f69c1fc54f737a52
NEXT_PUBLIC_HAS_MUD_FRAMEWORK=true
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgb(169, 31, 47)']}
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_IS_TESTNET=true
NEXT_PUBLIC_LOGOUT_URL=https://redstone-lattice.us.auth0.com/v2/logout
NEXT_PUBLIC_MARKETPLACE_ENABLED=false
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/garnet.svg
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/garnet.svg
NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/garnet-dark.svg
NEXT_PUBLIC_NETWORK_ID=17069
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/garnet.svg
NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/garnet-dark.svg
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgb(169, 31, 47)
NEXT_PUBLIC_OG_DESCRIPTION="Redstone is the home for onchain games, worlds, and other MUD applications"
# rollup
NEXT_PUBLIC_ROLLUP_TYPE=optimistic
NEXT_PUBLIC_NETWORK_NAME=Garnet Testnet
NEXT_PUBLIC_NETWORK_RPC_URL=https://partner-rpc.garnetchain.com/tireless-strand-dreamt-overcome
NEXT_PUBLIC_NETWORK_SHORT_NAME=Garnet Testnet
NEXT_PUBLIC_OG_DESCRIPTION=Redstone is the home for onchain games, worlds, and other MUD applications
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/garnet.png
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth-holesky.blockscout.com/
NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://garnet.qry.live/withdraw
NEXT_PUBLIC_HAS_MUD_FRAMEWORK=true
NEXT_PUBLIC_ROLLUP_TYPE=optimistic
NEXT_PUBLIC_STATS_API_HOST=https://stats-redstone-garnet.k8s.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
14 changes: 14 additions & 0 deletions theme/components/Badge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ const variantSubtle = defineStyle((props) => {
};
}

if (c === 'black-blue') {
return {
bg: mode('blue.50', 'blue.800')(props),
color: mode('blackAlpha.800', 'whiteAlpha.800')(props),
};
}

if (c === 'black-purple') {
return {
bg: mode('purple.100', 'purple.800')(props),
color: mode('blackAlpha.800', 'whiteAlpha.800')(props),
};
}

return {
bg: mode(`${ c }.50`, `${ c }.800`)(props),
color: mode(`${ c }.500`, `${ c }.100`)(props),
Expand Down
3 changes: 3 additions & 0 deletions theme/components/Button/Button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ const variantRadioGroup = defineStyle((props) => {
_active: {
bgColor: 'none',
},
_notFirst: {
borderLeftWidth: 0,
},
// We have a special state for this button variant that serves as a popover trigger.
// When any items (filters) are selected in the popover, the button should change its background and text color.
// The last CSS selector is for redefining styles for the TabList component.
Expand Down
3 changes: 0 additions & 3 deletions theme/components/Tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,6 @@ const variantRadioGroup = definePartsStyle((props) => {
&[data-selected=true][aria-selected=true]
`],
borderRadius: 'none',
_notFirst: {
borderLeftWidth: 0,
},
'&[role="tab"]': {
_first: {
borderTopLeftRadius: 'base',
Expand Down
1 change: 1 addition & 0 deletions tools/preset-sync/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const PRESETS = {
eth: 'https://eth.blockscout.com',
eth_goerli: 'https://eth-goerli.blockscout.com',
eth_sepolia: 'https://eth-sepolia.blockscout.com',
garnet: 'https://explorer.garnetchain.com',
gnosis: 'https://gnosis.blockscout.com',
optimism: 'https://optimism.blockscout.com',
optimism_celestia: 'https://opcelestia-raspberry.gelatoscout.com',
Expand Down
19 changes: 3 additions & 16 deletions types/api/account.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Abi } from 'viem';

import type { AddressParam } from './addressParams';
export interface AddressTag {
address_hash: string;
Expand Down Expand Up @@ -111,22 +113,7 @@ export interface CustomAbi {
id: number;
contract_address_hash: string;
contract_address: AddressParam;
abi: Array<AbiItem>;
}

export interface AbiItem {
type: 'function';
stateMutability: 'nonpayable' | 'view';
payable: boolean;
outputs: Array<AbiInputOutput>;
name: string;
inputs: Array<AbiInputOutput>;
constant: boolean;
}

interface AbiInputOutput {
type: 'uint256' | 'address';
name: string;
abi: Abi;
}

export type WatchlistErrors = {
Expand Down
16 changes: 8 additions & 8 deletions ui/address/AddressContract.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ test.describe('ABI functionality', () => {

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 expect(component.getByLabel('4.').getByRole('button', { name: 'Simulate' })).toBeEnabled();
await expect(component.getByLabel('4.').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();
await expect(component.getByLabel('7.').getByRole('button', { name: 'Simulate' })).toBeHidden();
await expect(component.getByLabel('7.').getByRole('button', { name: 'Write' })).toBeEnabled();
});

test('write, no wallet client', async({ render, createSocket, mockEnvs }) => {
Expand All @@ -86,11 +86,11 @@ test.describe('ABI functionality', () => {

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 expect(component.getByLabel('4.').getByRole('button', { name: 'Simulate' })).toBeEnabled();
await expect(component.getByLabel('4.').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();
await expect(component.getByLabel('7.').getByRole('button', { name: 'Simulate' })).toBeHidden();
await expect(component.getByLabel('7.').getByRole('button', { name: 'Write' })).toBeDisabled();
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 28 additions & 4 deletions ui/address/contract/methods/ContractAbi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,24 @@ import React from 'react';

import type { SmartContractMethod } from './types';

import { route } from 'nextjs-routes';

import { apos } from 'lib/html-entities';
import LinkInternal from 'ui/shared/links/LinkInternal';

import ContractAbiItem from './ContractAbiItem';
import useFormSubmit from './useFormSubmit';
import useScrollToMethod from './useScrollToMethod';

interface Props {
abi: Array<SmartContractMethod>;
visibleItems?: Array<number>;
addressHash: string;
tab: string;
sourceAddress?: string;
}

const ContractAbi = ({ abi, addressHash, sourceAddress, tab }: Props) => {
const ContractAbi = ({ abi, addressHash, sourceAddress, tab, visibleItems }: Props) => {
const [ expandedSections, setExpandedSections ] = React.useState<Array<number>>(abi.length === 1 ? [ 0 ] : []);
const [ id, setId ] = React.useState(0);

Expand Down Expand Up @@ -43,8 +49,10 @@ const ContractAbi = ({ abi, addressHash, sourceAddress, tab }: Props) => {
setId((id) => id + 1);
}, []);

const hasVisibleItems = !visibleItems || visibleItems.length > 0;

return (
<>
<div>
<Flex mb={ 3 }>
<Box fontWeight={ 500 } mr="auto">Contract information</Box>
{ abi.length > 1 && (
Expand All @@ -58,17 +66,33 @@ const ContractAbi = ({ abi, addressHash, sourceAddress, tab }: Props) => {
{ abi.map((item, index) => (
<ContractAbiItem
key={ index }
data={ item }
id={ id }
index={ index }
data={ item }
isVisible={ !visibleItems || visibleItems.includes(index) }
addressHash={ addressHash }
sourceAddress={ sourceAddress }
tab={ tab }
onSubmit={ handleFormSubmit }
/>
)) }
</Accordion>
</>
{ !hasVisibleItems && (
<div>
<div>Couldn{ apos }t find any method that matches your query.</div>
<div>
You can use custom ABI for this contract without verifying the contract in the{ ' ' }
<LinkInternal
href={ route({ pathname: '/address/[hash]', query: { hash: addressHash, tab: 'read_write_custom_methods' } }) }
scroll={ false }
>
Custom ABI
</LinkInternal>
{ ' ' }tab.
</div>
</div>
) }
</div>
);
};

Expand Down
Loading

0 comments on commit f93310d

Please sign in to comment.