Commit c07fb4e2 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into tom2drum/content-max-width

parents 96191dfb 3e24ed76
......@@ -20,6 +20,7 @@ on:
- eth_sepolia
- eth_goerli
- optimism
- optimism_celestia
- optimism_sepolia
- polygon
- rootstock
......
......@@ -20,6 +20,7 @@ on:
- eth_sepolia
- eth_goerli
- optimism
- optimism_celestia
- optimism_sepolia
- polygon
- rootstock
......
......@@ -368,6 +368,7 @@
"eth_goerli",
"eth_sepolia",
"optimism",
"optimism_celestia",
"optimism_sepolia",
"polygon",
"rootstock_testnet",
......
......@@ -25,6 +25,7 @@ export { default as publicTagsSubmission } from './publicTagsSubmission';
export { default as restApiDocs } from './restApiDocs';
export { default as rollup } from './rollup';
export { default as safe } from './safe';
export { default as saveOnGas } from './saveOnGas';
export { default as sentry } from './sentry';
export { default as sol2uml } from './sol2uml';
export { default as stats } from './stats';
......
......@@ -2,6 +2,8 @@ import type { Feature } from './types';
import type { RollupType } from 'types/client/rollup';
import { ROLLUP_TYPES } from 'types/client/rollup';
import stripTrailingSlash from 'lib/stripTrailingSlash';
import { getEnvValue } from '../utils';
const type = (() => {
......@@ -21,7 +23,7 @@ const config: Feature<{ type: RollupType; L1BaseUrl: string; L2WithdrawalUrl?: s
title,
isEnabled: true,
type,
L1BaseUrl,
L1BaseUrl: stripTrailingSlash(L1BaseUrl),
L2WithdrawalUrl,
});
}
......
import type { Feature } from './types';
import { getEnvValue } from '../utils';
import marketplace from './marketplace';
const title = 'Save on gas with GasHawk';
const config: Feature<{
apiUrlTemplate: string;
}> = (() => {
if (getEnvValue('NEXT_PUBLIC_SAVE_ON_GAS_ENABLED') === 'true' && marketplace.isEnabled) {
return Object.freeze({
title,
isEnabled: true,
apiUrlTemplate: 'https://core.gashawk.io/apiv2/stats/address/<address>/savingsPotential/0x1',
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { ContractCodeIde } from 'types/client/contract';
import { NAVIGATION_LINK_IDS, type NavItemExternal, type NavigationLinkId, type NavigationLayout } from 'types/client/navigation';
import type { ChainIndicatorId } from 'types/homepage';
import { HOME_STATS_WIDGET_IDS, type ChainIndicatorId, type HeroBannerConfig, type HomeStatsWidgetId } from 'types/homepage';
import type { NetworkExplorer } from 'types/networks';
import type { ColorThemeId } from 'types/settings';
import type { FontFamily } from 'types/ui';
import { COLOR_THEMES } from 'lib/settings/colorTheme';
import * as features from './features';
import * as views from './ui/views';
import { getEnvValue, getExternalAssetFilePath, parseEnvJson } from './utils';
......@@ -24,6 +26,22 @@ const hiddenLinks = (() => {
return result;
})();
const homePageStats: Array<HomeStatsWidgetId> = (() => {
const parsedValue = parseEnvJson<Array<HomeStatsWidgetId>>(getEnvValue('NEXT_PUBLIC_HOMEPAGE_STATS'));
if (!Array.isArray(parsedValue)) {
const rollupFeature = features.rollup;
if (rollupFeature.isEnabled && [ 'zkEvm', 'zkSync', 'arbitrum' ].includes(rollupFeature.type)) {
return [ 'latest_batch', 'average_block_time', 'total_txs', 'wallet_addresses', 'gas_tracker' ];
}
return [ 'total_blocks', 'average_block_time', 'total_txs', 'wallet_addresses', 'gas_tracker' ];
}
return parsedValue.filter((item) => HOME_STATS_WIDGET_IDS.includes(item));
})();
const highlightedRoutes = (() => {
const parsedValue = parseEnvJson<Array<NavigationLinkId>>(getEnvValue('NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES'));
return Array.isArray(parsedValue) ? parsedValue : [];
......@@ -34,9 +52,6 @@ const defaultColorTheme = (() => {
return COLOR_THEMES.find((theme) => theme.id === envValue);
})();
// eslint-disable-next-line max-len
const HOMEPAGE_PLATE_BACKGROUND_DEFAULT = 'radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)';
const UI = Object.freeze({
navigation: {
logo: {
......@@ -60,11 +75,13 @@ const UI = Object.freeze({
},
homepage: {
charts: parseEnvJson<Array<ChainIndicatorId>>(getEnvValue('NEXT_PUBLIC_HOMEPAGE_CHARTS')) || [],
stats: homePageStats,
heroBanner: parseEnvJson<HeroBannerConfig>(getEnvValue('NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG')),
// !!! DEPRECATED !!!
plate: {
background: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND') || HOMEPAGE_PLATE_BACKGROUND_DEFAULT,
textColor: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR') || 'white',
background: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND'),
textColor: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR'),
},
showAvgBlockTime: getEnvValue('NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME') === 'false' ? false : true,
},
views,
indexingAlert: {
......@@ -88,6 +105,10 @@ const UI = Object.freeze({
colorTheme: {
'default': defaultColorTheme,
},
fonts: {
heading: parseEnvJson<FontFamily>(getEnvValue('NEXT_PUBLIC_FONT_FAMILY_HEADING')),
body: parseEnvJson<FontFamily>(getEnvValue('NEXT_PUBLIC_FONT_FAMILY_BODY')),
},
maxContentWidth: getEnvValue('NEXT_PUBLIC_MAX_CONTENT_WIDTH_ENABLED') === 'false' ? false : true,
});
......
......@@ -36,4 +36,5 @@ NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/celo.png
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
\ No newline at end of file
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_HOMEPAGE_STATS=['total_blocks','average_block_time','total_txs','wallet_addresses','gas_tracker','current_epoch']
\ No newline at end of file
......@@ -49,7 +49,7 @@ NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'GeckoTerminal','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/geckoterminal.png','baseUrl':'https://www.geckoterminal.com/','paths':{'token':'/eth/pools'}},{'title':'Etherscan','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/etherscan.png','baseUrl':'https://etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}, {'title':'blockchair','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/blockchair.png','baseUrl':'https://blockchair.com/','paths':{'tx':'/ethereum/transaction','address':'/ethereum/address','token':'/ethereum/erc-20/token','block':'/ethereum/block'}},{'title':'sentio','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/sentio.png','baseUrl':'https://app.sentio.xyz/','paths':{'tx':'/tx/1','address':'/contract/1'}}, {'title':'Tenderly','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/tenderly.png','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/mainnet'}}, {'title':'0xPPL','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/0xPPL.png','baseUrl':'https://0xppl.com','paths':{'tx':'/Ethereum/tx','address':'/','token':'/c/Ethereum'}}, {'title':'3xpl','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/3xpl.png','baseUrl':'https://3xpl.com/','paths':{'tx':'/ethereum/transaction','address':'/ethereum/address'}} ]
NEXT_PUBLIC_NETWORK_ID=1
NEXT_PUBLIC_NETWORK_NAME=Ethereum
NEXT_PUBLIC_NETWORK_RPC_URL=https://eth.llamarpc.com
NEXT_PUBLIC_NETWORK_RPC_URL=https://eth.drpc.org
NEXT_PUBLIC_NETWORK_SHORT_NAME=Ethereum
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
......@@ -62,3 +62,4 @@ NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s-prod-1.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
NEXT_PUBLIC_SAVE_ON_GAS_ENABLED=true
......@@ -24,7 +24,6 @@ NEXT_PUBLIC_API_BASE_PATH=/
# ui config
## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=
## sidebar
NEXT_PUBLIC_NETWORK_LOGO=
......
# Set of ENVs for OP Celestia Raspberry network explorer
# https://opcelestia-raspberry.gelatoscout.com
# This is an auto-generated file. To update all values, run "yarn preset:sync --name=optimism_celestia"
# 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
# Instance ENVs
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={'id':'721628','width':'728','height':'90'}
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={'id':'721627','width':'300','height':'100'}
NEXT_PUBLIC_AD_BANNER_PROVIDER=adbutler
NEXT_PUBLIC_AD_TEXT_PROVIDER=none
NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_API_HOST=opcelestia-raspberry.gelatoscout.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'}]
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/opcelestia-raspberry.json
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x0f5b54de81848d8d8baa02c69030037218a2b4df622d64a2a429e11721606656
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgba(255, 0, 0, 1)
NEXT_PUBLIC_IS_TESTNET=true
NEXT_PUBLIC_MARKETPLACE_ENABLED=false
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism.svg
NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism.svg
NEXT_PUBLIC_NETWORK_ID=123420111
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg
NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg
NEXT_PUBLIC_NETWORK_NAME=OP Celestia Raspberry
NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.opcelestia-raspberry.gelato.digital
NEXT_PUBLIC_NETWORK_SHORT_NAME=opcelestia-raspberry
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth-sepolia.blockscout.com/
NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://bridge.gelato.network/bridge/opcelestia-raspberry
NEXT_PUBLIC_ROLLUP_TYPE=optimistic
NEXT_PUBLIC_STATS_API_HOST=https://stats-opcelestia-raspberry.k8s.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_WEB3_WALLETS=none
\ No newline at end of file
......@@ -25,7 +25,6 @@ NEXT_PUBLIC_API_BASE_PATH=/
# ui config
## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true
## sidebar
## footer
NEXT_PUBLIC_GIT_TAG=v1.0.11
......
......@@ -42,4 +42,5 @@ NEXT_PUBLIC_STATS_API_HOST=https://stats-rsk-testnet.k8s.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=['burnt_fees','total_reward','nonce']
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
\ No newline at end of file
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_HOMEPAGE_STATS=['total_blocks','average_block_time','total_txs','wallet_addresses','gas_tracker','btc_locked']
\ No newline at end of file
......@@ -18,6 +18,12 @@ echo "window.__envs = {" >> $output_file;
# Iterate through all environment variables
for var in $(env | grep '^NEXT_PUBLIC_' | cut -d= -f1); do
# Skip variables that start with NEXT_PUBLIC_VERCEL. Vercel injects these
# and they can cause runtime errors, particularly when commit messages wrap lines.
if [[ $var == NEXT_PUBLIC_VERCEL* ]]; then
continue
fi
# Get the value of the variable
value="${!var}"
......
......@@ -20,6 +20,7 @@ async function run() {
return result;
}, {} as Record<string, string>);
printDeprecationWarning(appEnvs);
await checkPlaceholdersCongruity(appEnvs);
await validateEnvs(appEnvs);
......@@ -135,3 +136,15 @@ function getEnvsPlaceholders(filePath: string): Promise<Array<string>> {
});
});
}
function printDeprecationWarning(envsMap: Record<string, string>) {
if (
envsMap.NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR ||
envsMap.NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND
) {
console.log('❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗');
// eslint-disable-next-line max-len
console.warn('The NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR and NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND variables are now deprecated and will be removed in the next release. Please migrate to the NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG variable.');
console.log('❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗\n');
}
}
......@@ -29,10 +29,11 @@ import type { ValidatorsChainType } from '../../../types/client/validators';
import type { WalletType } from '../../../types/client/wallets';
import { SUPPORTED_WALLETS } from '../../../types/client/wallets';
import type { CustomLink, CustomLinksGroup } from '../../../types/footerLinks';
import { CHAIN_INDICATOR_IDS } from '../../../types/homepage';
import type { ChainIndicatorId } from '../../../types/homepage';
import { CHAIN_INDICATOR_IDS, HOME_STATS_WIDGET_IDS } from '../../../types/homepage';
import type { ChainIndicatorId, HeroBannerButtonState, HeroBannerConfig, HomeStatsWidgetId } from '../../../types/homepage';
import { type NetworkVerificationTypeEnvs, type NetworkExplorer, type FeaturedNetwork, NETWORK_GROUPS } from '../../../types/networks';
import { COLOR_THEME_IDS } from '../../../types/settings';
import type { FontFamily } from '../../../types/ui';
import type { AddressViewId } from '../../../types/views/address';
import { ADDRESS_VIEWS_IDS, IDENTICON_TYPES } from '../../../types/views/address';
import { BLOCK_FIELDS_IDS } from '../../../types/views/block';
......@@ -390,6 +391,34 @@ const navItemExternalSchema: yup.ObjectSchema<NavItemExternal> = yup
url: yup.string().test(urlTest).required(),
});
const fontFamilySchema: yup.ObjectSchema<FontFamily> = yup
.object()
.transform(replaceQuotes)
.json()
.shape({
name: yup.string().required(),
url: yup.string().test(urlTest).required(),
});
const heroBannerButtonStateSchema: yup.ObjectSchema<HeroBannerButtonState> = yup.object({
background: yup.array().max(2).of(yup.string()),
text_color: yup.array().max(2).of(yup.string()),
});
const heroBannerSchema: yup.ObjectSchema<HeroBannerConfig> = yup.object()
.transform(replaceQuotes)
.json()
.shape({
background: yup.array().max(2).of(yup.string()),
text_color: yup.array().max(2).of(yup.string()),
border: yup.array().max(2).of(yup.string()),
button: yup.object({
_default: heroBannerButtonStateSchema,
_hover: heroBannerButtonStateSchema,
_selected: heroBannerButtonStateSchema,
}),
});
const footerLinkSchema: yup.ObjectSchema<CustomLink> = yup
.object({
text: yup.string().required(),
......@@ -538,9 +567,30 @@ const schema = yup
.transform(replaceQuotes)
.json()
.of(yup.string<ChainIndicatorId>().oneOf(CHAIN_INDICATOR_IDS)),
NEXT_PUBLIC_HOMEPAGE_STATS: yup
.array()
.transform(replaceQuotes)
.json()
.of(yup.string<HomeStatsWidgetId>().oneOf(HOME_STATS_WIDGET_IDS)),
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR: yup.string(),
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND: yup.string(),
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME: yup.boolean(),
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG: yup
.mixed()
.test(
'shape',
(ctx) => {
try {
heroBannerSchema.validateSync(ctx.originalValue);
throw new Error('Unknown validation error');
} catch (error: unknown) {
const message = typeof error === 'object' && error !== null && 'errors' in error && Array.isArray(error.errors) ? error.errors.join(', ') : '';
return 'Invalid schema were provided for NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG' + (message ? `: ${ message }` : '');
}
},
(data) => {
const isUndefined = data === undefined;
return isUndefined || heroBannerSchema.isValidSync(data);
}),
// b. sidebar
NEXT_PUBLIC_FEATURED_NETWORKS: yup
......@@ -634,6 +684,18 @@ const schema = yup
NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS: yup.boolean(),
NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE: yup.string(),
NEXT_PUBLIC_COLOR_THEME_DEFAULT: yup.string().oneOf(COLOR_THEME_IDS),
NEXT_PUBLIC_FONT_FAMILY_HEADING: yup
.mixed()
.test('shape', 'Invalid schema were provided for NEXT_PUBLIC_FONT_FAMILY_HEADING', (data) => {
const isUndefined = data === undefined;
return isUndefined || fontFamilySchema.isValidSync(data);
}),
NEXT_PUBLIC_FONT_FAMILY_BODY: yup
.mixed()
.test('shape', 'Invalid schema were provided for NEXT_PUBLIC_FONT_FAMILY_BODY', (data) => {
const isUndefined = data === undefined;
return isUndefined || fontFamilySchema.isValidSync(data);
}),
NEXT_PUBLIC_MAX_CONTENT_WIDTH_ENABLED: yup.boolean(),
// 5. Features configuration
......@@ -740,6 +802,7 @@ const schema = yup
value => value === undefined,
),
}),
NEXT_PUBLIC_SAVE_ON_GAS_ENABLED: yup.boolean(),
// 6. External services envs
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(),
......
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=none
NEXT_PUBLIC_API_SPEC_URL=none
NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=none
\ No newline at end of file
NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=none
NEXT_PUBLIC_HOMEPAGE_STATS=[]
\ No newline at end of file
......@@ -28,15 +28,18 @@ NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true
NEXT_PUBLIC_FEATURED_NETWORKS=https://example.com
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/accounts','/apps']
NEXT_PUBLIC_NAVIGATION_LAYOUT=horizontal
NEXT_PUBLIC_FONT_FAMILY_HEADING={'name':'Montserrat','url':'https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap'}
NEXT_PUBLIC_FONT_FAMILY_BODY={'name':'Raleway','url':'https://fonts.googleapis.com/css2?family=Raleway:wght@400;500;600;700&display=swap'}
NEXT_PUBLIC_FOOTER_LINKS=https://example.com
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS=false
NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=false
NEXT_PUBLIC_MAX_CONTENT_WIDTH_ENABLED=false
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_STATS=['total_blocks','average_block_time','total_txs','wallet_addresses','gas_tracker','current_epoch']
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR='#fff'
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND='rgb(255, 145, 0)'
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['lightpink'],'text_color':['deepskyblue','white'],'border':['3px solid black']}
NEXT_PUBLIC_GAS_TRACKER_ENABLED=true
NEXT_PUBLIC_GAS_TRACKER_UNITS=['gwei']
NEXT_PUBLIC_IS_TESTNET=true
......@@ -82,3 +85,4 @@ NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE=stability
NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'uniswap'},{'text':'Payment link','icon':'payment_link','url':'https://example.com'}]
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}
NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Need gas?', 'dapp_id': 'smol-refuel', 'url_template': 'https://smolrefuel.com/?outboundChain={chainId}&partner=blockscout&utm_source=blockscout&utm_medium=address&disableBridges=true', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png'}
NEXT_PUBLIC_SAVE_ON_GAS_ENABLED=true
......@@ -33,9 +33,12 @@ CONFIG_TEMPLATE_FILE="config.template.json"
# Path to the generated config JSON file
CONFIG_FILE="config.json"
# Escape special characters in MASTER_URL for sed
ESCAPED_MASTER_URL=$(printf '%s\n' "$MASTER_URL" | sed -e 's/[\/&]/\\&/g')
# Replace <api_key> and <master_url> placeholders in the JSON template file
API_KEY_VALUE="$FAVICON_GENERATOR_API_KEY"
sed -e "s|<api_key>|$API_KEY_VALUE|" -e "s|<master_url>|$MASTER_URL|" "$CONFIG_TEMPLATE_FILE" > "$CONFIG_FILE"
sed -e "s|<api_key>|$API_KEY_VALUE|" -e "s|<master_url>|$ESCAPED_MASTER_URL|" "$CONFIG_TEMPLATE_FILE" > "$CONFIG_FILE"
# Make the API POST request with JSON data from the config file
echo "⏳ Making request to API..."
......
......@@ -4,7 +4,7 @@ imagePullSecrets:
- name: regcred
config:
network:
id: 11155111
id: "11155111"
name: Blockscout
shortname: Blockscout
currency:
......
......@@ -10,3 +10,4 @@
| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` | Set to false if network doesn't have gas tracker | - | `true` | `false` | - | v1.25.0 | Replaced by NEXT_PUBLIC_GAS_TRACKER_ENABLED |
| NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL | `string` | Network governance token symbol | - | - | `GNO` | v1.12.0 | v1.29.0 | Replaced by NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL |
| NEXT_PUBLIC_SWAP_BUTTON_URL | `string` | Application ID in the marketplace or website URL | - | - | `uniswap` | v1.24.0 | v1.31.0 | Replaced by NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS |
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` | v1.0.x+ | v1.35.0 | Replaces by NEXT_PUBLIC_HOMEPAGE_STATS
\ No newline at end of file
......@@ -59,9 +59,10 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
- [Validators list](ENVS.md#validators-list)
- [Sentry error monitoring](ENVS.md#sentry-error-monitoring)
- [OpenTelemetry](ENVS.md#opentelemetry)
- [Swap button](ENVS.md#defi-dropdown)
- [DeFi dropdown](ENVS.md#defi-dropdown)
- [Multichain balance button](ENVS.md#multichain-balance-button)
- [Get gas button](ENVS.md#get-gas-button)
- [Save on gas with GasHawk](ENVS.md#save-on-gas-with-gashawk)
- [3rd party services configuration](ENVS.md#external-services-configuration)
&nbsp;
......@@ -93,7 +94,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
| NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS | `string` | Network currency decimals | - | `18` | `6` | v1.0.x+ |
| NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL | `string` | Network secondary coin symbol. | - | - | `GNO` | v1.29.0+ |
| NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES | `boolean` | Set to `true` for networks where users can pay transaction fees in either the native coin or ERC-20 tokens. | - | `false` | `true` | v1.33.0+ |
| NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE | `validation` | `mining` | Verification type in the network. Irrelevant for Arbitrum (verification type is always `posting`) and ZkEvm (verification type is always `sequencing`) L2s | - | `mining` | `validation` | v1.0.x+ |
| NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE | `validation` \| `mining` | Verification type in the network. Irrelevant for Arbitrum (verification type is always `posting`) and ZkEvm (verification type is always `sequencing`) L2s | - | `mining` | `validation` | v1.0.x+ |
| NEXT_PUBLIC_NETWORK_TOKEN_STANDARD_NAME | `string` | Name of the standard for creating tokens | - | `ERC` | `BEP` | v1.31.0+ |
| NEXT_PUBLIC_IS_TESTNET | `boolean`| Set to true if network is testnet | - | `false` | `true` | v1.0.x+ |
......@@ -118,9 +119,21 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'secondary_coin_price' \| 'market_cap' \| 'tvl'>` | List of charts displayed on the home page | - | - | `['daily_txs','coin_price','market_cap']` | v1.0.x+ |
| NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR | `string` | Text color of the hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | `white` | `\#DCFE76` | v1.0.x+ |
| NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND | `string` | Background css value for hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | `radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)` | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%)` \| `no-repeat bottom 20% right 0px/100% url(https://placekitten/1400/200)` | v1.1.0+ |
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` | v1.0.x+ |
| NEXT_PUBLIC_HOMEPAGE_STATS | `Array<'latest_batch' \| 'total_blocks' \| 'average_block_time' \| 'total_txs' \| 'latest_l1_state_batch' \| 'wallet_addresses' \| 'gas_tracker' \| 'btc_locked' \| 'current_epoch'>` | List of stats widgets displayed on the home page | - | For zkSync, zkEvm and Arbitrum rollups: `['latest_batch','average_block_time','total_txs','wallet_addresses','gas_tracker']`, for other cases: `['total_blocks','average_block_time','total_txs','wallet_addresses','gas_tracker']` | `['total_blocks','total_txs','wallet_addresses']` | v1.35.x+ |
| NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR | `string` | Text color of the hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead). **DEPRECATED** _Use `NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG` instead_ | - | `white` | `\#DCFE76` | v1.0.x+ |
| NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND | `string` | Background css value for hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead). **DEPRECATED** _Use `NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG` instead_ | - | `radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)` | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%)` \| `no-repeat bottom 20% right 0px/100% url(https://placekitten/1400/200)` | v1.1.0+ |
| NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG | `HeroBannerConfig`, see details [below](#hero-banner-configuration-properties) | Configuration of hero banner appearance. | - | - | See [below](#hero-banner-configuration-properties) | v1.35.0+ |
#### Hero banner configuration properties
_Note_ Here, all values are arrays of up to two strings. The first string represents the value for the light color mode, and the second string represents the value for the dark color mode. If the array contains only one string, it will be used for both color modes.
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| background | `[string, string]` | Banner background (could be a solid color, gradient or picture). The string should be a valid `background` CSS property value. | - | `['radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)']` | `['lightpink','no-repeat bottom 20% right 0px/100% url(https://placekitten/1400/200)']` |
| text_color | `[string, string]` | Banner text background. The string should be a valid `color` CSS property value. | - | `['white']` | `['lightpink','#DCFE76']` |
| border | `[string, string]` | Banner border. The string should be a valid `border` CSS property value. | - | - | `['1px solid yellow','4px dashed #DCFE76']` |
| button | `Partial<Record<'_default' \| '_hover' \| '_selected', {'background'?: [string, string]; 'text_color?:[string, string]'}>>` | The button on the banner. It has three possible states: `_default`, `_hover`, and `_selected`. The `_selected` state reflects when the user is logged in or their wallet is connected to the app. | - | - | `{'_default':{'background':['deeppink'],'text_color':['white']}}` |
&nbsp;
......@@ -286,6 +299,8 @@ Settings for meta tags, OG tags and SEO
| NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS | `boolean` | Set to `true` to hide indexing alert in the page footer about indexing block's internal transactions | - | `false` | `true` | v1.17.0+ |
| NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE | `string` | Used for displaying custom announcements or alerts in the header of the site. Could be a regular string or a HTML code. | - | - | `Hello world! 🤪` | v1.13.0+ |
| NEXT_PUBLIC_COLOR_THEME_DEFAULT | `'light' \| 'dim' \| 'midnight' \| 'dark'` | Preferred color theme of the app | - | - | `midnight` | v1.30.0+ |
| NEXT_PUBLIC_FONT_FAMILY_HEADING | `FontFamily`, see full description [below](#font-family-configuration-properties) | Special typeface to use in page headings (`<h1>`, `<h2>`, etc.) | - | - | `{'name':'Montserrat','url':'https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap'}` | v1.35.0+ |
| NEXT_PUBLIC_FONT_FAMILY_BODY | `FontFamily`, see full description [below](#font-family-configuration-properties) | Main typeface to use in page content elements. | - | - | `{'name':'Raleway','url':'https://fonts.googleapis.com/css2?family=Raleway:wght@400;500;600;700&display=swap'}` | v1.35.0+ |
| NEXT_PUBLIC_MAX_CONTENT_WIDTH_ENABLED | `boolean` | Set to `true` to restrict the page content width on extra-large screens. | - | `true` | `false` | v1.34.1+ |
#### Network explorer configuration properties
......@@ -307,6 +322,13 @@ Settings for meta tags, OG tags and SEO
| url | `string` | URL of the IDE with placeholders for contract hash (`{hash}`) and current domain (`{domain}`) | Required | - | `https://remix.blockscout.com/?address={hash}&blockscout={domain}` |
| icon_url | `string` | URL of the IDE icon | Required | - | `https://example.com/icon.svg` |
#### Font family configuration properties
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| name | `string` | Font family name; used to define the `font-family` CSS property. | Required | - | `Montserrat` |
| url | `string` | URL for external font. Ensure the font supports the following weights: 400, 500, 600, and 700. | Required | - | `https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap` |
&nbsp;
## App features
......@@ -409,7 +431,7 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi
| NEXT_PUBLIC_ROLLUP_L1_BASE_URL | `string` | Blockscout base URL for L1 network | Required | - | `'http://eth-goerli.blockscout.com'` | v1.24.0+ |
| NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL | `string` | URL for L2 -> L1 withdrawals (Optimistic stack only) | Required for `optimistic` rollups | - | `https://app.optimism.io/bridge/withdraw` | v1.24.0+ |
| NEXT_PUBLIC_FAULT_PROOF_ENABLED | `boolean` | Set to `true` for chains with fault proof system enabled (Optimistic stack only) | - | - | `true` | v1.31.0+ |
| NEXT_PUBLIC_HAS_MUD_FRAMEWORK | `boolean` | Set to `true` for instances that use MUD framework (Optimistic stack only) | - | - | `true` | - |
| NEXT_PUBLIC_HAS_MUD_FRAMEWORK | `boolean` | Set to `true` for instances that use MUD framework (Optimistic stack only) | - | - | `true` | v1.33.0+ |
&nbsp;
......@@ -735,6 +757,16 @@ If the feature is enabled, a Get gas button will be displayed in the top bar, wh
&nbsp;
### Save on gas with GasHawk
The feature enables a "Save with GasHawk" button next to the "Gas used" value on the address page.
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_SAVE_ON_GAS_ENABLED | `boolean` | Set to "true" to enable the feature | - | - | `true` | v1.35.0+ |
&nbsp;
## External services configuration
### Google ReCaptcha
......
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="10" r="10" fill="url(#a)"/>
<g clip-path="url(#b)" fill="#fff" fill-opacity=".95">
<path d="M15.763 12.826c.119-.243.178-.365.158-.462a.292.292 0 0 0-.148-.199c-.088-.047-.244-.02-.554.033a6.409 6.409 0 0 1-5.632-1.786 6.409 6.409 0 0 1-1.785-5.631c.053-.31.08-.466.033-.554a.292.292 0 0 0-.199-.149c-.098-.02-.219.04-.462.159a6.417 6.417 0 1 0 8.589 8.589Z"/>
<path d="M15.9 10.817c.152-.054.229-.082.31-.152a.686.686 0 0 0 .163-.228c.04-.1.04-.183.043-.35a6.398 6.398 0 0 0-1.879-4.624 6.398 6.398 0 0 0-4.624-1.88c-.167.003-.25.004-.35.044a.685.685 0 0 0-.229.163c-.07.081-.097.158-.151.31a5.25 5.25 0 0 0 6.717 6.717Z"/>
</g>
<defs>
<linearGradient id="a" x1="17.5" y1="2" x2="0" y2="20" gradientUnits="userSpaceOnUse">
<stop stop-color="#196E41"/>
<stop offset="1" stop-color="#092E1B"/>
</linearGradient>
<clipPath id="b">
<rect x="3" y="3" width="14" height="14" rx="7" fill="#fff"/>
</clipPath>
</defs>
</svg>
import { getAddress } from 'viem';
export default function getCheckedSummedAddress(address: string): string {
try {
return getAddress(address);
} catch (error) {
return address;
}
}
......@@ -31,7 +31,6 @@ import type {
AddressNFTsResponse,
AddressCollectionsResponse,
AddressNFTTokensFilter,
AddressCoinBalanceHistoryChartOld,
AddressMudTables,
AddressMudTablesFilter,
AddressMudRecords,
......@@ -39,7 +38,7 @@ import type {
AddressMudRecordsSorting,
AddressMudRecord,
} from 'types/api/address';
import type { AddressesResponse } from 'types/api/addresses';
import type { AddressesResponse, AddressesMetadataSearchResult, AddressesMetadataSearchFilters } from 'types/api/addresses';
import type { AddressMetadataInfo, PublicTagTypesResponse } from 'types/api/addressMetadata';
import type {
ArbitrumL2MessagesResponse,
......@@ -62,7 +61,7 @@ import type {
BlockEpochElectionRewardDetailsResponse,
} from 'types/api/block';
import type { ChartMarketResponse, ChartSecondaryCoinPriceResponse, ChartTransactionResponse } from 'types/api/charts';
import type { BackendVersionConfig } from 'types/api/configs';
import type { BackendVersionConfig, CsvExportConfig } from 'types/api/configs';
import type {
SmartContract,
SmartContractVerificationConfigRaw,
......@@ -86,6 +85,9 @@ import type {
OptimisticL2TxnBatchesResponse,
OptimisticL2WithdrawalsResponse,
OptimisticL2DisputeGamesResponse,
OptimismL2TxnBatch,
OptimismL2BatchTxs,
OptimismL2BatchBlocks,
} from 'types/api/optimisticL2';
import type { RawTracesResponse } from 'types/api/rawTrace';
import type { SearchRedirectResult, SearchResult, SearchResultFilters, SearchResultItem } from 'types/api/search';
......@@ -159,26 +161,26 @@ export const RESOURCES = {
path: '/api/account/v2/email/resend',
},
custom_abi: {
path: '/api/account/v2/user/custom_abis/:id?',
path: '/api/account/v2/user/custom_abis{/:id}',
pathParams: [ 'id' as const ],
},
watchlist: {
path: '/api/account/v2/user/watchlist/:id?',
path: '/api/account/v2/user/watchlist{/:id}',
pathParams: [ 'id' as const ],
filterFields: [ ],
},
private_tags_address: {
path: '/api/account/v2/user/tags/address/:id?',
path: '/api/account/v2/user/tags/address{/:id}',
pathParams: [ 'id' as const ],
filterFields: [ ],
},
private_tags_tx: {
path: '/api/account/v2/user/tags/transaction/:id?',
path: '/api/account/v2/user/tags/transaction{/:id}',
pathParams: [ 'id' as const ],
filterFields: [ ],
},
api_keys: {
path: '/api/account/v2/user/api_keys/:id?',
path: '/api/account/v2/user/api_keys{/:id}',
pathParams: [ 'id' as const ],
},
......@@ -208,7 +210,7 @@ export const RESOURCES = {
},
token_info_applications: {
path: '/api/v1/chains/:chainId/token-info-submissions/:id?',
path: '/api/v1/chains/:chainId/token-info-submissions{/:id}',
pathParams: [ 'chainId' as const, 'id' as const ],
endpoint: getFeaturePayload(config.features.addressVerification)?.api.endpoint,
basePath: getFeaturePayload(config.features.addressVerification)?.api.basePath,
......@@ -419,6 +421,10 @@ export const RESOURCES = {
path: '/api/v2/addresses/',
filterFields: [ ],
},
addresses_metadata_search: {
path: '/api/v2/proxy/metadata/addresses',
filterFields: [ 'slug' as const, 'tag_type' as const ],
},
// ADDRESS
address: {
......@@ -679,12 +685,29 @@ export const RESOURCES = {
},
optimistic_l2_txn_batches: {
path: '/api/v2/optimism/txn-batches',
path: '/api/v2/optimism/batches',
filterFields: [],
},
optimistic_l2_txn_batches_count: {
path: '/api/v2/optimism/txn-batches/count',
path: '/api/v2/optimism/batches/count',
},
optimistic_l2_txn_batch: {
path: '/api/v2/optimism/batches/:number',
pathParams: [ 'number' as const ],
},
optimistic_l2_txn_batch_txs: {
path: '/api/v2/transactions/optimism-batch/:number',
pathParams: [ 'number' as const ],
filterFields: [],
},
optimistic_l2_txn_batch_blocks: {
path: '/api/v2/blocks/optimism-batch/:number',
pathParams: [ 'number' as const ],
filterFields: [],
},
optimistic_l2_dispute_games: {
......@@ -894,6 +917,9 @@ export const RESOURCES = {
config_backend_version: {
path: '/api/v2/config/backend-version',
},
config_csv_export: {
path: '/api/v2/config/csv-export',
},
// CSV EXPORT
csv_export_token_holders: {
......@@ -960,7 +986,7 @@ export type ResourceErrorAccount<T> = ResourceError<{ errors: T }>
export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_rewards' |
'txs_validated' | 'txs_pending' | 'txs_with_blobs' | 'txs_watchlist' | 'txs_execution_node' |
'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' | 'tx_state_changes' | 'tx_blobs' |
'addresses' |
'addresses' | 'addresses_metadata_search' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
'search' |
'address_logs' | 'address_tokens' | 'address_nfts' | 'address_collections' |
......@@ -968,7 +994,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_reward
'token_instance_transfers' | 'token_instance_holders' |
'verified_contracts' |
'optimistic_l2_output_roots' | 'optimistic_l2_withdrawals' | 'optimistic_l2_txn_batches' | 'optimistic_l2_deposits' |
'optimistic_l2_dispute_games' |
'optimistic_l2_dispute_games' | 'optimistic_l2_txn_batch_txs' | 'optimistic_l2_txn_batch_blocks' |
'mud_worlds'| 'address_mud_tables' | 'address_mud_records' |
'shibarium_deposits' | 'shibarium_withdrawals' |
'arbitrum_l2_messages' | 'arbitrum_l2_txn_batches' | 'arbitrum_l2_txn_batch_txs' | 'arbitrum_l2_txn_batch_blocks' |
......@@ -1034,6 +1060,7 @@ Q extends 'tx_state_changes' ? TxStateChanges :
Q extends 'tx_blobs' ? TxBlobs :
Q extends 'tx_interpretation' ? TxInterpretationResponse :
Q extends 'addresses' ? AddressesResponse :
Q extends 'addresses_metadata_search' ? AddressesMetadataSearchResult :
Q extends 'address' ? Address :
Q extends 'address_counters' ? AddressCounters :
Q extends 'address_tabs_counters' ? AddressTabsCounters :
......@@ -1042,7 +1069,7 @@ Q extends 'address_internal_txs' ? AddressInternalTxsResponse :
Q extends 'address_token_transfers' ? AddressTokenTransferResponse :
Q extends 'address_blocks_validated' ? AddressBlocksValidatedResponse :
Q extends 'address_coin_balance' ? AddressCoinBalanceHistoryResponse :
Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChartOld | AddressCoinBalanceHistoryChart :
Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart :
Q extends 'address_logs' ? LogsResponseAddress :
Q extends 'address_tokens' ? AddressTokensResponse :
Q extends 'address_nfts' ? AddressNFTsResponse :
......@@ -1073,11 +1100,14 @@ Q extends 'optimistic_l2_output_roots' ? OptimisticL2OutputRootsResponse :
Q extends 'optimistic_l2_withdrawals' ? OptimisticL2WithdrawalsResponse :
Q extends 'optimistic_l2_deposits' ? OptimisticL2DepositsResponse :
Q extends 'optimistic_l2_txn_batches' ? OptimisticL2TxnBatchesResponse :
Q extends 'optimistic_l2_txn_batches_count' ? number :
Q extends 'optimistic_l2_txn_batch' ? OptimismL2TxnBatch :
Q extends 'optimistic_l2_txn_batch_txs' ? OptimismL2BatchTxs :
Q extends 'optimistic_l2_txn_batch_blocks' ? OptimismL2BatchBlocks :
Q extends 'optimistic_l2_dispute_games' ? OptimisticL2DisputeGamesResponse :
Q extends 'optimistic_l2_output_roots_count' ? number :
Q extends 'optimistic_l2_withdrawals_count' ? number :
Q extends 'optimistic_l2_deposits_count' ? number :
Q extends 'optimistic_l2_txn_batches_count' ? number :
Q extends 'optimistic_l2_dispute_games_count' ? number :
never;
// !!! IMPORTANT !!!
......@@ -1087,6 +1117,7 @@ never;
/* eslint-disable @typescript-eslint/indent */
export type ResourcePayloadB<Q extends ResourceName> =
Q extends 'config_backend_version' ? BackendVersionConfig :
Q extends 'config_csv_export' ? CsvExportConfig :
Q extends 'address_metadata_info' ? AddressMetadataInfo :
Q extends 'address_metadata_tag_types' ? PublicTagTypesResponse :
Q extends 'blob' ? Blob :
......@@ -1156,6 +1187,7 @@ Q extends 'txs_with_blobs' ? TTxsWithBlobsFilters :
Q extends 'tx_token_transfers' ? TokenTransferFilters :
Q extends 'token_transfers' ? TokenTransferFilters :
Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters :
Q extends 'addresses_metadata_search' ? AddressesMetadataSearchFilters :
Q extends 'address_token_transfers' ? AddressTokenTransferFilters :
Q extends 'address_tokens' ? AddressTokensFilter :
Q extends 'address_nfts' ? AddressNFTTokensFilter :
......
......@@ -12,6 +12,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/block/countdown': 'Regular page',
'/block/countdown/[height]': 'Regular page',
'/accounts': 'Root page',
'/accounts/label/[slug]': 'Root page',
'/address/[hash]': 'Regular page',
'/verified-contracts': 'Root page',
'/contract-verification': 'Root page',
......
......@@ -16,6 +16,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/block/countdown': DEFAULT_TEMPLATE,
'/block/countdown/[height]': DEFAULT_TEMPLATE,
'/accounts': DEFAULT_TEMPLATE,
'/accounts/label/[slug]': DEFAULT_TEMPLATE,
'/address/[hash]': 'View the account balance, transactions, and other data for %hash% on the %network_title%',
'/verified-contracts': DEFAULT_TEMPLATE,
'/contract-verification': DEFAULT_TEMPLATE,
......
......@@ -12,6 +12,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/block/countdown': '%network_name% block countdown index',
'/block/countdown/[height]': '%network_name% block %height% countdown',
'/accounts': '%network_name% top accounts',
'/accounts/label/[slug]': '%network_name% addresses search by label',
'/address/[hash]': '%network_name% address details for %hash%',
'/verified-contracts': 'Verified %network_name% contracts lookup - %network_name% explorer',
'/contract-verification': '%network_name% verify contract',
......
......@@ -10,6 +10,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/block/countdown': 'Block countdown search',
'/block/countdown/[height]': 'Block countdown',
'/accounts': 'Top accounts',
'/accounts/label/[slug]': 'Addresses search by label',
'/address/[hash]': 'Address details',
'/verified-contracts': 'Verified contracts',
'/contract-verification': 'Contract verification',
......
......@@ -35,7 +35,10 @@ const wagmiConfig = (() => {
url: config.app.baseUrl,
icons: [ config.UI.navigation.icon.default ].filter(Boolean),
},
enableEmail: true,
auth: {
email: true,
socials: [],
},
ssr: true,
batch: { multicall: { wait: 100 } },
});
......
......@@ -43,7 +43,7 @@ export const verified: SmartContract = {
file_path: '',
additional_sources: [],
verified_twin_address_hash: null,
minimal_proxy_address_hash: null,
proxy_type: null,
};
export const certified: SmartContract = {
......@@ -85,7 +85,7 @@ export const withProxyAddress: SmartContract = {
...verified,
is_verified: false,
verified_twin_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8',
minimal_proxy_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8',
proxy_type: 'eip1967',
};
export const selfDestructed: SmartContract = {
......@@ -133,7 +133,7 @@ export const nonVerified: SmartContract = {
additional_sources: [],
external_libraries: null,
verified_twin_address_hash: null,
minimal_proxy_address_hash: null,
proxy_type: null,
language: null,
license_type: null,
};
export const txnBatchesData = {
items: [
{
l1_tx_hashes: [
'0x5bc94d02b65743dfaa9e10a2d6e175aff2a05cce2128c8eaf848bd84ab9325c5',
'0x92a51bc623111dbb91f243e3452e60fab6f090710357f9d9b75ac8a0f67dfd9d',
],
l1_timestamp: '2023-02-24T10:16:12.000000Z',
l2_block_number: 5902836,
tx_count: 0,
},
{
l1_tx_hashes: [
'0xc45f846ee28ce9ba116ce2d378d3dd00b55d324b833b3ecd4241c919c572c4aa',
],
l1_timestamp: '2023-02-24T10:16:00.000000Z',
l2_block_number: 5902835,
tx_count: 0,
},
{
l1_tx_hashes: [
'0x48139721f792d3a68c3781b4cf50e66e8fc7dbb38adff778e09066ea5be9adb8',
],
l1_timestamp: '2023-02-24T10:16:00.000000Z',
l2_block_number: 5902834,
tx_count: 0,
},
],
next_page_params: {
block_number: 5902834,
items_count: 50,
},
};
import type {
OptimismL2TxnBatchTypeCallData,
OptimismL2TxnBatchTypeCelestia,
OptimismL2TxnBatchTypeEip4844,
OptimisticL2TxnBatchesResponse,
} from 'types/api/optimisticL2';
export const txnBatchesData: OptimisticL2TxnBatchesResponse = {
items: [
{
batch_data_container: 'in_blob4844',
internal_id: 260998,
l1_timestamp: '2022-11-10T11:29:11.000000Z',
l1_tx_hashes: [
'0x9553351f6bd1577f4e782738c087be08697fb11f3b91745138d71ba166d62c3b',
],
l2_block_end: 124882074,
l2_block_start: 124881833,
tx_count: 4011,
},
{
batch_data_container: 'in_calldata',
internal_id: 260997,
l1_timestamp: '2022-11-03T11:20:59.000000Z',
l1_tx_hashes: [
'0x80f5fba70d5685bc2b70df836942e892b24afa7bba289a2fac0ca8f4d554cc72',
],
l2_block_end: 124881832,
l2_block_start: 124881613,
tx_count: 4206,
},
{
internal_id: 260996,
l1_timestamp: '2024-09-03T11:14:23.000000Z',
l1_tx_hashes: [
'0x39f4c46cae57bae936acb9159e367794f41f021ed3788adb80ad93830edb5f22',
],
l2_block_end: 124881612,
l2_block_start: 124881380,
tx_count: 4490,
},
],
next_page_params: {
id: 5902834,
items_count: 50,
},
};
export const txnBatchTypeCallData: OptimismL2TxnBatchTypeCallData = {
batch_data_container: 'in_calldata',
internal_id: 309123,
l1_timestamp: '2022-08-10T10:30:24.000000Z',
l1_tx_hashes: [
'0x478c45f182631ae6f7249d40f31fdac36f41d88caa2e373fba35340a7345ca67',
],
l2_block_end: 10146784,
l2_block_start: 10145379,
tx_count: 1608,
};
export const txnBatchTypeCelestia: OptimismL2TxnBatchTypeCelestia = {
batch_data_container: 'in_celestia',
blobs: [
{
commitment: '0x39c18c21c6b127d58809b8d3b5931472421f9b51532959442f53038f10b78f2a',
height: 2584868,
l1_timestamp: '2024-08-28T16:51:12.000000Z',
l1_transaction_hash: '0x2bb0b96a8ba0f063a243ac3dee0b2f2d87edb2ba9ef44bfcbc8ed191af1c4c24',
namespace: '0x00000000000000000000000000000000000000000008e5f679bf7116cb',
},
],
internal_id: 309667,
l1_timestamp: '2022-08-28T16:51:12.000000Z',
l1_tx_hashes: [
'0x2bb0b96a8ba0f063a243ac3dee0b2f2d87edb2ba9ef44bfcbc8ed191af1c4c24',
],
l2_block_end: 10935879,
l2_block_start: 10934514,
tx_count: 1574,
};
export const txnBatchTypeEip4844: OptimismL2TxnBatchTypeEip4844 = {
batch_data_container: 'in_blob4844',
blobs: [
{
hash: '0x012a4f0c6db6bce9d3d357b2bf847764320bcb0107ab318f3a532f637bc60dfe',
l1_timestamp: '2022-08-23T03:59:12.000000Z',
l1_transaction_hash: '0x3870f136497e5501dc20d0974daf379c8636c958794d59a9c90d4f8a9f0ed20a',
},
{
hash: '0x01d1097cce23229931afbc2fd1cf0d707da26df7b39cef1c542276ae718de4f6',
l1_timestamp: '2022-08-23T03:59:12.000000Z',
l1_transaction_hash: '0x3870f136497e5501dc20d0974daf379c8636c958794d59a9c90d4f8a9f0ed20a',
},
],
internal_id: 2538459,
l1_timestamp: '2022-08-23T03:59:12.000000Z',
l1_tx_hashes: [
'0x3870f136497e5501dc20d0974daf379c8636c958794d59a9c90d4f8a9f0ed20a',
],
l2_block_end: 16291502,
l2_block_start: 16291373,
tx_count: 704,
};
......@@ -72,6 +72,7 @@ export const withoutGasInfo: HomeStats = {
export const withSecondaryCoin: HomeStats = {
...base,
secondary_coin_price: '3.398',
secondary_coin_image: 'http://localhost:3100/secondary_utia.jpg',
};
export const noChartData: HomeStats = {
......
......@@ -391,3 +391,8 @@ export const withRecipientNameTag = {
...withRecipientEns,
to: addressMock.withNameTag,
};
export const withRecipientContract = {
...withRecipientEns,
to: addressMock.contract,
};
......@@ -6,6 +6,7 @@ function generateCspPolicy() {
descriptors.app(),
descriptors.ad(),
descriptors.cloudFlare(),
descriptors.gasHawk(),
descriptors.googleAnalytics(),
descriptors.googleFonts(),
descriptors.googleReCaptcha(),
......
......@@ -30,6 +30,18 @@ const getCspReportUrl = () => {
}
};
const externalFontsDomains = (() => {
try {
return [
config.UI.fonts.heading?.url,
config.UI.fonts.body?.url,
]
.filter(Boolean)
.map((urlString) => new URL(urlString))
.map((url) => url.hostname);
} catch (error) {}
})();
export function app(): CspDev.DirectiveDescriptor {
return {
'default-src': [
......@@ -72,8 +84,9 @@ export function app(): CspDev.DirectiveDescriptor {
// https://github.com/vercel/next.js/issues/14221#issuecomment-657258278
config.app.isDev ? KEY_WORDS.UNSAFE_EVAL : '',
// hash of ColorModeScript
// hash of ColorModeScript: system + dark
'\'sha256-e7MRMmTzLsLQvIy1iizO1lXf7VWYoQ6ysj5fuUzvRwE=\'',
'\'sha256-9A7qFFHmxdWjZMQmfzYD2XWaNHLu1ZmQB0Ds4Go764k=\'',
],
'style-src': [
......@@ -115,6 +128,7 @@ export function app(): CspDev.DirectiveDescriptor {
'font-src': [
KEY_WORDS.DATA,
...MAIN_DOMAINS,
...(externalFontsDomains || []),
],
'object-src': [
......
import type CspDev from 'csp-dev';
import config from 'configs/app';
const feature = config.features.saveOnGas;
export function gasHawk(): CspDev.DirectiveDescriptor {
if (!feature.isEnabled) {
return {};
}
const apiOrigin = (() => {
try {
const url = new URL(feature.apiUrlTemplate);
return url.origin;
} catch (error) {
return '';
}
})();
if (!apiOrigin) {
return {};
}
return {
'connect-src': [
apiOrigin,
],
};
}
export { ad } from './ad';
export { app } from './app';
export { cloudFlare } from './cloudFlare';
export { gasHawk } from './gasHawk';
export { googleAnalytics } from './googleAnalytics';
export { googleFonts } from './googleFonts';
export { googleReCaptcha } from './googleReCaptcha';
......
......@@ -14,6 +14,7 @@ export function walletConnect(): CspDev.DirectiveDescriptor {
'*.web3modal.com',
'*.web3modal.org',
'*.walletconnect.com',
'*.walletconnect.org',
'wss://relay.walletconnect.com',
'wss://www.walletlink.org',
],
......
......@@ -112,7 +112,7 @@ export const optimisticRollup: GetServerSideProps<Props> = async(context) => {
return base(context);
};
const BATCH_ROLLUP_TYPES: Array<RollupType> = [ 'zkEvm', 'zkSync', 'arbitrum' ];
const BATCH_ROLLUP_TYPES: Array<RollupType> = [ 'zkEvm', 'zkSync', 'arbitrum', 'optimistic' ];
export const batch: GetServerSideProps<Props> = async(context) => {
if (!(rollupFeature.isEnabled && BATCH_ROLLUP_TYPES.includes(rollupFeature.type))) {
return {
......@@ -204,6 +204,16 @@ export const accounts: GetServerSideProps<Props> = async(context) => {
return base(context);
};
export const accountsLabelSearch: GetServerSideProps<Props> = async(context) => {
if (!config.features.addressMetadata.isEnabled || !context.query.tagType) {
return {
notFound: true,
};
}
return base(context);
};
export const userOps: GetServerSideProps<Props> = async(context) => {
if (!config.features.userOps.isEnabled) {
return {
......
......@@ -13,6 +13,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/account/verified-addresses">
| StaticRoute<"/account/watchlist">
| StaticRoute<"/accounts">
| DynamicRoute<"/accounts/label/[slug]", { "slug": string }>
| DynamicRoute<"/address/[hash]/contract-verification", { "hash": string }>
| DynamicRoute<"/address/[hash]", { "hash": string }>
| StaticRoute<"/api/config">
......
......@@ -12,6 +12,7 @@
"dev:preset": "./tools/scripts/dev.preset.sh",
"dev:preset:sync": "tsc -p ./tools/preset-sync/tsconfig.json && node ./tools/preset-sync/index.js",
"build": "next build",
"build:next": "./deploy/scripts/download_assets.sh ./public/assets/configs && yarn svg:build-sprite && ./deploy/scripts/make_envs_script.sh && next build",
"build:docker": "docker build --build-arg GIT_COMMIT_SHA=$(git rev-parse --short HEAD) --build-arg GIT_TAG=$(git describe --tags --abbrev=0) -t blockscout-frontend:local ./",
"start": "next start",
"start:docker:local": "docker run -p 3000:3000 --env-file .env.local blockscout-frontend:local",
......@@ -49,22 +50,22 @@
"@metamask/providers": "^10.2.1",
"@monaco-editor/react": "^4.4.6",
"@next/bundle-analyzer": "14.2.3",
"@opentelemetry/auto-instrumentations-node": "^0.39.4",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.45.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.45.0",
"@opentelemetry/resources": "^1.18.0",
"@opentelemetry/sdk-node": "^0.45.0",
"@opentelemetry/sdk-trace-node": "^1.18.0",
"@opentelemetry/semantic-conventions": "^1.18.0",
"@opentelemetry/auto-instrumentations-node": "0.43.0",
"@opentelemetry/exporter-metrics-otlp-proto": "0.49.1",
"@opentelemetry/exporter-trace-otlp-http": "0.49.1",
"@opentelemetry/resources": "1.22.0",
"@opentelemetry/sdk-node": "0.49.1",
"@opentelemetry/sdk-trace-node": "1.22.0",
"@opentelemetry/semantic-conventions": "1.22.0",
"@sentry/cli": "^2.21.2",
"@sentry/react": "7.24.0",
"@sentry/tracing": "7.24.0",
"@slise/embed-react": "^2.2.0",
"@tanstack/react-query": "^5.4.3",
"@tanstack/react-query-devtools": "^5.4.3",
"@tanstack/react-query": "5.55.4",
"@tanstack/react-query-devtools": "5.55.4",
"@types/papaparse": "^5.3.5",
"@types/react-scroll": "^1.8.4",
"@web3modal/wagmi": "4.2.1",
"@web3modal/wagmi": "5.1.7",
"airtable": "^0.12.2",
"bignumber.js": "^9.1.0",
"blo": "^1.1.1",
......@@ -86,11 +87,11 @@
"magic-bytes.js": "1.8.0",
"mixpanel-browser": "^2.47.0",
"monaco-editor": "^0.34.1",
"next": "14.2.3",
"next": "14.2.9",
"nextjs-routes": "^1.0.8",
"node-fetch": "^3.2.9",
"papaparse": "^5.3.2",
"path-to-regexp": "^6.2.1",
"path-to-regexp": "8.1.0",
"phoenix": "^1.6.15",
"pino-http": "^8.2.1",
"pino-pretty": "^9.1.1",
......@@ -109,8 +110,8 @@
"swagger-ui-react": "^5.9.0",
"use-font-face-observer": "^1.2.1",
"valibot": "0.38.0",
"viem": "2.10.9",
"wagmi": "2.9.2",
"viem": "2.21.5",
"wagmi": "2.12.10",
"xss": "^1.0.14"
},
"devDependencies": {
......
......@@ -6,6 +6,7 @@ import React from 'react';
import logRequestFromBot from 'nextjs/utils/logRequestFromBot';
import * as serverTiming from 'nextjs/utils/serverTiming';
import config from 'configs/app';
import theme from 'theme/theme';
import * as svgSprite from 'ui/shared/IconSvg';
......@@ -35,11 +36,11 @@ class MyDocument extends Document {
<Head>
{ /* FONTS */ }
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
href={ config.UI.fonts.heading?.url ?? 'https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap' }
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
href={ config.UI.fonts.body?.url ?? 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap' }
rel="stylesheet"
/>
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
const AccountsLabelSearch = dynamic(() => import('ui/pages/AccountsLabelSearch'), { ssr: false });
const Page: NextPage = () => {
return (
<PageNextJs pathname="/accounts/label/[slug]">
<AccountsLabelSearch/>
</PageNextJs>
);
};
export default Page;
export { accountsLabelSearch as getServerSideProps } from 'nextjs/getServerSideProps';
......@@ -17,6 +17,8 @@ const Batch = dynamic(() => {
switch (rollupFeature.type) {
case 'arbitrum':
return import('ui/pages/ArbitrumL2TxnBatch');
case 'optimistic':
return import('ui/pages/OptimisticL2TxnBatch');
case 'zkEvm':
return import('ui/pages/ZkEvmL2TxnBatch');
case 'zkSync':
......
......@@ -24,6 +24,7 @@
| "block_slim"
| "block"
| "brands/blockscout"
| "brands/celenium"
| "brands/safe"
| "brands/solidity_scan"
| "burger"
......
<svg viewBox="0 0 15 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.4 12.07c-.164-.152-.302-.312-.456-.454-.703-.668-.168-1.364-.168-1.364s-.844.132-2.113-.128a4.273 4.273 0 0 0-4.44 1.175 2.618 2.618 0 0 0-2.084 2.334 4.722 4.722 0 0 0-.002.5 4.64 4.64 0 0 0 .024.209s.023.132.038.196l.025.095v.003l.028.094.014.04a2.6 2.6 0 0 1 .286-.97.38.38 0 0 1 .346-.196 7 7 0 0 1 1.352.218 4.28 4.28 0 0 1 2.343 2.9h.001a4.683 4.683 0 0 1-.152 2.937A7.22 7.22 0 0 0 14.4 12.07Zm-4.82-.62a.644.644 0 0 1-1.074-.138c.118-.184.239-.278.36-.328.473-.212.807-.22.807-.22.1.221.073.489-.094.686Z" fill="url(#a)"/>
<path d="M9.594 16.721a4.743 4.743 0 0 1 .107.5l1.551 1.474.088.021a7.247 7.247 0 0 0 2.415-2.86c-3.78-3.424-7.12-2.33-7.856-2.252.464.029.916.103 1.352.218a4.28 4.28 0 0 1 2.343 2.9Z" fill="url(#b)"/>
<path d="M7.605.209A.46.46 0 0 0 7.22 0a.459.459 0 0 0-.387.209L1.381 8.544A7.185 7.185 0 0 0 0 12.782c-.004 3.86 3.064 7.044 6.889 7.218.562-.986.571-2.159.565-2.426a4.866 4.866 0 0 1-.236.006h-.033a4.757 4.757 0 0 1-3.361-1.422 4.755 4.755 0 0 1-1.396-3.374 4.751 4.751 0 0 1 .915-2.81l3.875-5.92L9.53 7.69a6.433 6.433 0 0 1 3.095.254L7.605.209Z" fill="url(#c)"/>
<path d="M12.256 17.96c-.686-1.602-1.57-2.637-2.252-3.249-1.07-.958-1.997-1.25-2.875-1.24-.877.01-1.361.15-1.361.15a.408.408 0 0 1 .13-.017c.465.029.917.102 1.352.217a4.28 4.28 0 0 1 2.344 2.9 4.683 4.683 0 0 1-.152 2.937 7.22 7.22 0 0 0 2.814-1.7v.001Z" fill="url(#d)"/>
<defs>
<linearGradient id="a" x1="7.369" y1="12.845" x2="12.113" y2="14.707" gradientUnits="userSpaceOnUse">
<stop stop-color="#FDF0C2"/>
<stop offset="1" stop-color="#F6C789"/>
</linearGradient>
<linearGradient id="b" x1="9.827" y1="13.377" x2="9.837" y2="18.83" gradientUnits="userSpaceOnUse">
<stop stop-color="#FDF1C3"/>
<stop offset=".625" stop-color="#F5C57D"/>
</linearGradient>
<linearGradient id="c" x1="2.388" y1="15.63" x2="11.492" y2="4.616" gradientUnits="userSpaceOnUse">
<stop stop-color="#F5C060"/>
<stop offset="1" stop-color="#F0994D"/>
</linearGradient>
<linearGradient id="d" x1="9.012" y1="13.47" x2="9.009" y2="15.153" gradientUnits="userSpaceOnUse">
<stop stop-color="#F5C780"/>
<stop offset="1" stop-color="#FADFA1"/>
</linearGradient>
</defs>
</svg>
import type {
OptimismL2TxnBatch,
OptimisticL2DepositsItem,
OptimisticL2DisputeGamesItem,
OptimisticL2OutputRootsItem,
......@@ -30,14 +31,29 @@ export const L2_WITHDRAWAL_ITEM: OptimisticL2WithdrawalsItem = {
};
export const L2_TXN_BATCHES_ITEM: OptimisticL2TxnBatchesItem = {
internal_id: 260991,
batch_data_container: 'in_blob4844',
l1_timestamp: '2023-06-01T14:46:48.000000Z',
l1_tx_hashes: [
TX_HASH,
],
l2_block_number: 5218590,
l2_block_start: 5218590,
l2_block_end: 5218777,
tx_count: 9,
};
export const L2_TXN_BATCH: OptimismL2TxnBatch = {
...L2_TXN_BATCHES_ITEM,
batch_data_container: 'in_blob4844',
blobs: [
{
hash: '0x01fb41e1ae9f827e13abb0ee94be2ee574a23ac31426cea630ddd18af854bc85',
l1_timestamp: '2024-09-03T13:26:23.000000Z',
l1_transaction_hash: '0xd25ee571f1701690615099b208a9431d8611d0130dc342bead6d9edc291f04b9',
},
],
};
export const L2_OUTPUT_ROOTS_ITEM: OptimisticL2OutputRootsItem = {
l1_block_number: 9103684,
l1_timestamp: '2023-06-01T15:26:12.000000Z',
......
......@@ -6,12 +6,12 @@ import { TX_HASH } from './tx';
export const PRIVATE_TAG_ADDRESS: AddressTag = {
address: ADDRESS_PARAMS,
address_hash: ADDRESS_HASH,
id: '4',
id: 4,
name: 'placeholder',
};
export const PRIVATE_TAG_TX: TransactionTag = {
id: '1',
id: 1,
name: 'placeholder',
transaction_hash: TX_HASH,
};
......@@ -21,7 +21,7 @@ export const WATCH_LIST_ITEM_WITH_TOKEN_INFO: WatchlistAddress = {
address_balance: '7072643779453701031672',
address_hash: ADDRESS_HASH,
exchange_rate: '0.00099052',
id: '18',
id: 18,
name: 'placeholder',
notification_methods: {
email: false,
......@@ -68,7 +68,7 @@ export const CUSTOM_ABI: CustomAbi = {
],
contract_address: ADDRESS_PARAMS,
contract_address_hash: ADDRESS_HASH,
id: '1',
id: 1,
name: 'placeholder',
};
......
......@@ -27,7 +27,7 @@ export const ADDRESS_INFO: Address = {
has_tokens: false,
has_validated_blocks: false,
hash: ADDRESS_HASH,
implementations: [ { address: ADDRESS_HASH, name: 'Proxy' } ],
implementations: [ { address: ADDRESS_HASH, name: 'Transparent Upgradable Proxy' } ],
is_contract: true,
is_verified: true,
name: 'ChainLink Token (goerli)',
......
......@@ -13,6 +13,8 @@ test.use({ viewport: { width: 150, height: 350 } });
{ variant: 'ghost', withDarkMode: true, states: [ 'default', 'hovered', 'active' ] },
{ variant: 'subtle', states: [ 'default', 'hovered' ] },
{ variant: 'subtle', colorScheme: 'gray', states: [ 'default', 'hovered' ], withDarkMode: true },
{ variant: 'hero', states: [ 'default', 'hovered' ], withDarkMode: true },
{ variant: 'header', states: [ 'default', 'hovered', 'selected' ], withDarkMode: true },
].forEach(({ variant, colorScheme, withDarkMode, states }) => {
test.describe(`variant ${ variant }${ colorScheme ? ` with ${ colorScheme } color scheme` : '' }${ withDarkMode ? ' +@dark-mode' : '' }`, () => {
test('', async({ render }) => {
......
......@@ -2,6 +2,8 @@ import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system';
import { mode } from '@chakra-ui/theme-tools';
import { runIfFn } from '@chakra-ui/utils';
import config from 'configs/app';
const variantSolid = defineStyle((props) => {
const { colorScheme: c } = props;
......@@ -150,12 +152,76 @@ const variantSubtle = defineStyle((props) => {
};
});
// for buttons in the hero banner
const variantHero = defineStyle((props) => {
return {
bg: mode(
config.UI.homepage.heroBanner?.button?._default?.background?.[0] || 'blue.600',
config.UI.homepage.heroBanner?.button?._default?.background?.[1] || 'blue.600',
)(props),
color: mode(
config.UI.homepage.heroBanner?.button?._default?.text_color?.[0] || 'white',
config.UI.homepage.heroBanner?.button?._default?.text_color?.[1] || 'white',
)(props),
_hover: {
bg: mode(
config.UI.homepage.heroBanner?.button?._hover?.background?.[0] || 'blue.400',
config.UI.homepage.heroBanner?.button?._hover?.background?.[1] || 'blue.400',
)(props),
color: mode(
config.UI.homepage.heroBanner?.button?._hover?.text_color?.[0] || 'white',
config.UI.homepage.heroBanner?.button?._hover?.text_color?.[1] || 'white',
)(props),
},
'&[data-selected=true]': {
bg: mode(
config.UI.homepage.heroBanner?.button?._selected?.background?.[0] || 'blue.50',
config.UI.homepage.heroBanner?.button?._selected?.background?.[1] || 'blue.50',
)(props),
color: mode(
config.UI.homepage.heroBanner?.button?._selected?.text_color?.[0] || 'blackAlpha.800',
config.UI.homepage.heroBanner?.button?._selected?.text_color?.[1] || 'blackAlpha.800',
)(props),
},
};
});
// for buttons in the page header
const variantHeader = defineStyle((props) => {
return {
bgColor: 'transparent',
color: mode('blackAlpha.800', 'gray.400')(props),
borderColor: mode('gray.300', 'gray.600')(props),
borderWidth: props.borderWidth || '2px',
borderStyle: 'solid',
_hover: {
color: 'link_hovered',
borderColor: 'link_hovered',
},
'&[data-selected=true]': {
bgColor: mode('blackAlpha.50', 'whiteAlpha.100')(props),
color: mode('blackAlpha.800', 'whiteAlpha.800')(props),
borderColor: 'transparent',
borderWidth: props.borderWidth || '0px',
},
'&[data-selected=true][data-warning=true]': {
bgColor: mode('orange.100', 'orange.900')(props),
color: mode('blackAlpha.800', 'whiteAlpha.800')(props),
borderColor: 'transparent',
borderWidth: props.borderWidth || '0px',
},
};
});
const variants = {
solid: variantSolid,
outline: variantOutline,
simple: variantSimple,
ghost: variantGhost,
subtle: variantSubtle,
hero: variantHero,
header: variantHeader,
};
const baseStyle = defineStyle({
......
import { theme } from '@chakra-ui/react';
export const BODY_TYPEFACE = 'Inter';
export const HEADING_TYPEFACE = 'Poppins';
import config from 'configs/app';
export const BODY_TYPEFACE = config.UI.fonts.body?.name ?? 'Inter';
export const HEADING_TYPEFACE = config.UI.fonts.heading?.name ?? 'Poppins';
const typography = {
fonts: {
......
......@@ -11,7 +11,7 @@
"devDependencies": {
"ts-loader": "^9.4.4",
"tsconfig-paths-webpack-plugin": "^4.1.0",
"webpack": "^5.88.2",
"webpack": "^5.94.0",
"webpack-cli": "^5.1.4"
}
},
......@@ -89,34 +89,11 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@types/eslint": {
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.0.tgz",
"integrity": "sha512-gi6WQJ7cHRgZxtkQEoyHMppPjq9Kxo5Tjn2prSKDSmZrCz8TZ3jSRCeTJm+WoM+oB0WG37bRqLzaaU3q7JypGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "*",
"@types/json-schema": "*"
}
},
"node_modules/@types/eslint-scope": {
"version": "3.7.7",
"resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
"integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/eslint": "*",
"@types/estree": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
......@@ -992,11 +969,10 @@
"license": "MIT"
},
"node_modules/micromatch": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
......@@ -1603,13 +1579,11 @@
}
},
"node_modules/webpack": {
"version": "5.93.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz",
"integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==",
"version": "5.94.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz",
"integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/eslint-scope": "^3.7.3",
"@types/estree": "^1.0.5",
"@webassemblyjs/ast": "^1.12.1",
"@webassemblyjs/wasm-edit": "^1.12.1",
......@@ -1618,7 +1592,7 @@
"acorn-import-attributes": "^1.9.5",
"browserslist": "^4.21.10",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.17.0",
"enhanced-resolve": "^5.17.1",
"es-module-lexer": "^1.2.1",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
......
......@@ -8,7 +8,7 @@
},
"devDependencies": {
"ts-loader": "^9.4.4",
"webpack": "^5.88.2",
"webpack": "^5.94.0",
"webpack-cli": "^5.1.4",
"tsconfig-paths-webpack-plugin": "^4.1.0"
},
......
......@@ -47,28 +47,12 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@types/eslint-scope@^3.7.3":
version "3.7.7"
resolved "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz"
integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==
dependencies:
"@types/eslint" "*"
"@types/estree" "*"
"@types/eslint@*":
version "9.6.0"
resolved "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.0.tgz"
integrity sha512-gi6WQJ7cHRgZxtkQEoyHMppPjq9Kxo5Tjn2prSKDSmZrCz8TZ3jSRCeTJm+WoM+oB0WG37bRqLzaaU3q7JypGg==
dependencies:
"@types/estree" "*"
"@types/json-schema" "*"
"@types/estree@*", "@types/estree@^1.0.5":
"@types/estree@^1.0.5":
version "1.0.5"
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz"
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
"@types/json-schema@*", "@types/json-schema@^7.0.8":
"@types/json-schema@^7.0.8":
version "7.0.15"
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz"
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
......@@ -348,7 +332,7 @@ electron-to-chromium@^1.4.820:
resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.0.tgz"
integrity sha512-Vb3xHHYnLseK8vlMJQKJYXJ++t4u1/qJ3vykuVrVjvdiOEhYyT1AuP4x03G8EnPmYvYOhe9T+dADTmthjRQMkA==
enhanced-resolve@^5.0.0, enhanced-resolve@^5.17.0, enhanced-resolve@^5.7.0:
enhanced-resolve@^5.0.0, enhanced-resolve@^5.17.1, enhanced-resolve@^5.7.0:
version "5.17.1"
resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz"
integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==
......@@ -552,9 +536,9 @@ merge-stream@^2.0.0:
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
micromatch@^4.0.0:
version "4.0.7"
resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz"
integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==
version "4.0.8"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
dependencies:
braces "^3.0.3"
picomatch "^2.3.1"
......@@ -887,12 +871,11 @@ webpack-sources@^3.2.3:
resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz"
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
webpack@^5.88.2:
version "5.93.0"
resolved "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz"
integrity sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==
webpack@^5.94.0:
version "5.94.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.94.0.tgz#77a6089c716e7ab90c1c67574a28da518a20970f"
integrity sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==
dependencies:
"@types/eslint-scope" "^3.7.3"
"@types/estree" "^1.0.5"
"@webassemblyjs/ast" "^1.12.1"
"@webassemblyjs/wasm-edit" "^1.12.1"
......@@ -901,7 +884,7 @@ webpack@^5.88.2:
acorn-import-attributes "^1.9.5"
browserslist "^4.21.10"
chrome-trace-event "^1.0.2"
enhanced-resolve "^5.17.0"
enhanced-resolve "^5.17.1"
es-module-lexer "^1.2.1"
eslint-scope "5.1.1"
events "^3.2.0"
......
......@@ -11,6 +11,7 @@ const PRESETS = {
eth_sepolia: 'https://eth-sepolia.blockscout.com',
gnosis: 'https://gnosis.blockscout.com',
optimism: 'https://optimism.blockscout.com',
optimism_celestia: 'https://opcelestia-raspberry.gelatoscout.com',
optimism_sepolia: 'https://optimism-sepolia.blockscout.com',
polygon: 'https://polygon.blockscout.com',
rootstock_testnet: 'https://rootstock-testnet.blockscout.com',
......
......@@ -3,7 +3,7 @@ export interface AddressTag {
address_hash: string;
address: AddressParam;
name: string;
id: string;
id: number;
}
export type AddressTags = Array<AddressTag>
......@@ -52,7 +52,7 @@ export interface Transaction {
export interface TransactionTag {
transaction_hash: string;
name: string;
id: string;
id: number;
}
export type TransactionTags = Array<TransactionTag>
......@@ -81,7 +81,7 @@ export interface WatchlistAddress {
exchange_rate: string;
notification_settings: NotificationSettings;
notification_methods: NotificationMethods;
id: string;
id: number;
address: AddressParam;
tokens_count: number;
tokens_fiat_value: string;
......@@ -107,7 +107,7 @@ export type CustomAbis = Array<CustomAbi>
export interface CustomAbi {
name: string;
id: string;
id: number;
contract_address_hash: string;
contract_address: AddressParam;
abi: Array<AbiItem>;
......
......@@ -142,12 +142,6 @@ export interface AddressCoinBalanceHistoryResponse {
} | null;
}
// remove after api release
export type AddressCoinBalanceHistoryChartOld = Array<{
date: string;
value: string;
}>
export type AddressCoinBalanceHistoryChart = {
items: Array<{
date: string;
......
......@@ -2,7 +2,7 @@ import type { AddressMetadataTagApi } from './addressMetadata';
export interface AddressImplementation {
address: string;
name: string | null;
name?: string | null;
}
export interface AddressTag {
......
import type { AddressParam } from './addressParams';
export type AddressesItem = AddressParam &{ tx_count: string; coin_balance: string }
export type AddressesItem = AddressParam & { tx_count: string; coin_balance: string | null }
export type AddressesResponse = {
items: Array<AddressesItem>;
......@@ -11,3 +11,13 @@ export type AddressesResponse = {
} | null;
total_supply: string;
}
export interface AddressesMetadataSearchResult {
items: Array<AddressesItem>;
next_page_params: null;
}
export interface AddressesMetadataSearchFilters {
slug: string;
tag_type: string;
}
......@@ -3,6 +3,7 @@ import type { Reward } from 'types/api/reward';
import type { Transaction } from 'types/api/transaction';
import type { ArbitrumBatchStatus, ArbitrumL2TxData } from './arbitrumL2';
import type { OptimisticL2BatchDataContainer, OptimisticL2BlobTypeEip4844, OptimisticL2BlobTypeCelestia } from './optimisticL2';
import type { TokenInfo } from './token';
import type { TokenTransfer } from './tokenTransfer';
import type { ZkSyncBatchesItem } from './zkSyncL2';
......@@ -59,6 +60,7 @@ export interface Block {
'batch_number': number | null;
};
arbitrum?: ArbitrumBlockData;
optimism?: OptimismBlockData;
// CELO FIELDS
celo?: {
epoch_number: number;
......@@ -78,6 +80,14 @@ type ArbitrumBlockData = {
'status': ArbitrumBatchStatus;
}
export interface OptimismBlockData {
batch_data_container: OptimisticL2BatchDataContainer;
internal_id: number;
blobs: Array<OptimisticL2BlobTypeEip4844> | Array<OptimisticL2BlobTypeCelestia> | null;
l1_timestamp: string;
l1_tx_hashes: Array<string>;
}
export interface BlocksResponse {
items: Array<Block>;
next_page_params: {
......
export interface BackendVersionConfig {
backend_version: string;
}
export interface CsvExportConfig {
limit: number;
}
......@@ -19,6 +19,20 @@ export type SmartContractLicenseType =
'gnu_agpl_v3' |
'bsl_1_1';
export type SmartContractProxyType =
| 'eip1167'
| 'eip1967'
| 'eip1822'
| 'eip930'
| 'eip2535'
| 'master_copy'
| 'basic_implementation'
| 'basic_get_implementation'
| 'comptroller'
| 'clone_with_immutable_arguments'
| 'unknown'
| null;
export interface SmartContract {
deployed_bytecode: string | null;
creation_bytecode: string | null;
......@@ -53,7 +67,7 @@ export interface SmartContract {
remappings?: Array<string>;
};
verified_twin_address_hash: string | null;
minimal_proxy_address_hash: string | null;
proxy_type: SmartContractProxyType | null;
language: string | null;
license_type: SmartContractLicenseType | null;
certified?: boolean;
......
import type { AddressParam } from './addressParams';
import type { Block } from './block';
import type { Transaction } from './transaction';
export type OptimisticL2DepositsItem = {
l1_block_number: number;
......@@ -35,21 +37,82 @@ export type OptimisticL2OutputRootsResponse = {
};
}
export type OptimisticL2BatchDataContainer = 'in_blob4844' | 'in_celestia' | 'in_calldata';
export type OptimisticL2TxnBatchesItem = {
l1_tx_hashes: Array<string>;
internal_id: number;
batch_data_container?: OptimisticL2BatchDataContainer;
l1_timestamp: string;
l2_block_number: number;
l1_tx_hashes: Array<string>;
l2_block_start: number;
l2_block_end: number;
tx_count: number;
}
export type OptimisticL2TxnBatchesResponse = {
items: Array<OptimisticL2TxnBatchesItem>;
next_page_params: {
block_number: number;
id: number;
items_count: number;
};
}
export interface OptimisticL2BlobTypeEip4844 {
hash: string;
l1_timestamp: string;
l1_transaction_hash: string;
}
export interface OptimisticL2BlobTypeCelestia {
commitment: string;
height: number;
l1_timestamp: string;
l1_transaction_hash: string;
namespace: string;
}
interface OptimismL2TxnBatchBase {
internal_id: number;
l1_timestamp: string;
l1_tx_hashes: Array<string>;
l2_block_start: number;
l2_block_end: number;
tx_count: number;
}
export interface OptimismL2TxnBatchTypeCallData extends OptimismL2TxnBatchBase {
batch_data_container: 'in_calldata';
}
export interface OptimismL2TxnBatchTypeEip4844 extends OptimismL2TxnBatchBase {
batch_data_container: 'in_blob4844';
blobs: Array<OptimisticL2BlobTypeEip4844> | null;
}
export interface OptimismL2TxnBatchTypeCelestia extends OptimismL2TxnBatchBase {
batch_data_container: 'in_celestia';
blobs: Array<OptimisticL2BlobTypeCelestia> | null;
}
export type OptimismL2TxnBatch = OptimismL2TxnBatchTypeCallData | OptimismL2TxnBatchTypeEip4844 | OptimismL2TxnBatchTypeCelestia;
export type OptimismL2BatchTxs = {
items: Array<Transaction>;
next_page_params: {
block_number: number;
index: number;
items_count: number;
} | null;
}
export type OptimismL2BatchBlocks = {
items: Array<Block>;
next_page_params: {
batch_number: number;
items_count: number;
} | null;
}
export type OptimisticL2WithdrawalsItem = {
'challenge_period_end': string | null;
'from': AddressParam | null;
......
......@@ -19,6 +19,7 @@ export type HomeStats = {
rootstock_locked_btc?: string | null;
last_output_root_size?: string | null;
secondary_coin_price?: string | null;
secondary_coin_image?: string | null;
celo?: {
epoch_number: number;
};
......
export const CHAIN_INDICATOR_IDS = [ 'daily_txs', 'coin_price', 'secondary_coin_price', 'market_cap', 'tvl' ] as const;
export type ChainIndicatorId = typeof CHAIN_INDICATOR_IDS[number];
export const HOME_STATS_WIDGET_IDS = [
'latest_batch',
'total_blocks',
'average_block_time',
'total_txs',
'latest_l1_state_batch',
'wallet_addresses',
'gas_tracker',
'btc_locked',
'current_epoch',
] as const;
export type HomeStatsWidgetId = typeof HOME_STATS_WIDGET_IDS[number];
export interface HeroBannerButtonState {
background?: Array<string | undefined>;
text_color?: Array<string | undefined>;
}
export interface HeroBannerConfig {
background?: Array<string | undefined>;
text_color?: Array<string | undefined>;
border?: Array<string | undefined>;
button?: {
_default?: HeroBannerButtonState;
_hover?: HeroBannerButtonState;
_selected?: HeroBannerButtonState;
};
}
export interface FontFamily {
name: string;
url: string;
}
......@@ -21,6 +21,7 @@ import AddressBalance from './details/AddressBalance';
import AddressImplementations from './details/AddressImplementations';
import AddressNameInfo from './details/AddressNameInfo';
import AddressNetWorth from './details/AddressNetWorth';
import AddressSaveOnGas from './details/AddressSaveOnGas';
import TokenSelect from './tokenSelect/TokenSelect';
import useAddressCountersQuery from './utils/useAddressCountersQuery';
import type { AddressQuery } from './utils/useAddressQuery';
......@@ -211,6 +212,12 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
/>
) :
0 }
{ !countersQuery.isPlaceholderData && countersQuery.data?.gas_usage_count && (
<AddressSaveOnGas
gasUsed={ countersQuery.data.gas_usage_count }
address={ data.hash }
/>
) }
</DetailsInfoItem.Value>
</>
) }
......
......@@ -20,8 +20,7 @@ const AddressCoinBalanceChart = ({ addressHash }: Props) => {
return undefined;
}
const dataItems = 'items' in data ? data.items : data;
return dataItems.map(({ date, value }) => ({
return data.items.map(({ date, value }) => ({
date: new Date(date),
value: BigNumber(value).div(10 ** config.chain.currency.decimals).toNumber(),
}));
......@@ -35,7 +34,7 @@ const AddressCoinBalanceChart = ({ addressHash }: Props) => {
isLoading={ isPending }
h="300px"
units={ currencyUnits.ether }
emptyText={ data && 'days' in data && `Insufficient data for the past ${ data.days } days` }
emptyText={ data?.days && `Insufficient data for the past ${ data.days } days` }
/>
);
};
......
import { Flex, Skeleton, Button, Grid, GridItem, Alert, Link, chakra, Box, useColorModeValue } from '@chakra-ui/react';
import { Flex, Skeleton, Button, Grid, GridItem, Alert, chakra, Box, useColorModeValue } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useQueryClient } from '@tanstack/react-query';
import type { Channel } from 'phoenix';
......@@ -24,6 +24,7 @@ import LinkExternal from 'ui/shared/links/LinkExternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import RawDataSnippet from 'ui/shared/RawDataSnippet';
import ContractCodeProxyPattern from './ContractCodeProxyPattern';
import ContractSecurityAudits from './ContractSecurityAudits';
import ContractSourceCode from './ContractSourceCode';
......@@ -230,7 +231,7 @@ const ContractCode = ({ addressHash, contractQuery, channel }: Props) => {
Warning! Contract bytecode has been changed and does not match the verified one. Therefore, interaction with this smart contract may be risky.
</Alert>
) }
{ !data?.is_verified && data?.verified_twin_address_hash && !data?.minimal_proxy_address_hash && (
{ !data?.is_verified && data?.verified_twin_address_hash && (!data?.proxy_type || data.proxy_type === 'unknown') && (
<Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap">
<span>Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB </span>
<AddressEntity
......@@ -246,23 +247,7 @@ const ContractCode = ({ addressHash, contractQuery, channel }: Props) => {
<span> page</span>
</Alert>
) }
{ data?.minimal_proxy_address_hash && (
<Alert status="warning" flexWrap="wrap" whiteSpace="pre-wrap">
<span>Minimal Proxy Contract for </span>
<AddressEntity
address={{ hash: data.minimal_proxy_address_hash, is_contract: true }}
truncation="constant"
fontSize="sm"
fontWeight="500"
noCopy
/>
<span>. </span>
<Box>
<Link href="https://eips.ethereum.org/EIPS/eip-1167">EIP-1167</Link>
<span> - minimal bytecode implementation that delegates all calls to a known address</span>
</Box>
</Alert>
) }
{ data?.proxy_type && <ContractCodeProxyPattern type={ data.proxy_type }/> }
</Flex>
{ data?.is_verified && (
<Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 6 } mb={ 8 }>
......
import React from 'react';
import { test, expect } from 'playwright/lib';
import ContractCodeProxyPattern from './ContractCodeProxyPattern';
test('proxy type with link +@mobile', async({ render }) => {
const component = await render(<ContractCodeProxyPattern type="eip1167"/>);
await expect(component).toHaveScreenshot();
});
test('proxy type with link but without description', async({ render }) => {
const component = await render(<ContractCodeProxyPattern type="master_copy"/>);
await expect(component).toHaveScreenshot();
});
test('proxy type without link', async({ render }) => {
const component = await render(<ContractCodeProxyPattern type="basic_implementation"/>);
await expect(component).toHaveScreenshot();
});
import { Alert } from '@chakra-ui/react';
import React from 'react';
import type { SmartContractProxyType } from 'types/api/contract';
import LinkExternal from 'ui/shared/links/LinkExternal';
interface Props {
type: NonNullable<SmartContractProxyType>;
}
const PROXY_TYPES: Record<NonNullable<SmartContractProxyType>, {
name: string;
link?: string;
description?: string;
}> = {
eip1167: {
name: 'EIP-1167',
link: 'https://eips.ethereum.org/EIPS/eip-1167',
description: 'Minimal proxy',
},
eip1967: {
name: 'EIP-1967',
link: 'https://eips.ethereum.org/EIPS/eip-1967',
description: 'Proxy storage slots',
},
eip1822: {
name: 'EIP-1822',
link: 'https://eips.ethereum.org/EIPS/eip-1822',
description: 'Universal upgradeable proxy standard (UUPS)',
},
eip2535: {
name: 'EIP-2535',
link: 'https://eips.ethereum.org/EIPS/eip-2535',
description: 'Diamond proxy',
},
eip930: {
name: 'ERC-930',
link: 'https://github.com/ethereum/EIPs/issues/930',
description: 'Eternal storage',
},
clone_with_immutable_arguments: {
name: 'Clones with immutable arguments',
link: 'https://github.com/wighawag/clones-with-immutable-args',
},
master_copy: {
name: 'GnosisSafe',
link: 'https://github.com/safe-global/safe-smart-account',
},
comptroller: {
name: 'Compound protocol',
link: 'https://github.com/compound-finance/compound-protocol',
},
basic_implementation: {
name: 'public implementation getter in proxy smart-contract',
},
basic_get_implementation: {
name: 'public getImplementation getter in proxy smart-contract',
},
unknown: {
name: 'Unknown proxy pattern',
},
};
const ContractCodeProxyPattern = ({ type }: Props) => {
const proxyInfo = PROXY_TYPES[type];
if (!proxyInfo || type === 'unknown') {
return null;
}
return (
<Alert status="warning" flexWrap="wrap" whiteSpace="pre-wrap">
{ proxyInfo.link ? (
<>
This proxy smart-contract is detected via <LinkExternal href={ proxyInfo.link }>{ proxyInfo.name }</LinkExternal>
{ proxyInfo.description && ` - ${ proxyInfo.description }` }
</>
) : (
<>
This proxy smart-contract is detected via { proxyInfo.name }
{ proxyInfo.description && ` - ${ proxyInfo.description }` }
</>
) }
</Alert>
);
};
export default React.memo(ContractCodeProxyPattern);
......@@ -49,9 +49,13 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
}, [ deleteModalProps ]);
const formData = React.useMemo(() => {
if (typeof watchListId !== 'number') {
return;
}
return {
address_hash: hash,
id: String(watchListId),
id: watchListId,
};
}, [ hash, watchListId ]);
......@@ -83,12 +87,14 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
onSuccess={ handleAddOrDeleteSuccess }
data={ formData }
/>
<DeleteAddressModal
{ ...deleteModalProps }
onClose={ handleDeleteModalClose }
data={ formData }
onSuccess={ handleAddOrDeleteSuccess }
/>
{ formData && (
<DeleteAddressModal
{ ...deleteModalProps }
onClose={ handleDeleteModalClose }
data={ formData }
onSuccess={ handleAddOrDeleteSuccess }
/>
) }
</>
);
};
......
import { Image, Skeleton } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import * as v from 'valibot';
import config from 'configs/app';
import LinkExternal from 'ui/shared/links/LinkExternal';
import TextSeparator from 'ui/shared/TextSeparator';
const feature = config.features.saveOnGas;
const responseSchema = v.object({
percent: v.number(),
});
const ERROR_NAME = 'Invalid response schema';
interface Props {
gasUsed: string;
address: string;
}
const AddressSaveOnGas = ({ gasUsed, address }: Props) => {
const gasUsedNumber = Number(gasUsed);
const query = useQuery({
queryKey: [ 'gas_hawk_saving_potential', { address } ],
queryFn: async() => {
if (!feature.isEnabled) {
return;
}
const response = await fetch(feature.apiUrlTemplate.replace('<address>', address));
const data = await response.json();
return data;
},
select: (response) => {
const parsedResponse = v.safeParse(responseSchema, response);
if (!parsedResponse.success) {
throw Error('Invalid response schema');
}
return parsedResponse.output;
},
placeholderData: { percent: 42 },
enabled: feature.isEnabled && gasUsedNumber > 0,
});
const errorMessage = query.error && 'message' in query.error ? query.error.message : undefined;
React.useEffect(() => {
if (errorMessage === ERROR_NAME) {
fetch('/node-api/monitoring/invalid-api-schema', {
method: 'POST',
body: JSON.stringify({
resource: 'gas_hawk_saving_potential',
url: feature.isEnabled ? feature.apiUrlTemplate.replace('<address>', address) : undefined,
}),
});
}
}, [ address, errorMessage ]);
if (gasUsedNumber <= 0 || !feature.isEnabled || query.isError || !query.data?.percent) {
return null;
}
const percent = Math.round(query.data.percent);
if (percent < 1) {
return null;
}
return (
<>
<TextSeparator color="divider"/>
<Skeleton isLoaded={ !query.isPlaceholderData } display="flex" alignItems="center" columnGap={ 2 }>
<Image src="/static/gas_hawk_logo.svg" w="15px" h="20px" alt="GasHawk logo"/>
<LinkExternal href="https://www.gashawk.io" fontSize="sm">
Save { percent.toLocaleString(undefined, { maximumFractionDigits: 0 }) }% with GasHawk
</LinkExternal>
</Skeleton>
</>
);
};
export default React.memo(AddressSaveOnGas);
......@@ -42,7 +42,7 @@ const DomainsGrid = ({ data }: { data: Array<bens.Domain> }) => {
rowGap={ 4 }
mt={ 2 }
>
{ data.slice(0, 9).map((domain) => <EnsEntity key={ domain.id } name={ domain.name } protocol={ domain.protocol } noCopy/>) }
{ data.slice(0, 9).map((domain) => <EnsEntity key={ domain.id } domain={ domain.name } protocol={ domain.protocol } noCopy/>) }
</Grid>
);
};
......@@ -126,7 +126,7 @@ const AddressEnsDomains = ({ query, addressHash, mainDomainName }: Props) => {
<Box w="100%">
<chakra.span color="text_secondary" fontSize="xs">Primary*</chakra.span>
<Flex alignItems="center" fontSize="md" mt={ 2 }>
<EnsEntity name={ mainDomain.name } protocol={ mainDomain.protocol } fontWeight={ 600 } noCopy/>
<EnsEntity domain={ mainDomain.name } protocol={ mainDomain.protocol } fontWeight={ 600 } noCopy/>
{ mainDomain.expiry_date &&
<chakra.span color="text_secondary" whiteSpace="pre"> (expires { dayjs(mainDomain.expiry_date).fromNow() })</chakra.span> }
</Flex>
......
......@@ -23,11 +23,7 @@ const ERC20TokensTableItem = ({
return (
<Tr
sx={{
'&:hover [aria-label="Add token to wallet"]': {
opacity: 1,
},
}}
role="group"
>
<Td verticalAlign="middle">
<TokenEntity
......@@ -46,7 +42,7 @@ const ERC20TokensTableItem = ({
truncation="constant"
noIcon
/>
<AddressAddToWallet token={ token } ml={ 4 } isLoading={ isLoading } opacity="0"/>
<AddressAddToWallet token={ token } ml={ 4 } isLoading={ isLoading } opacity="0" _groupHover={{ opacity: 1 }}/>
</Flex>
</Td>
<Td isNumeric verticalAlign="middle">
......
......@@ -24,7 +24,7 @@ interface Params {
addressQuery: AddressQuery;
}
export default function useAddressQuery({ hash, addressQuery }: Params): AddressCountersQuery {
export default function useAddressCountersQuery({ hash, addressQuery }: Params): AddressCountersQuery {
const enabled = Boolean(hash) && !addressQuery.isPlaceholderData;
const apiQuery = useApiQuery<'address_counters', { status: number }>('address_counters', {
......
......@@ -25,7 +25,7 @@ const AddressesListItem = ({
isLoading,
}: Props) => {
const addressBalance = BigNumber(item.coin_balance).div(BigNumber(10 ** config.chain.currency.decimals));
const addressBalance = BigNumber(item.coin_balance || 0).div(BigNumber(10 ** config.chain.currency.decimals));
return (
<ListItemMobile rowGap={ 3 }>
......
......@@ -24,7 +24,7 @@ const AddressesTableItem = ({
isLoading,
}: Props) => {
const addressBalance = BigNumber(item.coin_balance).div(BigNumber(10 ** config.chain.currency.decimals));
const addressBalance = BigNumber(item.coin_balance || 0).div(BigNumber(10 ** config.chain.currency.decimals));
const addressBalanceChunks = addressBalance.dp(8).toFormat().split('.');
return (
......
import { HStack, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { AddressesItem } from 'types/api/addresses';
import config from 'configs/app';
import { currencyUnits } from 'lib/units';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
type Props = {
item: AddressesItem;
isLoading?: boolean;
}
const AddressesLabelSearchListItem = ({
item,
isLoading,
}: Props) => {
const addressBalance = BigNumber(item.coin_balance || 0).div(BigNumber(10 ** config.chain.currency.decimals));
return (
<ListItemMobile rowGap={ 3 }>
<AddressEntity
address={ item }
isLoading={ isLoading }
fontWeight={ 700 }
w="100%"
/>
<HStack spacing={ 3 } maxW="100%" alignItems="flex-start">
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 } flexShrink={ 0 }>{ `Balance ${ currencyUnits.ether }` }</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary" minW="0" whiteSpace="pre-wrap">
<span>{ addressBalance.dp(8).toFormat() }</span>
</Skeleton>
</HStack>
<HStack spacing={ 3 }>
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Txn count</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary">
<span>{ Number(item.tx_count).toLocaleString() }</span>
</Skeleton>
</HStack>
</ListItemMobile>
);
};
export default React.memo(AddressesLabelSearchListItem);
import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react';
import type { AddressesItem } from 'types/api/addresses';
import { currencyUnits } from 'lib/units';
import { default as Thead } from 'ui/shared/TheadSticky';
import AddressesLabelSearchTableItem from './AddressesLabelSearchTableItem';
interface Props {
items: Array<AddressesItem>;
top: number;
isLoading?: boolean;
}
const AddressesLabelSearchTable = ({ items, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm">
<Thead top={ top }>
<Tr>
<Th width="70%">Address</Th>
<Th width="15%" isNumeric>{ `Balance ${ currencyUnits.ether }` }</Th>
<Th width="15%" isNumeric>Txn count</Th>
</Tr>
</Thead>
<Tbody>
{ items.map((item, index) => (
<AddressesLabelSearchTableItem
key={ item.hash + (isLoading ? index : '') }
item={ item }
isLoading={ isLoading }
/>
)) }
</Tbody>
</Table>
);
};
export default AddressesLabelSearchTable;
import { Tr, Td, Text, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { AddressesItem } from 'types/api/addresses';
import config from 'configs/app';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
type Props = {
item: AddressesItem;
isLoading?: boolean;
}
const AddressesLabelSearchTableItem = ({
item,
isLoading,
}: Props) => {
const addressBalance = BigNumber(item.coin_balance || 0).div(BigNumber(10 ** config.chain.currency.decimals));
const addressBalanceChunks = addressBalance.dp(8).toFormat().split('.');
return (
<Tr>
<Td>
<AddressEntity
address={ item }
isLoading={ isLoading }
fontWeight={ 700 }
my="2px"
/>
</Td>
<Td isNumeric>
<Skeleton isLoaded={ !isLoading } display="inline-block" maxW="100%">
<Text lineHeight="24px" as="span">{ addressBalanceChunks[0] + (addressBalanceChunks[1] ? '.' : '') }</Text>
<Text lineHeight="24px" variant="secondary" as="span">{ addressBalanceChunks[1] }</Text>
</Skeleton>
</Td>
<Td isNumeric>
<Skeleton isLoaded={ !isLoading } display="inline-block" lineHeight="24px">
{ Number(item.tx_count).toLocaleString() }
</Skeleton>
</Td>
</Tr>
);
};
export default React.memo(AddressesLabelSearchTableItem);
......@@ -19,6 +19,7 @@ import getNetworkValidationActionText from 'lib/networks/getNetworkValidationAct
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import getQueryParamString from 'lib/router/getQueryParamString';
import { currencyUnits } from 'lib/units';
import OptimisticL2TxnBatchDA from 'ui/shared/batch/OptimisticL2TxnBatchDA';
import BlockGasUsed from 'ui/shared/block/BlockGasUsed';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
......@@ -202,7 +203,7 @@ const BlockDetails = ({ query }: Props) => {
hint="Batch number"
isLoading={ isPlaceholderData }
>
Batch
Batch
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
{ data.arbitrum.batch_number ?
......@@ -212,6 +213,28 @@ const BlockDetails = ({ query }: Props) => {
</>
) }
{ rollupFeature.isEnabled && rollupFeature.type === 'optimistic' && data.optimism && !config.UI.views.block.hiddenFields?.batch && (
<>
<DetailsInfoItem.Label
hint="Batch number"
isLoading={ isPlaceholderData }
>
Batch
</DetailsInfoItem.Label>
<DetailsInfoItem.Value columnGap={ 3 }>
{ data.optimism.internal_id ?
<BatchEntityL2 isLoading={ isPlaceholderData } number={ data.optimism.internal_id }/> :
<Skeleton isLoaded={ !isPlaceholderData }>Pending</Skeleton> }
{ data.optimism.batch_data_container && (
<OptimisticL2TxnBatchDA
container={ data.optimism.batch_data_container }
isLoading={ isPlaceholderData }
/>
) }
</DetailsInfoItem.Value>
</>
) }
<DetailsInfoItem.Label
hint="Size of the block in bytes"
isLoading={ isPlaceholderData }
......
......@@ -26,7 +26,7 @@ const TABS_HEIGHT = 88;
interface Props {
type?: BlockType;
query: QueryWithPagesResult<'blocks'>;
query: QueryWithPagesResult<'blocks'> | QueryWithPagesResult<'optimistic_l2_txn_batch_blocks'>;
enableSocket?: boolean;
top?: number;
}
......
......@@ -18,13 +18,14 @@ type FileTypes = '.sol' | '.yul' | '.json' | '.vy'
interface Props {
name?: 'sources' | 'interfaces';
fileTypes: Array<FileTypes>;
fullFilePath?: boolean;
multiple?: boolean;
required?: boolean;
title: string;
hint: string | React.ReactNode;
}
const ContractVerificationFieldSources = ({ fileTypes, multiple, required, title, hint, name = 'sources' }: Props) => {
const ContractVerificationFieldSources = ({ fileTypes, multiple, required, title, hint, name = 'sources', fullFilePath }: Props) => {
const { setValue, getValues, control, formState, clearErrors } = useFormContext<FormFields>();
const error = (() => {
......@@ -114,7 +115,7 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, required, title
rowGap={ 2 }
w="100%"
>
<DragAndDropArea onDrop={ onChange } p={{ base: 3, lg: 6 }} isDisabled={ formState.isSubmitting }>
<DragAndDropArea onDrop={ onChange } fullFilePath={ fullFilePath } p={{ base: 3, lg: 6 }} isDisabled={ formState.isSubmitting }>
{ hasValue ? renderFiles(field.value) : renderUploadButton() }
</DragAndDropArea>
</Flex>
......@@ -123,7 +124,7 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, required, title
{ errorElement }
</>
);
}, [ fileTypes, multiple, commonError, formState.isSubmitting, renderFiles, renderUploadButton ]);
}, [ fileTypes, multiple, commonError, formState.isSubmitting, renderFiles, renderUploadButton, fullFilePath ]);
const validateFileType = React.useCallback(async(value: FieldPathValue<FormFields, typeof name>): Promise<ValidateResult> => {
if (Array.isArray(value)) {
......
......@@ -18,6 +18,7 @@ const ContractVerificationMultiPartFile = () => {
<ContractVerificationFieldSources
fileTypes={ FILE_TYPES }
multiple
fullFilePath
required
title="Sources *.sol or *.yul files"
hint="Upload all Solidity or Yul contract source files."
......
......@@ -37,6 +37,7 @@ const ContractVerificationVyperMultiPartFile = () => {
name="interfaces"
fileTypes={ INTERFACE_TYPES }
multiple
fullFilePath
title="Interfaces (.vy or .json)"
hint={ interfacesHint }
/>
......
......@@ -98,7 +98,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const onSubmit: SubmitHandler<Inputs> = useCallback((formData) => {
setAlertVisible(false);
mutation.mutate({ ...formData, id: data?.id });
mutation.mutate({ ...formData, id: data?.id ? String(data.id) : undefined });
}, [ mutation, data, setAlertVisible ]);
const renderContractAddressInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'contract_address_hash'>}) => {
......
......@@ -30,7 +30,7 @@ const CustomAbiTable = ({ data, isLoading, onDeleteClick, onEditClick }: Props)
<Tbody>
{ data?.map((item, index) => (
<CustomAbiTableItem
key={ item.id + (isLoading ? index : '') }
key={ item.id + (isLoading ? String(index) : '') }
item={ item }
isLoading={ isLoading }
onDeleteClick={ onDeleteClick }
......
......@@ -32,6 +32,7 @@ const GasTrackerPriceSnippet = ({ data, type, isLoading }: Props) => {
average: useColorModeValue('gray.50', 'whiteAlpha.200'),
slow: useColorModeValue('gray.50', 'whiteAlpha.200'),
};
const borderColor = useColorModeValue('gray.200', 'whiteAlpha.300');
return (
<Box
......@@ -41,6 +42,11 @@ const GasTrackerPriceSnippet = ({ data, type, isLoading }: Props) => {
py={ 6 }
w={{ lg: 'calc(100% / 3)' }}
bgColor={ bgColors[type] }
_notLast={{
borderColor: borderColor,
borderRightWidth: { lg: '2px' },
borderBottomWidth: { base: '2px', lg: '0' },
}}
>
<Skeleton textStyle="h3" isLoaded={ !isLoading } w="fit-content">{ TITLES[type] }</Skeleton>
<Flex columnGap={ 3 } alignItems="center" mt={ 3 }>
......
......@@ -21,13 +21,6 @@ const GasTrackerPrices = ({ prices, isLoading }: Props) => {
borderWidth="2px"
borderRadius="xl"
overflow="hidden"
sx={{
'li:not(:last-child)': {
borderColor: borderColor,
borderRightWidth: { lg: '2px' },
borderBottomWidth: { base: '2px', lg: '0' },
},
}}
>
{ prices.fast && <GasTrackerPriceSnippet type="fast" data={ prices.fast } isLoading={ isLoading }/> }
{ prices.average && <GasTrackerPriceSnippet type="average" data={ prices.average } isLoading={ isLoading }/> }
......
import type { BrowserContext } from '@playwright/test';
import React from 'react';
import * as profileMock from 'mocks/user/profile';
import { contextWithAuth } from 'playwright/fixtures/auth';
import { test, expect } from 'playwright/lib';
import * as pwConfig from 'playwright/utils/config';
import HeroBanner from './HeroBanner';
const authTest = test.extend<{ context: BrowserContext }>({
context: contextWithAuth,
});
authTest('customization +@dark-mode', async({ render, page, mockEnvs, mockApiResponse, mockAssetResponse }) => {
const IMAGE_URL = 'https://localhost:3000/my-image.png';
await mockEnvs([
// eslint-disable-next-line max-len
[ 'NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG', `{"background":["lightpink","no-repeat center/cover url(${ IMAGE_URL })"],"text_color":["deepskyblue","white"],"border":["3px solid green","3px dashed yellow"],"button":{"_default":{"background":["deeppink"],"text_color":["white"]},"_selected":{"background":["lime"]}}}` ],
]);
await page.route(IMAGE_URL, (route) => {
return route.fulfill({
status: 200,
path: './playwright/mocks/image_long.jpg',
});
});
await mockApiResponse('user_info', profileMock.base);
await mockAssetResponse(profileMock.base.avatar, './playwright/mocks/image_s.jpg');
const component = await render(<HeroBanner/>);
await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ],
maskColor: pwConfig.maskColor,
});
});
import { Box, Flex, Heading, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import AdBanner from 'ui/shared/ad/AdBanner';
import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
import SearchBar from 'ui/snippets/searchBar/SearchBar';
import WalletMenuDesktop from 'ui/snippets/walletMenu/WalletMenuDesktop';
const BACKGROUND_DEFAULT = 'radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)';
const TEXT_COLOR_DEFAULT = 'white';
const BORDER_DEFAULT = 'none';
const HeroBanner = () => {
const background = useColorModeValue(
config.UI.homepage.heroBanner?.background?.[0] || config.UI.homepage.plate.background || BACKGROUND_DEFAULT,
config.UI.homepage.heroBanner?.background?.[1] || config.UI.homepage.plate.background || BACKGROUND_DEFAULT,
);
const textColor = useColorModeValue(
config.UI.homepage.heroBanner?.text_color?.[0] || config.UI.homepage.plate.textColor || TEXT_COLOR_DEFAULT,
config.UI.homepage.heroBanner?.text_color?.[1] || config.UI.homepage.plate.textColor || TEXT_COLOR_DEFAULT,
);
const border = useColorModeValue(
config.UI.homepage.heroBanner?.border?.[0] || BORDER_DEFAULT,
config.UI.homepage.heroBanner?.border?.[1] || BORDER_DEFAULT,
);
return (
<Flex
w="100%"
background={ background }
border={ border }
borderRadius="md"
p={{ base: 4, lg: 8 }}
columnGap={ 8 }
alignItems="center"
>
<Box flexGrow={ 1 }>
<Flex mb={{ base: 2, lg: 3 }} justifyContent="space-between" alignItems="center" columnGap={ 2 }>
<Heading
as="h1"
fontSize={{ base: '18px', lg: '30px' }}
lineHeight={{ base: '24px', lg: '36px' }}
fontWeight={{ base: 500, lg: 700 }}
color={ textColor }
>
{
config.meta.seo.enhancedDataEnabled ?
`${ config.chain.name } blockchain explorer` :
`${ config.chain.name } explorer`
}
</Heading>
{ config.UI.navigation.layout === 'vertical' && (
<Box display={{ base: 'none', lg: 'flex' }}>
{ config.features.account.isEnabled && <ProfileMenuDesktop isHomePage/> }
{ config.features.blockchainInteraction.isEnabled && <WalletMenuDesktop isHomePage/> }
</Box>
) }
</Flex>
<SearchBar isHomepage/>
</Box>
<AdBanner platform="mobile" w="fit-content" flexShrink={ 0 } borderRadius="md" overflow="hidden" display={{ base: 'none', lg: 'block ' }}/>
</Flex>
);
};
export default React.memo(HeroBanner);
......@@ -9,7 +9,10 @@ import Stats from './Stats';
test.describe('all items', () => {
let component: Locator;
test.beforeEach(async({ render, mockApiResponse }) => {
test.beforeEach(async({ render, mockApiResponse, mockEnvs }) => {
await mockEnvs([
[ 'NEXT_PUBLIC_HOMEPAGE_STATS', '["total_blocks","average_block_time","total_txs","wallet_addresses","gas_tracker","btc_locked"]' ],
]);
await mockApiResponse('stats', statsMock.withBtcLocked);
component = await render(<Stats/>);
});
......@@ -28,7 +31,7 @@ test('no gas info', async({ render, mockApiResponse }) => {
test('4 items default view +@mobile -@default', async({ render, mockApiResponse, mockEnvs }) => {
await mockEnvs([
[ 'NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME', 'false' ],
[ 'NEXT_PUBLIC_HOMEPAGE_STATS', '["total_txs","gas_tracker","wallet_addresses","total_blocks"]' ],
]);
await mockApiResponse('stats', statsMock.base);
const component = await render(<Stats/>);
......@@ -37,8 +40,7 @@ test('4 items default view +@mobile -@default', async({ render, mockApiResponse,
test('3 items default view +@mobile -@default', async({ render, mockApiResponse, mockEnvs }) => {
await mockEnvs([
[ 'NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME', 'false' ],
[ 'NEXT_PUBLIC_GAS_TRACKER_ENABLED', 'false' ],
[ 'NEXT_PUBLIC_HOMEPAGE_STATS', '["total_txs","wallet_addresses","total_blocks"]' ],
]);
await mockApiResponse('stats', statsMock.base);
const component = await render(<Stats/>);
......
......@@ -2,6 +2,8 @@ import { Grid } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { HomeStatsWidgetId } from 'types/homepage';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { WEI } from 'lib/consts';
......@@ -12,7 +14,6 @@ import IconSvg from 'ui/shared/IconSvg';
import type { Props as StatsWidgetProps } from 'ui/shared/stats/StatsWidget';
import StatsWidget from 'ui/shared/stats/StatsWidget';
const hasAvgBlockTime = config.UI.homepage.showAvgBlockTime;
const rollupFeature = config.features.rollup;
const Stats = () => {
......@@ -35,37 +36,54 @@ const Stats = () => {
const zkEvmLatestBatchQuery = useApiQuery('homepage_zkevm_latest_batch', {
queryOptions: {
placeholderData: 12345,
enabled: rollupFeature.isEnabled && rollupFeature.type === 'zkEvm',
enabled: rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' && config.UI.homepage.stats.includes('latest_batch'),
},
});
const zkSyncLatestBatchQuery = useApiQuery('homepage_zksync_latest_batch', {
queryOptions: {
placeholderData: 12345,
enabled: rollupFeature.isEnabled && rollupFeature.type === 'zkSync',
enabled: rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && config.UI.homepage.stats.includes('latest_batch'),
},
});
const arbitrumLatestBatchQuery = useApiQuery('homepage_arbitrum_latest_batch', {
queryOptions: {
placeholderData: 12345,
enabled: rollupFeature.isEnabled && rollupFeature.type === 'arbitrum',
enabled: rollupFeature.isEnabled && rollupFeature.type === 'arbitrum' && config.UI.homepage.stats.includes('latest_batch'),
},
});
if (isError || zkEvmLatestBatchQuery.isError || zkSyncLatestBatchQuery.isError || arbitrumLatestBatchQuery.isError) {
const latestBatchQuery = (() => {
if (!rollupFeature.isEnabled || !config.UI.homepage.stats.includes('latest_batch')) {
return;
}
switch (rollupFeature.type) {
case 'zkEvm':
return zkEvmLatestBatchQuery;
case 'zkSync':
return zkSyncLatestBatchQuery;
case 'arbitrum':
return arbitrumLatestBatchQuery;
}
})();
if (isError || latestBatchQuery?.isError) {
return null;
}
const isLoading = isPlaceholderData ||
(rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' && zkEvmLatestBatchQuery.isPlaceholderData) ||
(rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && zkSyncLatestBatchQuery.isPlaceholderData) ||
(rollupFeature.isEnabled && rollupFeature.type === 'arbitrum' && arbitrumLatestBatchQuery.isPlaceholderData);
const isLoading = isPlaceholderData || latestBatchQuery?.isPlaceholderData;
interface Item extends StatsWidgetProps {
id: HomeStatsWidgetId;
}
const content = (() => {
const items: Array<Item> = (() => {
if (!data) {
return null;
return [];
}
const gasInfoTooltip = hasGasTracker && data.gas_prices && data.gas_prices.average ? (
<GasInfoTooltip data={ data } dataUpdatedAt={ dataUpdatedAt }>
<IconSvg
......@@ -80,41 +98,40 @@ const Stats = () => {
</GasInfoTooltip>
) : null;
const hasBatches = rollupFeature.isEnabled && (rollupFeature.type === 'zkEvm' || rollupFeature.type === 'zkSync' || rollupFeature.type === 'arbitrum');
const latestBatch =
(hasBatches && rollupFeature.type === 'zkEvm' ? zkEvmLatestBatchQuery.data : null) ||
(hasBatches && rollupFeature.type === 'zkSync' ? zkSyncLatestBatchQuery.data : null) ||
(hasBatches && rollupFeature.type === 'arbitrum' ? arbitrumLatestBatchQuery.data : null) || 0;
const items: Array<StatsWidgetProps> = [
hasBatches && {
return [
latestBatchQuery?.data !== undefined && {
id: 'latest_batch' as const,
icon: 'txn_batches_slim' as const,
label: 'Latest batch',
value: latestBatch.toLocaleString(),
value: latestBatchQuery.data.toLocaleString(),
href: { pathname: '/batches' as const },
isLoading,
},
!hasBatches && {
{
id: 'total_blocks' as const,
icon: 'block_slim' as const,
label: 'Total blocks',
value: Number(data.total_blocks).toLocaleString(),
href: { pathname: '/blocks' as const },
isLoading,
},
hasAvgBlockTime && {
{
id: 'average_block_time' as const,
icon: 'clock-light' as const,
label: 'Average block time',
value: `${ (data.average_block_time / 1000).toFixed(1) }s`,
isLoading,
},
{
id: 'total_txs' as const,
icon: 'transactions_slim' as const,
label: 'Total transactions',
value: Number(data.total_transactions).toLocaleString(),
href: { pathname: '/txs' as const },
isLoading,
},
rollupFeature.isEnabled && data.last_output_root_size && {
data.last_output_root_size && {
id: 'latest_l1_state_batch' as const,
icon: 'txn_batches_slim' as const,
label: 'Latest L1 state batch',
value: data.last_output_root_size,
......@@ -122,12 +139,14 @@ const Stats = () => {
isLoading,
},
{
id: 'wallet_addresses' as const,
icon: 'wallet' as const,
label: 'Wallet addresses',
value: Number(data.total_addresses).toLocaleString(),
isLoading,
},
hasGasTracker && data.gas_prices && {
id: 'gas_tracker' as const,
icon: 'gas' as const,
label: 'Gas tracker',
value: data.gas_prices.average ? <GasPrice data={ data.gas_prices.average }/> : 'N/A',
......@@ -135,33 +154,39 @@ const Stats = () => {
isLoading,
},
data.rootstock_locked_btc && {
id: 'btc_locked' as const,
icon: 'coins/bitcoin' as const,
label: 'BTC Locked in 2WP',
value: `${ BigNumber(data.rootstock_locked_btc).div(WEI).dp(0).toFormat() } RBTC`,
isLoading,
},
data.celo && {
id: 'current_epoch' as const,
icon: 'hourglass' as const,
label: 'Current epoch',
value: `#${ data.celo.epoch_number }`,
isLoading,
},
].filter(Boolean);
return (
<>
{ items.map((item, index) => (
<StatsWidget
key={ item.icon }
{ ...item }
isLoading={ isLoading }
_last={ items.length % 2 === 1 && index === items.length - 1 ? { gridColumn: 'span 2' } : undefined }/>
),
) }
</>
);
]
.filter(Boolean)
.filter(({ id }) => config.UI.homepage.stats.includes(id))
.sort((a, b) => {
const indexA = config.UI.homepage.stats.indexOf(a.id);
const indexB = config.UI.homepage.stats.indexOf(b.id);
if (indexA > indexB) {
return 1;
}
if (indexA < indexB) {
return -1;
}
return 0;
});
})();
if (items.length === 0) {
return null;
}
return (
<Grid
gridTemplateColumns="1fr 1fr"
......@@ -169,7 +194,14 @@ const Stats = () => {
flexBasis="50%"
flexGrow={ 1 }
>
{ content }
{ items.map((item, index) => (
<StatsWidget
key={ item.id }
{ ...item }
isLoading={ isLoading }
_last={ items.length % 2 === 1 && index === items.length - 1 ? { gridColumn: 'span 2' } : undefined }/>
),
) }
</Grid>
);
......
......@@ -21,6 +21,7 @@ test.describe('daily txs chart', () => {
await mockApiResponse('stats', statsMock.withSecondaryCoin);
await mockApiResponse('stats_charts_txs', dailyTxsMock.base);
await mockAssetResponse(statsMock.withSecondaryCoin.coin_image as string, './playwright/mocks/image_svg.svg');
await mockAssetResponse(statsMock.withSecondaryCoin.secondary_coin_image as string, './playwright/mocks/image_s.jpg');
component = await render(<ChainIndicators/>);
await page.waitForFunction(() => {
return document.querySelector('path[data-name="gradient-chart-area"]')?.getAttribute('opacity') === '1';
......
......@@ -7,7 +7,6 @@ import config from 'configs/app';
import { sortByDateDesc } from 'ui/shared/chart/utils/sorts';
import IconSvg from 'ui/shared/IconSvg';
import NativeTokenIcon from 'ui/shared/NativeTokenIcon';
import TokenLogoPlaceholder from 'ui/shared/TokenLogoPlaceholder';
const nonNullTailReducer = (result: Array<TimeChartItemRaw>, item: TimeChartItemRaw) => {
if (item.value === null && result.length === 0) {
......@@ -71,7 +70,7 @@ const secondaryCoinPriceIndicator: TChainIndicator<'stats_charts_secondary_coin_
'$N/A' :
'$' + Number(stats.secondary_coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }),
valueDiff: () => null,
icon: <TokenLogoPlaceholder boxSize={ 6 }/>,
icon: <NativeTokenIcon boxSize={ 6 } type="secondary"/>,
hint: `${ config.chain.secondaryCoin.symbol } token daily price in USD.`,
api: {
resourceName: 'stats_charts_secondary_coin_price',
......
import React from 'react';
import * as depositMock from 'mocks/l2deposits/deposits';
import * as depositMock from 'mocks/optimism/deposits';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';
......
......@@ -25,7 +25,7 @@ const NameDomainsListItem = ({
<ListItemMobileGrid.Container>
<ListItemMobileGrid.Label isLoading={ isLoading }>Domain</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<EnsEntity name={ name } protocol={ protocol } isLoading={ isLoading } fontWeight={ 500 }/>
<EnsEntity domain={ name } protocol={ protocol } isLoading={ isLoading } fontWeight={ 500 }/>
</ListItemMobileGrid.Value>
{ resolvedAddress && (
......
......@@ -24,7 +24,7 @@ const NameDomainsTableItem = ({
return (
<Tr>
<Td verticalAlign="middle">
<EnsEntity name={ name } protocol={ protocol } isLoading={ isLoading } fontWeight={ 600 }/>
<EnsEntity domain={ name } protocol={ protocol } isLoading={ isLoading } fontWeight={ 600 }/>
</Td>
<Td verticalAlign="middle">
{ resolvedAddress && <AddressEntity address={ resolvedAddress } isLoading={ isLoading } fontWeight={ 500 }/> }
......
import React from 'react';
import type { AddressesMetadataSearchResult } from 'types/api/addresses';
import * as addressMocks from 'mocks/address/address';
import { test, expect } from 'playwright/lib';
import AccountsLabelSearch from './AccountsLabelSearch';
const addresses: AddressesMetadataSearchResult = {
items: [
{
...addressMocks.withName,
tx_count: '1',
coin_balance: '12345678901234567890000',
},
{
...addressMocks.token,
tx_count: '109123890123',
coin_balance: '22222345678901234567890000',
ens_domain_name: null,
},
{
...addressMocks.withoutName,
tx_count: '11',
coin_balance: '1000000000000000000',
},
{
...addressMocks.eoa,
tx_count: '420',
coin_balance: null,
},
],
next_page_params: null,
};
const hooksConfig = {
router: {
query: {
slug: 'euler-finance-exploit',
tagType: 'generic',
tagName: 'Euler finance exploit',
},
},
};
test('base view +@mobile', async({ render, mockTextAd, mockApiResponse }) => {
await mockTextAd();
await mockApiResponse(
'addresses_metadata_search',
addresses,
{
queryParams: {
slug: 'euler-finance-exploit',
tag_type: 'generic',
},
},
);
const component = await render(<AccountsLabelSearch/>, { hooksConfig });
await expect(component).toHaveScreenshot();
});
import { chakra, Flex, Hide, Show, Skeleton } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { EntityTag as TEntityTag, EntityTagType } from 'ui/shared/EntityTags/types';
import getQueryParamString from 'lib/router/getQueryParamString';
import { TOP_ADDRESS } from 'stubs/address';
import { generateListStub } from 'stubs/utils';
import AddressesLabelSearchListItem from 'ui/addressesLabelSearch/AddressesLabelSearchListItem';
import AddressesLabelSearchTable from 'ui/addressesLabelSearch/AddressesLabelSearchTable';
import { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import EntityTag from 'ui/shared/EntityTags/EntityTag';
import PageTitle from 'ui/shared/Page/PageTitle';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import StickyPaginationWithText from 'ui/shared/StickyPaginationWithText';
const AccountsLabelSearch = () => {
const router = useRouter();
const slug = getQueryParamString(router.query.slug);
const tagType = getQueryParamString(router.query.tagType);
const tagName = getQueryParamString(router.query.tagName);
const { isError, isPlaceholderData, data, pagination } = useQueryWithPages({
resourceName: 'addresses_metadata_search',
filters: {
slug,
tag_type: tagType,
},
options: {
placeholderData: generateListStub<'addresses_metadata_search'>(
TOP_ADDRESS,
50,
{
next_page_params: null,
},
),
},
});
const content = data?.items ? (
<>
<Hide below="lg" ssr={ false }>
<AddressesLabelSearchTable
top={ pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 }
items={ data.items }
isLoading={ isPlaceholderData }
/>
</Hide>
<Show below="lg" ssr={ false }>
{ data.items.map((item, index) => {
return (
<AddressesLabelSearchListItem
key={ item.hash + (isPlaceholderData ? index : '') }
item={ item }
isLoading={ isPlaceholderData }
/>
);
}) }
</Show>
</>
) : null;
const text = (() => {
if (isError) {
return null;
}
const num = data?.items.length || 0;
const tagData: TEntityTag = {
tagType: tagType as EntityTagType,
slug,
name: tagName || slug,
ordinal: 0,
};
return (
<Flex alignItems="center" columnGap={ 2 } flexWrap="wrap" rowGap={ 1 }>
<Skeleton
isLoaded={ !isPlaceholderData }
display="inline-block"
>
Found{ ' ' }
<chakra.span fontWeight={ 700 }>
{ num }{ data?.next_page_params || pagination.page > 1 ? '+' : '' }
</chakra.span>{ ' ' }
matching result{ num > 1 ? 's' : '' } for
</Skeleton>
<EntityTag data={ tagData } isLoading={ isPlaceholderData } noLink/>
</Flex>
);
})();
const actionBar = <StickyPaginationWithText text={ text } pagination={ pagination }/>;
return (
<>
<PageTitle title="Search result" withTextAd/>
<DataListDisplay
isError={ isError }
items={ data?.items }
emptyText={ text }
content={ content }
actionBar={ actionBar }
/>
</>
);
};
export default AccountsLabelSearch;
......@@ -6,6 +6,7 @@ import type { EntityTag } from 'ui/shared/EntityTags/types';
import type { RoutedTab } from 'ui/shared/Tabs/types';
import config from 'configs/app';
import getCheckedSummedAddress from 'lib/address/getCheckedSummedAddress';
import useAddressMetadataInfoQuery from 'lib/address/useAddressMetadataInfoQuery';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
......@@ -60,6 +61,7 @@ const AddressPageContent = () => {
const tabsScrollRef = React.useRef<HTMLDivElement>(null);
const hash = getQueryParamString(router.query.hash);
const checkSummedHash = React.useMemo(() => getCheckedSummedAddress(hash), [ hash ]);
const areQueriesEnabled = !useCheckDomainNameParam(hash);
const addressQuery = useAddressQuery({ hash, isEnabled: areQueriesEnabled });
......@@ -270,7 +272,11 @@ const AddressPageContent = () => {
const titleContentAfter = (
<EntityTags
tags={ tags }
isLoading={ isLoading || (config.features.addressMetadata.isEnabled && addressMetadataQuery.isPending) }
isLoading={
isLoading ||
(config.features.userOps.isEnabled && userOpsAccountQuery.isPlaceholderData) ||
(config.features.addressMetadata.isEnabled && addressMetadataQuery.isPending)
}
/>
);
......@@ -300,7 +306,7 @@ const AddressPageContent = () => {
<Flex alignItems="center" w="100%" columnGap={ 2 } rowGap={ 2 } flexWrap={{ base: 'wrap', lg: 'nowrap' }}>
{ addressQuery.data?.ens_domain_name && (
<EnsEntity
name={ addressQuery.data?.ens_domain_name }
domain={ addressQuery.data?.ens_domain_name }
protocol={ !addressEnsDomainsQuery.isPending ? addressMainDomain?.protocol : null }
fontFamily="heading"
fontSize="lg"
......@@ -310,14 +316,14 @@ const AddressPageContent = () => {
/>
) }
<AddressEntity
address={{ ...addressQuery.data, hash, name: '', ens_domain_name: '', implementations: null }}
address={{ ...addressQuery.data, hash: checkSummedHash, name: '', ens_domain_name: '', implementations: null }}
isLoading={ isLoading }
fontFamily="heading"
fontSize="lg"
fontWeight={ 500 }
noLink
isSafeAddress={ isSafeAddress }
iconColor={ isSafeAddress ? safeIconColor : undefined }
icon={{ color: isSafeAddress ? safeIconColor : undefined }}
mr={ 4 }
/>
{ !isLoading && addressQuery.data?.is_contract && addressQuery.data.token &&
......@@ -325,7 +331,7 @@ const AddressPageContent = () => {
{ !isLoading && !addressQuery.data?.is_contract && config.features.account.isEnabled && (
<AddressFavoriteButton hash={ hash } watchListId={ addressQuery.data?.watchlist_address_id }/>
) }
<AddressQrCode address={{ hash }} isLoading={ isLoading }/>
<AddressQrCode address={{ hash: checkSummedHash }} isLoading={ isLoading }/>
<AccountActionsMenu isLoading={ isLoading }/>
<HStack ml="auto" gap={ 2 }/>
{ !isLoading && addressQuery.data?.is_contract && addressQuery.data?.is_verified && config.UI.views.address.solidityscanEnabled &&
......
......@@ -167,7 +167,7 @@ const BlockPageContent = () => {
})();
const titleSecondRow = (
<>
{ !config.UI.views.block.hiddenFields?.miner && (
{ !config.UI.views.block.hiddenFields?.miner && blockQuery.data?.miner && (
<Skeleton
isLoaded={ !blockQuery.isPlaceholderData }
fontFamily="heading"
......@@ -179,7 +179,7 @@ const BlockPageContent = () => {
<chakra.span flexShrink={ 0 }>
{ `${ capitalize(getNetworkValidationActionText()) } by` }
</chakra.span>
<AddressEntity address={ blockQuery.data?.miner }/>
<AddressEntity address={ blockQuery.data.miner }/>
</Skeleton>
) }
<NetworkExplorers type="block" pathParam={ heightOrHash } ml={{ base: config.UI.views.block.hiddenFields?.miner ? 0 : 3, lg: 'auto' }}/>
......
......@@ -15,6 +15,7 @@ test('base view +@mobile +@dark-mode', async({ render, page, mockApiResponse })
},
};
await mockApiResponse('address', addressMock.validator, { pathParams: { hash: addressMock.hash } });
await mockApiResponse('config_csv_export', { limit: 42123 });
const component = await render(<Box sx={{ '.recaptcha': { w: '304px', h: '78px' } }}><CsvExport/></Box>, { hooksConfig });
......
......@@ -88,7 +88,13 @@ const CsvExport = () => {
},
});
const isLoading = addressQuery.isPending || (exportTypeParam === 'holders' && tokenQuery.isPending);
const configQuery = useApiQuery('config_csv_export', {
queryOptions: {
enabled: Boolean(addressHash),
},
});
const isLoading = addressQuery.isPending || configQuery.isPending || (exportTypeParam === 'holders' && tokenQuery.isPending);
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/address');
......@@ -147,7 +153,9 @@ const CsvExport = () => {
return null;
}
if (exportTypeParam === 'holders') {
const limit = (configQuery.data?.limit || 10_000).toLocaleString(undefined, { maximumFractionDigits: 3, notation: 'compact' });
if (exportTypeParam === 'holders' && tokenQuery.data) {
return (
<Flex mb={ 10 } whiteSpace="pre-wrap" flexWrap="wrap">
<span>Export { exportType.text } for token </span>
......@@ -160,11 +168,15 @@ const CsvExport = () => {
noSymbol
/>
<span> to CSV file. </span>
<span>Exports are limited to the top 10K holders by amount held.</span>
<span>Exports are limited to the top { limit } holders by amount held.</span>
</Flex>
);
}
if (!addressQuery.data) {
return null;
}
return (
<Flex mb={ 10 } whiteSpace="pre-wrap" flexWrap="wrap">
<span>Export { exportType.text } for address </span>
......@@ -176,7 +188,7 @@ const CsvExport = () => {
<span>{ nbsp }</span>
{ filterType && filterValue && <span>with applied filter by { filterType } ({ filterValue }) </span> }
<span>to CSV file. </span>
<span>Exports are limited to the last 10K { exportType.text }.</span>
<span>Exports are limited to the last { limit } { exportType.text }.</span>
</Flex>
);
})();
......
......@@ -64,7 +64,7 @@ const CustomAbiPage: React.FC = () => {
<Box display={{ base: 'block', lg: 'none' }}>
{ data?.map((item, index) => (
<CustomAbiListItem
key={ item.id + (isPlaceholderData ? index : '') }
key={ item.id + (isPlaceholderData ? String(index) : '') }
item={ item }
isLoading={ isPlaceholderData }
onDeleteClick={ onDeleteClick }
......
......@@ -49,33 +49,6 @@ test.describe('default view', () => {
});
});
test.describe('custom hero plate background', () => {
const IMAGE_URL = 'https://localhost:3000/my-image.png';
test.beforeEach(async({ mockEnvs }) => {
await mockEnvs([
[ 'NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND', `no-repeat center/cover url(${ IMAGE_URL })` ],
]);
});
test('default view', async({ render, page }) => {
await page.route(IMAGE_URL, (route) => {
return route.fulfill({
status: 200,
path: './playwright/mocks/image_long.jpg',
});
});
const component = await render(<Home/>);
const heroPlate = component.locator('div[data-label="hero plate"]');
await expect(heroPlate).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ],
maskColor: pwConfig.maskColor,
});
});
});
// had to separate mobile test, otherwise all the tests fell on CI
test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
......
import { Box, Flex, Heading } from '@chakra-ui/react';
import { Box, Flex } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import HeroBanner from 'ui/home/HeroBanner';
import ChainIndicators from 'ui/home/indicators/ChainIndicators';
import LatestArbitrumL2Batches from 'ui/home/latestBatches/LatestArbitrumL2Batches';
import LatestZkEvmL2Batches from 'ui/home/latestBatches/LatestZkEvmL2Batches';
......@@ -9,50 +10,13 @@ import LatestBlocks from 'ui/home/LatestBlocks';
import Stats from 'ui/home/Stats';
import Transactions from 'ui/home/Transactions';
import AdBanner from 'ui/shared/ad/AdBanner';
import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
import SearchBar from 'ui/snippets/searchBar/SearchBar';
import WalletMenuDesktop from 'ui/snippets/walletMenu/WalletMenuDesktop';
const rollupFeature = config.features.rollup;
const Home = () => {
return (
<Box as="main">
<Flex
w="100%"
background={ config.UI.homepage.plate.background }
borderRadius="md"
p={{ base: 4, lg: 8 }}
columnGap={ 8 }
alignItems="center"
data-label="hero plate"
>
<Box flexGrow={ 1 }>
<Flex mb={{ base: 2, lg: 3 }} justifyContent="space-between" alignItems="center" columnGap={ 2 }>
<Heading
as="h1"
fontSize={{ base: '18px', lg: '30px' }}
lineHeight={{ base: '24px', lg: '36px' }}
fontWeight={{ base: 500, lg: 700 }}
color={ config.UI.homepage.plate.textColor }
>
{
config.meta.seo.enhancedDataEnabled ?
`${ config.chain.name } blockchain explorer` :
`${ config.chain.name } explorer`
}
</Heading>
{ config.UI.navigation.layout === 'vertical' && (
<Box display={{ base: 'none', lg: 'flex' }}>
{ config.features.account.isEnabled && <ProfileMenuDesktop isHomePage/> }
{ config.features.blockchainInteraction.isEnabled && <WalletMenuDesktop isHomePage/> }
</Box>
) }
</Flex>
<SearchBar isHomepage/>
</Box>
<AdBanner platform="mobile" w="fit-content" flexShrink={ 0 } borderRadius="md" overflow="hidden" display={{ base: 'none', lg: 'block ' }}/>
</Flex>
<HeroBanner/>
<Flex flexDir={{ base: 'column', lg: 'row' }} columnGap={ 2 } rowGap={ 1 } mt={ 3 } _empty={{ mt: 0 }}>
<Stats/>
<ChainIndicators/>
......
......@@ -57,7 +57,7 @@ const NameDomain = () => {
flexWrap={{ base: 'wrap', lg: 'nowrap' }}
>
<EnsEntity
name={ domainName }
domain={ domainName }
protocol={ infoQuery.data?.protocol }
isLoading={ isLoading }
noLink
......
import React from 'react';
import { data as depositsData } from 'mocks/l2deposits/deposits';
import { data as depositsData } from 'mocks/optimism/deposits';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';
......
import React from 'react';
import { data as disputeGamesData } from 'mocks/l2disputeGames/disputeGames';
import { data as disputeGamesData } from 'mocks/optimism/disputeGames';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';
......
import React from 'react';
import { outputRootsData } from 'mocks/l2outputRoots/outputRoots';
import { outputRootsData } from 'mocks/optimism/outputRoots';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';
......
import { useRouter } from 'next/router';
import React from 'react';
import type { RoutedTab } from 'ui/shared/Tabs/types';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
import throwOnAbsentParamError from 'lib/errors/throwOnAbsentParamError';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useIsMobile from 'lib/hooks/useIsMobile';
import getQueryParamString from 'lib/router/getQueryParamString';
import { BLOCK } from 'stubs/block';
import { L2_TXN_BATCH } from 'stubs/L2';
import { TX } from 'stubs/tx';
import { generateListStub } from 'stubs/utils';
import BlocksContent from 'ui/blocks/BlocksContent';
import TextAd from 'ui/shared/ad/TextAd';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
import OptimisticL2TxnBatchDetails from 'ui/txnBatches/optimisticL2/OptimisticL2TxnBatchDetails';
import TxsWithFrontendSorting from 'ui/txs/TxsWithFrontendSorting';
const TAB_LIST_PROPS = {
marginBottom: 0,
py: 5,
marginTop: -5,
};
const TABS_HEIGHT = 80;
const OptimisticL2TxnBatch = () => {
const router = useRouter();
const appProps = useAppContext();
const number = getQueryParamString(router.query.number);
const tab = getQueryParamString(router.query.tab);
const isMobile = useIsMobile();
const batchQuery = useApiQuery('optimistic_l2_txn_batch', {
pathParams: { number },
queryOptions: {
enabled: Boolean(number),
placeholderData: L2_TXN_BATCH,
},
});
const batchTxsQuery = useQueryWithPages({
resourceName: 'optimistic_l2_txn_batch_txs',
pathParams: { number },
options: {
enabled: Boolean(!batchQuery.isPlaceholderData && batchQuery.data?.internal_id && tab === 'txs'),
placeholderData: generateListStub<'optimistic_l2_txn_batch_txs'>(TX, 50, { next_page_params: {
block_number: 1338932,
index: 1,
items_count: 50,
} }),
},
});
const batchBlocksQuery = useQueryWithPages({
resourceName: 'optimistic_l2_txn_batch_blocks',
pathParams: { number },
options: {
enabled: Boolean(!batchQuery.isPlaceholderData && batchQuery.data?.internal_id && tab === 'blocks'),
placeholderData: generateListStub<'optimistic_l2_txn_batch_blocks'>(BLOCK, 50, { next_page_params: {
batch_number: 1338932,
items_count: 50,
} }),
},
});
throwOnAbsentParamError(number);
throwOnResourceLoadError(batchQuery);
let pagination;
if (tab === 'txs') {
pagination = batchTxsQuery.pagination;
}
if (tab === 'blocks') {
pagination = batchBlocksQuery.pagination;
}
const hasPagination = !isMobile && pagination?.isVisible;
const tabs: Array<RoutedTab> = React.useMemo(() => ([
{ id: 'index', title: 'Details', component: <OptimisticL2TxnBatchDetails query={ batchQuery }/> },
{
id: 'txs',
title: 'Transactions',
component: <TxsWithFrontendSorting query={ batchTxsQuery } showSocketInfo={ false } top={ hasPagination ? TABS_HEIGHT : 0 }/>,
},
{
id: 'blocks',
title: 'Blocks',
component: <BlocksContent type="block" query={ batchBlocksQuery } enableSocket={ false } top={ hasPagination ? TABS_HEIGHT : 0 }/>,
},
].filter(Boolean)), [ batchQuery, batchTxsQuery, batchBlocksQuery, hasPagination ]);
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.endsWith('/batches');
if (!hasGoBackLink) {
return;
}
return {
label: 'Back to tx batches list',
url: appProps.referrer,
};
}, [ appProps.referrer ]);
return (
<>
<TextAd mb={ 6 }/>
<PageTitle
title={ `Batch #${ number }` }
backLink={ backLink }
/>
{ batchQuery.isPlaceholderData ?
<TabsSkeleton tabs={ tabs }/> : (
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ hasPagination && pagination ? <Pagination { ...(pagination) }/> : null }
stickyEnabled={ hasPagination }
/>
) }
</>
);
};
export default OptimisticL2TxnBatch;
import React from 'react';
import { txnBatchesData } from 'mocks/l2txnBatches/txnBatches';
import { txnBatchesData } from 'mocks/optimism/txnBatches';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';
......
......@@ -23,7 +23,7 @@ const OptimisticL2TxnBatches = () => {
{
next_page_params: {
items_count: 50,
block_number: 9045200,
id: 9045200,
},
},
),
......@@ -41,7 +41,7 @@ const OptimisticL2TxnBatches = () => {
<Show below="lg" ssr={ false }>
{ data.items.map(((item, index) => (
<OptimisticL2TxnBatchesListItem
key={ item.l2_block_number + (isPlaceholderData ? String(index) : '') }
key={ item.internal_id + (isPlaceholderData ? String(index) : '') }
item={ item }
isLoading={ isPlaceholderData }
/>
......@@ -61,8 +61,8 @@ const OptimisticL2TxnBatches = () => {
return (
<Skeleton isLoaded={ !countersQuery.isPlaceholderData && !isPlaceholderData } display="flex" flexWrap="wrap">
Tx batch (L2 block)
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].l2_block_number } </Text>to
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].l2_block_number } </Text>
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].internal_id } </Text>to
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].internal_id } </Text>
(total of { countersQuery.data?.toLocaleString() } batches)
</Skeleton>
);
......
import React from 'react';
import { data as withdrawalsData } from 'mocks/l2withdrawals/withdrawals';
import { data as withdrawalsData } from 'mocks/optimism/withdrawals';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';
......
......@@ -51,7 +51,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisibl
const isEdit = data?.id;
if (isEdit) {
return apiFetch('private_tags_address', {
pathParams: { id: data.id },
pathParams: { id: String(data.id) },
fetchParams: { method: 'PUT', body },
});
}
......
......@@ -34,7 +34,7 @@ const AddressTagTable = ({ data, onDeleteClick, onEditClick, isLoading, top }: P
{ data?.map((item: AddressTag, index: number) => (
<AddressTagTableItem
item={ item }
key={ item.id + (isLoading ? index : '') }
key={ item.id + (isLoading ? String(index) : '') }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
isLoading={ isLoading }
......
......@@ -25,7 +25,7 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, data, type })
const mutationFn = useCallback(() => {
const resourceName = type === 'address' ? 'private_tags_address' : 'private_tags_tx';
return apiFetch(resourceName, {
pathParams: { id: data.id },
pathParams: { id: String(data.id) },
fetchParams: { method: 'DELETE' },
});
}, [ type, apiFetch, data.id ]);
......
......@@ -61,7 +61,7 @@ const PrivateAddressTags = () => {
{ addressTagsData?.items.map((item: AddressTag, index: number) => (
<AddressTagListItem
item={ item }
key={ item.id + (isPlaceholderData ? index : '') }
key={ item.id + (isPlaceholderData ? String(index) : '') }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
isLoading={ isPlaceholderData }
......
......@@ -62,7 +62,7 @@ const PrivateTransactionTags = () => {
<Box display={{ base: 'block', lg: 'none' }}>
{ transactionTagsData?.items.map((item, index) => (
<TransactionTagListItem
key={ item.id + (isPlaceholderData ? index : '') }
key={ item.id + (isPlaceholderData ? String(index) : '') }
item={ item }
isLoading={ isPlaceholderData }
onDeleteClick={ onDeleteClick }
......
......@@ -55,7 +55,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVi
if (isEdit) {
return apiFetch('private_tags_tx', {
pathParams: { id: data.id },
pathParams: { id: String(data.id) },
fetchParams: { method: 'PUT', body },
});
}
......
......@@ -33,7 +33,7 @@ const AddressTagTable = ({ data, isLoading, onDeleteClick, onEditClick, top }: P
<Tbody>
{ data?.map((item, index) => (
<TransactionTagTableItem
key={ item.id + (isLoading ? index : '') }
key={ item.id + (isLoading ? String(index) : '') }
item={ item }
isLoading={ isLoading }
onDeleteClick={ onDeleteClick }
......
......@@ -344,10 +344,10 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
<Text
overflow="hidden"
textOverflow="ellipsis"
sx={{
style={{
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
display: '-webkit-box',
'-webkit-box-orient': 'vertical',
'-webkit-line-clamp': '3',
}}
>
{ data.app.description }
......
......@@ -15,15 +15,16 @@ interface Props {
data: TEntityTag;
isLoading?: boolean;
maxW?: ResponsiveValue<string>;
noLink?: boolean;
}
const EntityTag = ({ data, isLoading, maxW }: Props) => {
const EntityTag = ({ data, isLoading, maxW, noLink }: Props) => {
if (isLoading) {
return <Skeleton borderRadius="sm" w="100px" h="24px"/>;
}
const hasLink = Boolean(getTagLinkParams(data));
const hasLink = !noLink && Boolean(getTagLinkParams(data));
const iconColor = data.meta?.textColor ?? 'gray.400';
const name = (() => {
......@@ -63,7 +64,7 @@ const EntityTag = ({ data, isLoading, maxW }: Props) => {
colorScheme={ hasLink ? 'gray-blue' : 'gray' }
_hover={ hasLink ? { opacity: 0.76 } : undefined }
>
<EntityTagLink data={ data }>
<EntityTagLink data={ data } noLink={ noLink }>
{ icon }
<TruncatedValue value={ name } tooltipPlacement="top"/>
</EntityTagLink>
......
......@@ -11,11 +11,12 @@ import { getTagLinkParams } from './utils';
interface Props {
data: EntityTag;
children: React.ReactNode;
noLink?: boolean;
}
const EntityTagLink = ({ data, children }: Props) => {
const EntityTagLink = ({ data, children, noLink }: Props) => {
const linkParams = getTagLinkParams(data);
const linkParams = !noLink ? getTagLinkParams(data) : undefined;
const handleLinkClick = React.useCallback(() => {
if (!linkParams?.href) {
......
import type { EntityTag } from './types';
// import { route } from 'nextjs-routes';
import { route } from 'nextjs-routes';
export function getTagLinkParams(data: EntityTag): { type: 'external' | 'internal'; href: string } | undefined {
if (data.meta?.warpcastHandle) {
......@@ -17,11 +17,10 @@ export function getTagLinkParams(data: EntityTag): { type: 'external' | 'interna
};
}
// Uncomment this block when "Tag search" page is ready - issue #1869
// if (data.tagType === 'generic' || data.tagType === 'protocol') {
// return {
// type: 'internal',
// href: route({ pathname: '/' }),
// };
// }
if (data.tagType === 'generic' || data.tagType === 'protocol') {
return {
type: 'internal',
href: route({ pathname: '/accounts/label/[slug]', query: { slug: data.slug, tagType: data.tagType, tagName: data.name } }),
};
}
}
......@@ -10,9 +10,10 @@ import TokenLogoPlaceholder from './TokenLogoPlaceholder';
type Props = {
isLoading?: boolean;
className?: string;
type?: 'primary' | 'secondary';
}
const NativeTokenIcon = (props: Props) => {
const NativeTokenIcon = ({ isLoading, className, type }: Props) => {
const statsQueryResult = useApiQuery('stats', {
queryOptions: {
refetchOnMount: false,
......@@ -20,18 +21,20 @@ const NativeTokenIcon = (props: Props) => {
},
});
if (props.isLoading || statsQueryResult.isPlaceholderData) {
return <Skeleton borderRadius="base" className={ props.className }/>;
if (isLoading || statsQueryResult.isPlaceholderData) {
return <Skeleton borderRadius="base" className={ className }/>;
}
const src = type === 'secondary' ? statsQueryResult.data?.secondary_coin_image : statsQueryResult.data?.coin_image;
return (
<Image
borderRadius="base"
className={ props.className }
src={ statsQueryResult.data?.coin_image || '' }
className={ className }
src={ src || '' }
alt={ `${ config.chain.currency.symbol } logo` }
fallback={ <TokenLogoPlaceholder borderRadius="base" className={ props.className }/> }
fallbackStrategy={ statsQueryResult.data?.coin_image ? 'onError' : 'beforeLoadOrError' }
fallback={ <TokenLogoPlaceholder borderRadius="base" className={ className }/> }
fallbackStrategy={ src ? 'onError' : 'beforeLoadOrError' }
/>
);
};
......
......@@ -60,7 +60,7 @@ const DefaultView = () => {
beforeTitle={ (
<TokenEntity.Icon
token={ tokenData }
iconSize="lg"
size="lg"
/>
) }
backLink={ backLink }
......
......@@ -52,7 +52,7 @@ const LongNameAndManyTags = () => {
beforeTitle={ (
<TokenEntity.Icon
token={ tokenData }
iconSize="lg"
size="lg"
/>
) }
contentAfter={ contentAfter }
......
......@@ -19,9 +19,22 @@ export default function useScrollToActiveTab({ activeTabIndex, tabsRefs, listRef
const activeTabRef = tabsRefs[activeTabIndex];
if (activeTabRef.current && listRef.current) {
const activeTabRect = activeTabRef.current.getBoundingClientRect();
const containerWidth = listRef.current.getBoundingClientRect().width;
const activeTabWidth = activeTabRef.current.getBoundingClientRect().width;
const left = tabsRefs.slice(0, activeTabIndex)
.map((tab) => tab.current?.getBoundingClientRect())
.filter(Boolean)
.map((rect) => rect.width)
.reduce((result, item) => result + item, 0);
const isWithinFirstPage = containerWidth > left + activeTabWidth;
if (isWithinFirstPage) {
return;
}
listRef.current.scrollTo({
left: activeTabRect.left,
left,
behavior: 'smooth',
});
}
......
......@@ -29,7 +29,7 @@ const NftTokenTransferSnippet = ({ value, token, tokenId }: Props) => {
hash={ token.address }
id={ tokenId }
fontWeight={ 600 }
iconSize="md"
icon={{ size: 'md' }}
maxW={{ base: '100%', lg: '150px' }}
w="auto"
flexShrink={ 0 }
......
import React from 'react';
import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2';
import type { ExcludeUndefined } from 'types/utils';
import Tag from 'ui/shared/chakra/Tag';
export interface Props {
container: ExcludeUndefined<OptimisticL2TxnBatchesItem['batch_data_container']>;
isLoading?: boolean;
}
const OptimisticL2TxnBatchDA = ({ container, isLoading }: Props) => {
const text = (() => {
switch (container) {
case 'in_blob4844':
return 'EIP-4844 blob';
case 'in_calldata':
return 'Calldata';
case 'in_celestia':
return 'Celestia blob';
}
})();
return (
<Tag colorScheme="yellow" isLoading={ isLoading }>
{ text }
</Tag>
);
};
export default React.memo(OptimisticL2TxnBatchDA);
......@@ -9,7 +9,7 @@ import { test, expect } from 'playwright/lib';
import AddressEntity from './AddressEntity';
const iconSizes = [ 'md', 'lg' ];
const iconSizes = [ 'md', 'lg' ] as const;
test.use({ viewport: { width: 180, height: 140 } });
......@@ -19,7 +19,7 @@ test.describe('icon size', () => {
const component = await render(
<AddressEntity
address={ addressMock.withoutName }
iconSize={ size }
icon={{ size }}
/>,
);
......@@ -69,7 +69,7 @@ test.describe('proxy contract', () => {
test('without implementation name', async({ render, page }) => {
const component = await render(
<AddressEntity
address={{ ...addressMock.contract, implementations: [ { address: addressMock.contract.implementations?.[0].address } ] }}
address={{ ...addressMock.contract, implementations: [ { address: addressMock.contract.implementations?.[0].address as string } ] }}
/>,
);
......@@ -81,7 +81,7 @@ test.describe('proxy contract', () => {
test('without any name', async({ render, page }) => {
const component = await render(
<AddressEntity
address={{ ...addressMock.contract, name: undefined, implementations: [ { address: addressMock.contract.implementations?.[0].address } ] }}
address={{ ...addressMock.contract, name: undefined, implementations: [ { address: addressMock.contract.implementations?.[0].address as string } ] }}
/>,
);
......
import type { As } from '@chakra-ui/react';
import { Box, Flex, Skeleton, Tooltip, chakra, VStack } from '@chakra-ui/react';
import _omit from 'lodash/omit';
import React from 'react';
import type { AddressParam } from 'types/api/addressParams';
......@@ -10,7 +9,7 @@ import { route } from 'nextjs-routes';
import { useAddressHighlightContext } from 'lib/contexts/addressHighlight';
import * as EntityBase from 'ui/shared/entities/base/components';
import { getIconProps } from '../base/utils';
import { distributeEntityProps, getIconProps } from '../base/utils';
import AddressEntityContentProxy from './AddressEntityContentProxy';
import AddressIdenticon from './AddressIdenticon';
......@@ -29,10 +28,7 @@ const Link = chakra((props: LinkProps) => {
);
});
type IconProps = Omit<EntityBase.IconBaseProps, 'name'> & Pick<EntityProps, 'address' | 'isSafeAddress'> & {
asProp?: As;
name?: EntityBase.IconBaseProps['name'];
};
type IconProps = Pick<EntityProps, 'address' | 'isSafeAddress'> & EntityBase.IconBaseProps;
const Icon = (props: IconProps) => {
if (props.noIcon) {
......@@ -40,8 +36,8 @@ const Icon = (props: IconProps) => {
}
const styles = {
...getIconProps(props.iconSize),
marginRight: 2,
...getIconProps(props.size),
marginRight: props.marginRight ?? 2,
};
if (props.isLoading) {
......@@ -80,7 +76,7 @@ const Icon = (props: IconProps) => {
return (
<Flex marginRight={ styles.marginRight }>
<AddressIdenticon
size={ props.iconSize === 'lg' ? 30 : 20 }
size={ props.size === 'lg' ? 30 : 20 }
hash={ props.address.hash }
/>
</Flex>
......@@ -137,18 +133,18 @@ const Copy = (props: CopyProps) => {
const Container = EntityBase.Container;
interface AddressProp extends Partial<AddressParam> {
hash: string;
}
export interface EntityProps extends EntityBase.EntityBaseProps {
address: Pick<AddressParam,
'hash' | 'name' | 'is_contract' | 'is_verified' | 'implementations' | 'ens_domain_name' | 'metadata'
>;
address: AddressProp;
isSafeAddress?: boolean;
noHighlight?: boolean;
}
const AddressEntry = (props: EntityProps) => {
const linkProps = _omit(props, [ 'className' ]);
const partsProps = _omit(props, [ 'className', 'onClick' ]);
const partsProps = distributeEntityProps(props);
const context = useAddressHighlightContext(props.noHighlight);
return (
......@@ -162,16 +158,16 @@ const AddressEntry = (props: EntityProps) => {
position="relative"
zIndex={ 0 }
>
<Icon { ...partsProps } color={ props.iconColor }/>
<Link { ...linkProps }>
<Content { ...partsProps }/>
<Icon { ...partsProps.icon }/>
<Link { ...partsProps.link }>
<Content { ...partsProps.content }/>
</Link>
<Copy { ...partsProps }/>
<Copy { ...partsProps.copy }/>
</Container>
);
};
export default React.memo(chakra(AddressEntry));
export default React.memo(chakra<As, EntityProps>(AddressEntry));
export {
Container,
......
......@@ -18,9 +18,7 @@ export type Truncation = 'constant' | 'constant_long' | 'dynamic' | 'tail' | 'no
export interface EntityBaseProps {
className?: string;
href?: string;
iconName?: IconName;
iconSize?: IconSize;
iconColor?: IconProps['color'];
icon?: EntityIconProps;
isExternal?: boolean;
isLoading?: boolean;
noCopy?: boolean;
......@@ -81,28 +79,30 @@ const Link = chakra(({ isLoading, children, isExternal, onClick, href, noLink }:
);
});
export interface IconBaseProps extends Pick<EntityBaseProps, 'isLoading' | 'iconSize' | 'noIcon'> {
name: IconName;
color?: IconProps['color'];
borderRadius?: IconProps['borderRadius'];
interface EntityIconProps extends Pick<IconProps, 'color' | 'borderRadius' | 'marginRight' | 'boxSize'> {
name?: IconName;
size?: IconSize;
}
const Icon = ({ isLoading, iconSize, noIcon, name, color, borderRadius }: IconBaseProps) => {
export interface IconBaseProps extends Pick<EntityBaseProps, 'isLoading' | 'noIcon'>, EntityIconProps {
}
const Icon = ({ isLoading, noIcon, size, name, color, borderRadius, marginRight, boxSize }: IconBaseProps) => {
const defaultColor = useColorModeValue('gray.500', 'gray.400');
if (noIcon) {
if (noIcon || !name) {
return null;
}
const styles = getIconProps(iconSize);
const styles = getIconProps(size);
return (
<IconSvg
name={ name }
boxSize={ styles.boxSize }
boxSize={ boxSize ?? styles.boxSize }
isLoading={ isLoading }
borderRadius={ borderRadius ?? 'base' }
display="block"
mr={ 2 }
mr={ marginRight ?? 2 }
color={ color ?? defaultColor }
minW={ 0 }
flexShrink={ 0 }
......
import type { EntityBaseProps } from './components';
export type IconSize = 'md' | 'lg';
export function getIconProps(size: IconSize = 'md') {
......@@ -14,3 +16,16 @@ export function getIconProps(size: IconSize = 'md') {
}
}
}
export function distributeEntityProps<Props extends EntityBaseProps>(props: Props) {
const { className, onClick, icon, ...restProps } = props;
return {
container: { className },
icon: { ...restProps, ...icon },
link: { ...restProps, onClick },
content: restProps,
symbol: restProps,
copy: restProps,
};
}
import type { As } from '@chakra-ui/react';
import { chakra } from '@chakra-ui/react';
import _omit from 'lodash/omit';
import React from 'react';
import { route } from 'nextjs-routes';
import * as EntityBase from 'ui/shared/entities/base/components';
import { distributeEntityProps } from '../base/utils';
type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'hash'>;
const Link = chakra((props: LinkProps) => {
......@@ -21,11 +23,7 @@ const Link = chakra((props: LinkProps) => {
);
});
type IconProps = Omit<EntityBase.IconBaseProps, 'name'> & {
name?: EntityBase.IconBaseProps['name'];
};
const Icon = (props: IconProps) => {
const Icon = (props: EntityBase.IconBaseProps) => {
return (
<EntityBase.Icon
{ ...props }
......@@ -65,21 +63,20 @@ export interface EntityProps extends EntityBase.EntityBaseProps {
}
const BlobEntity = (props: EntityProps) => {
const linkProps = _omit(props, [ 'className' ]);
const partsProps = _omit(props, [ 'className', 'onClick' ]);
const partsProps = distributeEntityProps(props);
return (
<Container className={ props.className }>
<Icon { ...partsProps }/>
<Link { ...linkProps }>
<Content { ...partsProps }/>
<Container { ...partsProps.container }>
<Icon { ...partsProps.icon }/>
<Link { ...partsProps.link }>
<Content { ...partsProps.content }/>
</Link>
<Copy { ...partsProps }/>
<Copy { ...partsProps.copy }/>
</Container>
);
};
export default React.memo(chakra(BlobEntity));
export default React.memo(chakra<As, EntityProps>(BlobEntity));
export {
Container,
......
import { chakra } from '@chakra-ui/react';
import React from 'react';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import * as BlobEntity from './BlobEntity';
const rollupFeature = config.features.rollup;
const BlobEntityL1 = (props: BlobEntity.EntityProps) => {
if (!rollupFeature.isEnabled) {
return null;
}
const defaultHref = rollupFeature.L1BaseUrl + route({
pathname: '/blobs/[hash]',
query: { hash: props.hash },
});
return (
<BlobEntity.default { ...props } href={ props.href ?? defaultHref } isExternal/>
);
};
export default chakra(BlobEntityL1);
import { chakra } from '@chakra-ui/react';
import _omit from 'lodash/omit';
import React from 'react';
import { route } from 'nextjs-routes';
......@@ -11,23 +10,18 @@ import * as BlockEntity from './BlockEntity';
const rollupFeature = config.features.rollup;
const BatchEntityL2 = (props: BlockEntity.EntityProps) => {
const linkProps = _omit(props, [ 'className' ]);
const partsProps = _omit(props, [ 'className', 'onClick' ]);
if (!rollupFeature.isEnabled) {
return null;
}
const defaultHref = route({ pathname: '/batches/[number]', query: { number: props.number.toString() } });
return (
<BlockEntity.Container className={ props.className }>
<BlockEntity.Icon { ...partsProps } name="txn_batches_slim"/>
<BlockEntity.Link
{ ...linkProps }
href={ route({ pathname: '/batches/[number]', query: { number: props.number.toString() } }) }
>
<BlockEntity.Content { ...partsProps }/>
</BlockEntity.Link>
</BlockEntity.Container>
<BlockEntity.default
{ ...props }
href={ props.href ?? defaultHref }
icon={{ name: 'txn_batches_slim' }}
/>
);
};
......
......@@ -4,7 +4,7 @@ import { test, expect } from 'playwright/lib';
import BlockEntity from './BlockEntity';
const iconSizes = [ 'md', 'lg' ];
const iconSizes = [ 'md', 'lg' ] as const;
test.use({ viewport: { width: 180, height: 30 } });
......@@ -14,7 +14,7 @@ test.describe('icon sizes', () => {
const component = await render(
<BlockEntity
number={ 17943507 }
iconSize={ size }
icon={{ size }}
/>,
);
......
import type { As } from '@chakra-ui/react';
import { chakra } from '@chakra-ui/react';
import _omit from 'lodash/omit';
import React from 'react';
import { route } from 'nextjs-routes';
import * as EntityBase from 'ui/shared/entities/base/components';
import { distributeEntityProps } from '../base/utils';
type LinkProps = EntityBase.LinkBaseProps & Partial<Pick<EntityProps, 'hash' | 'number'>>;
const Link = chakra((props: LinkProps) => {
......@@ -22,11 +24,7 @@ const Link = chakra((props: LinkProps) => {
);
});
type IconProps = Omit<EntityBase.IconBaseProps, 'name'> & {
name?: EntityBase.IconBaseProps['name'];
};
const Icon = (props: IconProps) => {
const Icon = (props: EntityBase.IconBaseProps) => {
return (
<EntityBase.Icon
{ ...props }
......@@ -55,20 +53,19 @@ export interface EntityProps extends EntityBase.EntityBaseProps {
}
const BlockEntity = (props: EntityProps) => {
const linkProps = _omit(props, [ 'className' ]);
const partsProps = _omit(props, [ 'className', 'onClick' ]);
const partsProps = distributeEntityProps(props);
return (
<Container className={ props.className }>
<Icon { ...partsProps }/>
<Link { ...linkProps }>
<Content { ...partsProps }/>
<Container { ...partsProps.container }>
<Icon { ...partsProps.icon }/>
<Link { ...partsProps.link }>
<Content { ...partsProps.content }/>
</Link>
</Container>
);
};
export default React.memo(chakra(BlockEntity));
export default React.memo(chakra<As, EntityProps>(BlockEntity));
export {
Container,
......
import { chakra } from '@chakra-ui/react';
import _omit from 'lodash/omit';
import React from 'react';
import { route } from 'nextjs-routes';
......@@ -11,25 +10,16 @@ import * as BlockEntity from './BlockEntity';
const rollupFeature = config.features.rollup;
const BlockEntityL1 = (props: BlockEntity.EntityProps) => {
const linkProps = _omit(props, [ 'className' ]);
const partsProps = _omit(props, [ 'className', 'onClick' ]);
if (!rollupFeature.isEnabled) {
return null;
}
return (
<BlockEntity.Container className={ props.className }>
<BlockEntity.Icon { ...partsProps }/>
<BlockEntity.Link
{ ...linkProps }
isExternal
href={ rollupFeature.L1BaseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: props.hash ?? String(props.number) } }) }
>
<BlockEntity.Content { ...partsProps }/>
</BlockEntity.Link>
</BlockEntity.Container>
);
const defaultHref = rollupFeature.L1BaseUrl + route({
pathname: '/block/[height_or_hash]',
query: { height_or_hash: props.hash ?? String(props.number) },
});
return <BlockEntity.default { ...props } href={ props.href ?? defaultHref } isExternal/>;
};
export default chakra(BlockEntityL1);
import { chakra } from '@chakra-ui/react';
import _omit from 'lodash/omit';
import React from 'react';
import config from 'configs/app';
......@@ -9,21 +8,11 @@ import * as BlockEntity from './BlockEntity';
const rollupFeature = config.features.rollup;
const BlockEntityL2 = (props: BlockEntity.EntityProps) => {
const linkProps = _omit(props, [ 'className' ]);
const partsProps = _omit(props, [ 'className', 'onClick' ]);
if (!rollupFeature.isEnabled) {
return null;
}
return (
<BlockEntity.Container className={ props.className }>
<BlockEntity.Icon { ...partsProps } name="txn_batches_slim"/>
<BlockEntity.Link { ...linkProps }>
<BlockEntity.Content { ...partsProps }/>
</BlockEntity.Link>
</BlockEntity.Container>
);
return <BlockEntity.default { ...props } icon={{ name: 'txn_batches_slim' }}/>;
};
export default chakra(BlockEntityL2);
......@@ -6,7 +6,7 @@ import { test, expect } from 'playwright/lib';
import EnsEntity from './EnsEntity';
const name = 'cat.eth';
const iconSizes = [ 'md', 'lg' ];
const iconSizes = [ 'md', 'lg' ] as const;
test.use({ viewport: { width: 180, height: 30 } });
......@@ -15,8 +15,8 @@ test.describe('icon size', () => {
test(size, async({ render }) => {
const component = await render(
<EnsEntity
name={ name }
iconSize={ size }
domain={ name }
icon={{ size }}
/>,
);
......@@ -28,7 +28,7 @@ test.describe('icon size', () => {
test('loading', async({ render }) => {
const component = await render(
<EnsEntity
name={ name }
domain={ name }
isLoading
/>,
);
......@@ -39,7 +39,7 @@ test('loading', async({ render }) => {
test('with long name', async({ render }) => {
const component = await render(
<EnsEntity
name="kitty.kitty.kitty.cat.eth"
domain="kitty.kitty.kitty.cat.eth"
/>,
);
......@@ -51,7 +51,7 @@ test('with long name', async({ render }) => {
test('customization', async({ render }) => {
const component = await render(
<EnsEntity
name={ name }
domain={ name }
p={ 3 }
borderWidth="1px"
borderColor="blue.700"
......@@ -68,7 +68,7 @@ test.describe('', () => {
const component = await render(
<EnsEntity
name={ name }
domain={ name }
protocol={ domainMock.protocolA }
/>,
);
......
import type { As } from '@chakra-ui/react';
import { Box, chakra, Flex, Image, PopoverBody, PopoverContent, PopoverTrigger, Portal, Skeleton, Text } from '@chakra-ui/react';
import _omit from 'lodash/omit';
import React from 'react';
import type * as bens from '@blockscout/bens-types';
......@@ -12,12 +12,12 @@ import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/links/LinkExternal';
import TruncatedValue from 'ui/shared/TruncatedValue';
import { getIconProps } from '../base/utils';
import { distributeEntityProps, getIconProps } from '../base/utils';
type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'name'>;
type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'domain'>;
const Link = chakra((props: LinkProps) => {
const defaultHref = route({ pathname: '/name-domains/[name]', query: { name: props.name } });
const defaultHref = route({ pathname: '/name-domains/[name]', query: { name: props.domain } });
return (
<EntityBase.Link
......@@ -29,15 +29,13 @@ const Link = chakra((props: LinkProps) => {
);
});
type IconProps = Omit<EntityBase.IconBaseProps, 'name'> & Pick<EntityProps, 'protocol'> & {
iconName?: EntityBase.IconBaseProps['name'];
};
type IconProps = Pick<EntityProps, 'protocol'> & EntityBase.IconBaseProps;
const Icon = (props: IconProps) => {
const icon = <EntityBase.Icon { ...props } name={ props.iconName ?? 'ENS_slim' }/>;
const icon = <EntityBase.Icon { ...props } name={ props.name ?? 'ENS_slim' }/>;
if (props.protocol) {
const styles = getIconProps(props.iconSize);
const styles = getIconProps(props.size);
if (props.isLoading) {
return <Skeleton boxSize={ styles.boxSize } borderRadius="sm" mr={ 2 }/>;
......@@ -98,24 +96,24 @@ const Icon = (props: IconProps) => {
return icon;
};
type ContentProps = Omit<EntityBase.ContentBaseProps, 'text'> & Pick<EntityProps, 'name'>;
type ContentProps = Omit<EntityBase.ContentBaseProps, 'text'> & Pick<EntityProps, 'domain'>;
const Content = chakra((props: ContentProps) => {
return (
<TruncatedValue
isLoading={ props.isLoading }
value={ props.name }
value={ props.domain }
/>
);
});
type CopyProps = Omit<EntityBase.CopyBaseProps, 'text'> & Pick<EntityProps, 'name'>;
type CopyProps = Omit<EntityBase.CopyBaseProps, 'text'> & Pick<EntityProps, 'domain'>;
const Copy = (props: CopyProps) => {
return (
<EntityBase.Copy
{ ...props }
text={ props.name }
text={ props.domain }
/>
);
};
......@@ -123,26 +121,25 @@ const Copy = (props: CopyProps) => {
const Container = EntityBase.Container;
export interface EntityProps extends EntityBase.EntityBaseProps {
name: string;
domain: string;
protocol?: bens.ProtocolInfo | null;
}
const EnsEntity = (props: EntityProps) => {
const linkProps = _omit(props, [ 'className' ]);
const partsProps = _omit(props, [ 'className', 'onClick' ]);
const partsProps = distributeEntityProps(props);
return (
<Container className={ props.className }>
<Icon { ...partsProps }/>
<Link { ...linkProps }>
<Content { ...partsProps }/>
<Container { ...partsProps.container }>
<Icon { ...partsProps.icon }/>
<Link { ...partsProps.link }>
<Content { ...partsProps.content }/>
</Link>
<Copy { ...partsProps }/>
<Copy { ...partsProps.copy }/>
</Container>
);
};
export default React.memo(chakra(EnsEntity));
export default React.memo(chakra<As, EntityProps>(EnsEntity));
export {
Container,
......
......@@ -4,7 +4,7 @@ import { test, expect } from 'playwright/lib';
import NftEntity from './NftEntity';
const iconSizes = [ 'md', 'lg' ];
const iconSizes = [ 'md', 'lg' ] as const;
const hash = '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859';
test.use({ viewport: { width: 180, height: 30 } });
......@@ -15,8 +15,8 @@ test.describe('icon sizes', () => {
const component = await render(
<NftEntity
hash={ hash }
id={ 1042 }
iconSize={ size }
id="1042"
icon={{ size }}
/>,
);
......@@ -29,7 +29,7 @@ test('loading', async({ render }) => {
const component = await render(
<NftEntity
hash={ hash }
id={ 1042 }
id="1042"
isLoading
/>,
);
......@@ -41,7 +41,7 @@ test('long id', async({ render }) => {
const component = await render(
<NftEntity
hash={ hash }
id={ 1794350723452223 }
id="1794350723452223"
/>,
);
......@@ -52,7 +52,7 @@ test('customization', async({ render }) => {
const component = await render(
<NftEntity
hash={ hash }
id={ 1042 }
id="1042"
p={ 3 }
borderWidth="1px"
borderColor="blue.700"
......
import type { As } from '@chakra-ui/react';
import { chakra } from '@chakra-ui/react';
import _omit from 'lodash/omit';
import React from 'react';
import { route } from 'nextjs-routes';
......@@ -7,13 +7,11 @@ import { route } from 'nextjs-routes';
import * as EntityBase from 'ui/shared/entities/base/components';
import TruncatedValue from 'ui/shared/TruncatedValue';
const Container = EntityBase.Container;
import { distributeEntityProps } from '../base/utils';
type IconProps = Pick<EntityProps, 'isLoading' | 'noIcon' | 'iconSize'> & {
name?: EntityBase.IconBaseProps['name'];
};
const Container = EntityBase.Container;
const Icon = (props: IconProps) => {
const Icon = (props: EntityBase.IconBaseProps) => {
if (props.noIcon) {
return null;
}
......@@ -21,7 +19,7 @@ const Icon = (props: IconProps) => {
return (
<EntityBase.Icon
{ ...props }
iconSize={ props.iconSize ?? 'lg' }
size={ props.size ?? 'lg' }
name={ props.name ?? 'nft_shield' }
/>
);
......@@ -59,20 +57,19 @@ export interface EntityProps extends EntityBase.EntityBaseProps {
}
const NftEntity = (props: EntityProps) => {
const linkProps = _omit(props, [ 'className' ]);
const partsProps = _omit(props, [ 'className', 'onClick' ]);
const partsProps = distributeEntityProps(props);
return (
<Container className={ props.className } w="100%">
<Icon { ...partsProps }/>
<Link { ...linkProps }>
<Content { ...partsProps }/>
<Container w="100%" { ...partsProps.container }>
<Icon { ...partsProps.icon }/>
<Link { ...partsProps.link }>
<Content { ...partsProps.content }/>
</Link>
</Container>
);
};
export default React.memo(chakra(NftEntity));
export default React.memo(chakra<As, EntityProps>(NftEntity));
export {
Container,
......
......@@ -6,7 +6,7 @@ import { test, expect } from 'playwright/lib';
import TokenEntity from './TokenEntity';
const iconSizes = [ 'md', 'lg' ];
const iconSizes = [ 'md', 'lg' ] as const;
test.use({ viewport: { width: 300, height: 100 } });
......@@ -16,7 +16,7 @@ test.describe('icon size', () => {
const component = await render(
<TokenEntity
token={ tokenMock.tokenInfo }
iconSize={ size }
icon={{ size }}
/>,
);
......@@ -37,6 +37,7 @@ test('with logo, long name and symbol', async({ page, render }) => {
await render(
<TokenEntity
token={{
type: 'ERC-20',
name: 'This token is the best token ever',
symbol: 'DUCK DUCK DUCK',
address: tokenMock.tokenInfo.address,
......
import type { ChakraProps } from '@chakra-ui/react';
import type { As } from '@chakra-ui/react';
import { Image, Skeleton, chakra } from '@chakra-ui/react';
import _omit from 'lodash/omit';
import React from 'react';
import type { TokenInfo } from 'types/api/token';
......@@ -11,7 +10,7 @@ import * as EntityBase from 'ui/shared/entities/base/components';
import TokenLogoPlaceholder from 'ui/shared/TokenLogoPlaceholder';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
import { getIconProps } from '../base/utils';
import { distributeEntityProps, getIconProps } from '../base/utils';
type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'token'>;
......@@ -28,10 +27,7 @@ const Link = chakra((props: LinkProps) => {
);
});
type IconProps = Pick<EntityProps, 'token' | 'isLoading' | 'iconSize' | 'noIcon' | 'className'> & {
marginRight?: ChakraProps['marginRight'];
boxSize?: ChakraProps['boxSize'];
};
type IconProps = Pick<EntityProps, 'token' | 'className'> & EntityBase.IconBaseProps;
const Icon = (props: IconProps) => {
if (props.noIcon) {
......@@ -40,7 +36,7 @@ const Icon = (props: IconProps) => {
const styles = {
marginRight: props.marginRight ?? 2,
boxSize: props.boxSize ?? getIconProps(props.iconSize).boxSize,
boxSize: props.boxSize ?? getIconProps(props.size).boxSize,
borderRadius: 'base',
flexShrink: 0,
};
......@@ -143,22 +139,21 @@ export interface EntityProps extends EntityBase.EntityBaseProps {
}
const TokenEntity = (props: EntityProps) => {
const linkProps = _omit(props, [ 'className' ]);
const partsProps = _omit(props, [ 'className', 'onClick' ]);
const partsProps = distributeEntityProps(props);
return (
<Container className={ props.className } w="100%">
<Icon { ...partsProps }/>
<Link { ...linkProps }>
<Content { ...partsProps }/>
<Container w="100%" { ...partsProps.container }>
<Icon { ...partsProps.icon }/>
<Link { ...partsProps.link }>
<Content { ...partsProps.content }/>
</Link>
<Symbol { ...partsProps }/>
<Copy { ...partsProps }/>
<Symbol { ...partsProps.symbol }/>
<Copy { ...partsProps.copy }/>
</Container>
);
};
export default React.memo(chakra(TokenEntity));
export default React.memo(chakra<As, EntityProps>(TokenEntity));
export {
Container,
......
......@@ -5,7 +5,7 @@ import { test, expect } from 'playwright/lib';
import TxEntity from './TxEntity';
const hash = '0x376db52955d5bce114d0ccea2dcf22289b4eae1b86bcae5a59bb5fdbfef48899';
const iconSizes = [ 'md', 'lg' ];
const iconSizes = [ 'md', 'lg' ] as const;
test.use({ viewport: { width: 180, height: 30 } });
......@@ -15,7 +15,7 @@ test.describe('icon size', () => {
const component = await render(
<TxEntity
hash={ hash }
iconSize={ size }
icon={{ size }}
/>,
);
......
import type { As } from '@chakra-ui/react';
import { chakra } from '@chakra-ui/react';
import _omit from 'lodash/omit';
import React from 'react';
import { route } from 'nextjs-routes';
import * as EntityBase from 'ui/shared/entities/base/components';
import { distributeEntityProps } from '../base/utils';
type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'hash'>;
const Link = chakra((props: LinkProps) => {
......@@ -21,11 +23,7 @@ const Link = chakra((props: LinkProps) => {
);
});
type IconProps = Omit<EntityBase.IconBaseProps, 'name'> & {
name?: EntityBase.IconBaseProps['name'];
};
const Icon = (props: IconProps) => {
const Icon = (props: EntityBase.IconBaseProps) => {
return (
<EntityBase.Icon
{ ...props }
......@@ -66,21 +64,20 @@ export interface EntityProps extends EntityBase.EntityBaseProps {
}
const TxEntity = (props: EntityProps) => {
const linkProps = _omit(props, [ 'className' ]);
const partsProps = _omit(props, [ 'className', 'onClick' ]);
const partsProps = distributeEntityProps(props);
return (
<Container className={ props.className }>
<Icon { ...partsProps } name={ props.iconName } color={ props.iconColor }/>
<Link { ...linkProps }>
<Content { ...partsProps }/>
<Container { ...partsProps.container }>
<Icon { ...partsProps.icon }/>
<Link { ...partsProps.link }>
<Content { ...partsProps.content }/>
</Link>
<Copy { ...partsProps }/>
<Copy { ...partsProps.copy }/>
</Container>
);
};
export default React.memo(chakra(TxEntity));
export default React.memo(chakra<As, EntityProps>(TxEntity));
export {
Container,
......
import { chakra } from '@chakra-ui/react';
import _omit from 'lodash/omit';
import React from 'react';
import { route } from 'nextjs-routes';
......@@ -11,26 +10,16 @@ import * as TxEntity from './TxEntity';
const rollupFeature = config.features.rollup;
const TxEntityL1 = (props: TxEntity.EntityProps) => {
const partsProps = _omit(props, [ 'className', 'onClick' ]);
const linkProps = _omit(props, [ 'className' ]);
if (!rollupFeature.isEnabled) {
return null;
}
return (
<TxEntity.Container className={ props.className }>
<TxEntity.Icon { ...partsProps }/>
<TxEntity.Link
{ ...linkProps }
isExternal
href={ rollupFeature.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: props.hash } }) }
>
<TxEntity.Content { ...partsProps }/>
</TxEntity.Link>
<TxEntity.Copy { ...partsProps }/>
</TxEntity.Container>
);
const defaultHref = rollupFeature.L1BaseUrl + route({
pathname: '/tx/[hash]',
query: { hash: props.hash },
});
return <TxEntity.default { ...props } href={ props.href ?? defaultHref } isExternal/>;
};
export default chakra(TxEntityL1);
......@@ -5,7 +5,7 @@ import { test, expect } from 'playwright/lib';
import UserOpEntity from './UserOpEntity';
const hash = '0x376db52955d5bce114d0ccea2dcf22289b4eae1b86bcae5a59bb5fdbfef48899';
const iconSizes = [ 'md', 'lg' ];
const iconSizes = [ 'md', 'lg' ] as const;
test.use({ viewport: { width: 180, height: 30 } });
......@@ -15,7 +15,7 @@ test.describe('icon size', () => {
const component = await render(
<UserOpEntity
hash={ hash }
iconSize={ size }
icon={{ size }}
/>,
);
......
import type { As } from '@chakra-ui/react';
import { chakra } from '@chakra-ui/react';
import _omit from 'lodash/omit';
import React from 'react';
import { route } from 'nextjs-routes';
import * as EntityBase from 'ui/shared/entities/base/components';
import { distributeEntityProps } from '../base/utils';
type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'hash'>;
const Link = chakra((props: LinkProps) => {
......@@ -21,11 +23,7 @@ const Link = chakra((props: LinkProps) => {
);
});
type IconProps = Omit<EntityBase.IconBaseProps, 'name'> & {
name?: EntityBase.IconBaseProps['name'];
};
const Icon = (props: IconProps) => {
const Icon = (props: EntityBase.IconBaseProps) => {
return (
<EntityBase.Icon
{ ...props }
......@@ -65,21 +63,20 @@ export interface EntityProps extends EntityBase.EntityBaseProps {
}
const UserOpEntity = (props: EntityProps) => {
const linkProps = _omit(props, [ 'className' ]);
const partsProps = _omit(props, [ 'className', 'onClick' ]);
const partsProps = distributeEntityProps(props);
return (
<Container className={ props.className }>
<Icon { ...partsProps }/>
<Link { ...linkProps }>
<Content { ...partsProps }/>
<Container { ...partsProps.container }>
<Icon { ...partsProps.icon }/>
<Link { ...partsProps.link }>
<Content { ...partsProps.content }/>
</Link>
<Copy { ...partsProps }/>
<Copy { ...partsProps.copy }/>
</Container>
);
};
export default React.memo(chakra(UserOpEntity));
export default React.memo(chakra<As, EntityProps>(UserOpEntity));
export {
Container,
......
......@@ -9,9 +9,10 @@ interface Props {
onDrop: (files: Array<File>) => void;
className?: string;
isDisabled?: boolean;
fullFilePath?: boolean;
}
const DragAndDropArea = ({ onDrop, children, className, isDisabled }: Props) => {
const DragAndDropArea = ({ onDrop, children, className, isDisabled, fullFilePath }: Props) => {
const [ isDragOver, setIsDragOver ] = React.useState(false);
const handleDrop = React.useCallback(async(event: DragEvent<HTMLDivElement>) => {
......@@ -22,11 +23,11 @@ const DragAndDropArea = ({ onDrop, children, className, isDisabled }: Props) =>
}
const fileEntries = await getAllFileEntries(event.dataTransfer.items);
const files = await Promise.all(fileEntries.map(convertFileEntryToFile));
const files = await Promise.all(fileEntries.map((fileEntry) => convertFileEntryToFile(fileEntry, fullFilePath)));
onDrop(files);
setIsDragOver(false);
}, [ isDisabled, onDrop ]);
}, [ isDisabled, onDrop, fullFilePath ]);
const handleDragOver = React.useCallback((event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
......
import stripLeadingSlash from 'lib/stripLeadingSlash';
// Function to get all files in drop directory
export async function getAllFileEntries(dataTransferItemList: DataTransferItemList): Promise<Array<FileSystemFileEntry>> {
const fileEntries: Array<FileSystemFileEntry> = [];
......@@ -54,11 +56,13 @@ async function readEntriesPromise(directoryReader: DirectoryReader): Promise<Arr
} catch (err) {}
}
export function convertFileEntryToFile(entry: FileSystemFileEntry): Promise<File> {
export function convertFileEntryToFile(entry: FileSystemFileEntry, fullFilePath?: boolean): Promise<File> {
return new Promise((resolve) => {
entry.file(async(file: File) => {
// const newFile = new File([ file ], entry.fullPath, { lastModified: file.lastModified, type: file.type });
resolve(file);
const newFile = fullFilePath ?
new File([ file ], stripLeadingSlash(entry.fullPath), { lastModified: file.lastModified, type: file.type }) :
file;
resolve(newFile);
});
});
}
......@@ -13,6 +13,7 @@ interface Props {
const Item = ({ label, children, isLoading }: { label: string; children: React.ReactNode; isLoading?: boolean}) => {
return (
<Flex
w="100%"
columnGap={ 5 }
rowGap={ 2 }
px={{ base: 0, lg: 4 }}
......@@ -35,6 +36,7 @@ const LogDecodedInputDataHeader = ({ methodId, methodCall, isLoading, rightSlot
fontSize="sm"
lineHeight={ 5 }
flexGrow={ 1 }
w="100%"
>
<Flex columnGap={ 2 } w="100%">
<Item label="Method id" isLoading={ isLoading }>
......@@ -43,7 +45,7 @@ const LogDecodedInputDataHeader = ({ methodId, methodCall, isLoading, rightSlot
{ rightSlot }
</Flex>
<Item label="Call" isLoading={ isLoading }>
<Skeleton isLoaded={ !isLoading } whiteSpace="pre-wrap">{ methodCall }</Skeleton>
<Skeleton isLoaded={ !isLoading } whiteSpace="pre-wrap" w="100%">{ methodCall }</Skeleton>
</Item>
</VStack>
);
......
......@@ -33,7 +33,7 @@ const Row = ({ name, type, indexed, value, isLoading }: ArrayElement<DecodedInpu
if (type === 'address' && typeof value === 'string') {
return (
<AddressEntity
address={{ hash: value, name: '', implementation_name: null, is_contract: false, is_verified: false }}
address={{ hash: value, name: '' }}
isLoading={ isLoading }
/>
);
......
......@@ -53,7 +53,7 @@ const LogItem = ({ address, index, topics, data, decoded, type, tx_hash: txHash,
) }
{ hasTxInfo ? <RowHeader isLoading={ isLoading }>Transaction</RowHeader> : <RowHeader isLoading={ isLoading }>Address</RowHeader> }
<GridItem display="flex" alignItems="center">
{ type === 'address' ? (
{ type === 'address' && txHash ? (
<TxEntity
hash={ txHash }
isLoading={ isLoading }
......
......@@ -51,7 +51,7 @@ const LogTopic = ({ hex, index, isLoading }: Props) => {
case 'address': {
return (
<AddressEntity
address={{ hash: value, name: '', implementation_name: null, is_contract: false, is_verified: false }}
address={{ hash: value, name: '' }}
isLoading={ isLoading }
/>
);
......
......@@ -13,11 +13,11 @@ interface Props {
const NftImageFullscreen = ({ src, isOpen, onClose }: Props) => {
const imgRef = React.useRef<HTMLImageElement>(null);
const [ hasDimentions, setHasDimentions ] = React.useState<boolean>(true);
const [ hasDimensions, setHasDimensions ] = React.useState<boolean>(true);
const checkWidth = React.useCallback(() => {
if (imgRef.current?.getBoundingClientRect().width === 0) {
setHasDimentions(false);
setHasDimensions(false);
}
}, [ ]);
......@@ -30,7 +30,7 @@ const NftImageFullscreen = ({ src, isOpen, onClose }: Props) => {
maxW="90vw"
ref={ imgRef }
onLoad={ checkWidth }
sx={ hasDimentions ? {} : { width: '90vw', height: '90vh' } }
{ ...(hasDimensions ? {} : { width: '90vw', height: '90vh' }) }
/>
</NftMediaFullscreenModal>
);
......
......@@ -72,7 +72,7 @@ const NftMedia = ({ imageUrl, animationUrl, className, isLoading, withFullscreen
switch (type) {
case 'video':
return <NftVideo { ...props } autoPlay={ autoplayVideo }/>;
return <NftVideo { ...props } autoPlay={ autoplayVideo } poster={ imageUrl || undefined }/>;
case 'html':
return <NftHtml { ...props }/>;
case 'image':
......
......@@ -5,22 +5,37 @@ import { mediaStyleProps, videoPlayProps } from './utils';
interface Props {
src: string;
poster?: string;
autoPlay?: boolean;
onLoad: () => void;
onError: () => void;
onClick?: () => void;
}
const NftVideo = ({ src, autoPlay = true, onLoad, onError, onClick }: Props) => {
const NftVideo = ({ src, poster, autoPlay = true, onLoad, onError, onClick }: Props) => {
const ref = React.useRef<HTMLVideoElement>(null);
const handleMouseEnter = React.useCallback(() => {
!autoPlay && ref.current?.play();
}, [ autoPlay ]);
const handleMouseLeave = React.useCallback(() => {
!autoPlay && ref.current?.pause();
}, [ autoPlay ]);
return (
<chakra.video
ref={ ref }
{ ...videoPlayProps }
autoPlay={ autoPlay }
poster={ poster }
src={ src }
onCanPlayThrough={ onLoad }
onError={ onError }
borderRadius="md"
onClick={ onClick }
onMouseEnter={ handleMouseEnter }
onMouseLeave={ handleMouseLeave }
{ ...mediaStyleProps }
/>
);
......
......@@ -56,6 +56,7 @@ const TxInterpretationElementByType = (
<chakra.span display="inline-block" verticalAlign="top" _notFirst={{ marginLeft: 1 }}>
<AddressEntity
address={ addressDataMap?.[value.hash] || value }
icon={{ marginRight: 1 }}
truncation="constant"
onClick={ onAddressClick }
whiteSpace="initial"
......@@ -68,6 +69,7 @@ const TxInterpretationElementByType = (
<chakra.span display="inline-block" verticalAlign="top" _notFirst={{ marginLeft: 1 }}>
<TokenEntity
token={ value }
icon={{ marginRight: 1 }}
onlySymbol
noCopy
width="fit-content"
......@@ -83,7 +85,8 @@ const TxInterpretationElementByType = (
return (
<chakra.span display="inline-block" verticalAlign="top" _notFirst={{ marginLeft: 1 }}>
<EnsEntity
name={ value }
domain={ value }
icon={{ marginRight: 1 }}
width="fit-content"
_notFirst={{ marginLeft: 1 }}
whiteSpace="initial"
......@@ -146,7 +149,7 @@ const TxInterpretation = ({ summary, isLoading, addressDataMap, className }: Pro
return (
<Skeleton isLoaded={ !isLoading } className={ className } fontWeight={ 500 } whiteSpace="pre-wrap" >
<Tooltip label="Transaction summary">
<IconSvg name="lightning" boxSize={ 5 } color="text_secondary" mr={ 2 } verticalAlign="text-top"/>
<IconSvg name="lightning" boxSize={ 5 } color="text_secondary" mr={ 1 } verticalAlign="text-top"/>
</Tooltip>
{ chunks.map((chunk, index) => {
return (
......
......@@ -56,6 +56,7 @@ const NavigationDesktop = () => {
return (
<Flex
display={{ base: 'none', lg: 'flex' }}
role="group"
position="relative"
flexDirection="column"
alignItems="stretch"
......@@ -65,11 +66,6 @@ const NavigationDesktop = () => {
py={ 12 }
width={{ lg: isExpanded ? '229px' : '92px', xl: isCollapsed ? '92px' : '229px' }}
{ ...getDefaultTransitionProps({ transitionProperty: 'width, padding' }) }
sx={{
'&:hover #expand-icon': {
display: 'block',
},
}}
onClick={ handleContainerClick }
>
<TestnetBadge position="absolute" pl={ 3 } w="49px" top="34px"/>
......@@ -125,8 +121,8 @@ const NavigationDesktop = () => {
cursor="pointer"
onClick={ handleTogglerClick }
aria-label="Expand/Collapse menu"
id="expand-icon"
display="none"
_groupHover={{ display: 'block' }}
/>
</Flex>
);
......
......@@ -9,8 +9,6 @@ import Popover from 'ui/shared/chakra/Popover';
import UserAvatar from 'ui/shared/UserAvatar';
import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent';
import useMenuButtonColors from '../useMenuButtonColors';
type Props = {
isHomePage?: boolean;
className?: string;
......@@ -21,7 +19,6 @@ type Props = {
const ProfileMenuDesktop = ({ isHomePage, className, fallbackIconSize, buttonBoxSize }: Props) => {
const { data, error, isPending } = useFetchProfileInfo();
const loginUrl = useLoginUrl();
const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors();
const [ hasMenu, setHasMenu ] = React.useState(false);
React.useEffect(() => {
......@@ -50,29 +47,6 @@ const ProfileMenuDesktop = ({ isHomePage, className, fallbackIconSize, buttonBox
};
})();
const variant = React.useMemo(() => {
if (hasMenu) {
return 'subtle';
}
return isHomePage ? 'solid' : 'outline';
}, [ hasMenu, isHomePage ]);
let iconButtonStyles: Partial<IconButtonProps> = {};
if (hasMenu) {
iconButtonStyles = {
bg: isHomePage ? 'blue.50' : themedBackground,
};
} else if (isHomePage) {
iconButtonStyles = {
color: 'white',
};
} else {
iconButtonStyles = {
borderColor: themedBorderColor,
color: themedColor,
};
}
return (
<Popover openDelay={ 300 } placement="bottom-end" gutter={ 10 } isLazy>
<Tooltip
......@@ -88,12 +62,11 @@ const ProfileMenuDesktop = ({ isHomePage, className, fallbackIconSize, buttonBox
className={ className }
aria-label="profile menu"
icon={ <UserAvatar size={ 20 } fallbackIconSize={ fallbackIconSize }/> }
variant={ variant }
colorScheme="blue"
variant={ isHomePage ? 'hero' : 'header' }
data-selected={ hasMenu }
boxSize={ buttonBoxSize ?? '40px' }
flexShrink={ 0 }
{ ...iconButtonProps }
{ ...iconButtonStyles }
/>
</PopoverTrigger>
</Box>
......
......@@ -8,13 +8,10 @@ import * as mixpanel from 'lib/mixpanel/index';
import UserAvatar from 'ui/shared/UserAvatar';
import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent';
import useMenuButtonColors from '../useMenuButtonColors';
const ProfileMenuMobile = () => {
const { isOpen, onOpen, onClose } = useDisclosure();
const { data, error, isPending } = useFetchProfileInfo();
const loginUrl = useLoginUrl();
const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors();
const [ hasMenu, setHasMenu ] = React.useState(false);
const handleSignInClick = React.useCallback(() => {
......@@ -48,13 +45,10 @@ const ProfileMenuMobile = () => {
<IconButton
aria-label="profile menu"
icon={ <UserAvatar size={ 20 }/> }
variant={ data?.avatar ? 'subtle' : 'outline' }
colorScheme="gray"
variant="header"
data-selected={ hasMenu }
boxSize="40px"
flexShrink={ 0 }
bg={ data?.avatar ? themedBackground : undefined }
color={ themedColor }
borderColor={ !data?.avatar ? themedBorderColor : undefined }
onClick={ hasMenu ? onOpen : undefined }
{ ...iconButtonProps }
/>
......
......@@ -46,7 +46,7 @@ const SearchBar = ({ isHomepage }: Props) => {
const recentSearchKeywords = getRecentSearchKeywords();
const { searchTerm, debouncedSearchTerm, handleSearchTermChange, query, pathname } = useQuickSearchQuery();
const { searchTerm, debouncedSearchTerm, handleSearchTermChange, query } = useQuickSearchQuery();
const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
......@@ -55,13 +55,13 @@ const SearchBar = ({ isHomepage }: Props) => {
const url = route(resultRoute);
mixpanel.logEvent(mixpanel.EventTypes.SEARCH_QUERY, {
'Search query': searchTerm,
'Source page type': mixpanel.getPageType(pathname),
'Source page type': mixpanel.getPageType(router.pathname),
'Result URL': url,
});
saveToRecentKeywords(searchTerm);
router.push(resultRoute, undefined, { shallow: true });
}
}, [ searchTerm, pathname, router ]);
}, [ searchTerm, router ]);
const handleFocus = React.useCallback(() => {
onOpen();
......@@ -90,18 +90,24 @@ const SearchBar = ({ isHomepage }: Props) => {
const handleItemClick = React.useCallback((event: React.MouseEvent<HTMLAnchorElement>) => {
mixpanel.logEvent(mixpanel.EventTypes.SEARCH_QUERY, {
'Search query': searchTerm,
'Source page type': mixpanel.getPageType(pathname),
'Source page type': mixpanel.getPageType(router.pathname),
'Result URL': event.currentTarget.href,
});
saveToRecentKeywords(searchTerm);
onClose();
}, [ pathname, searchTerm, onClose ]);
}, [ router.pathname, searchTerm, onClose ]);
const menuPaddingX = isMobile && !isHomepage ? 24 : 0;
const calculateMenuWidth = React.useCallback(() => {
menuWidth.current = (inputRef.current?.getBoundingClientRect().width || 0) - menuPaddingX;
}, [ menuPaddingX ]);
// clear input on page change
React.useEffect(() => {
handleSearchTermChange('');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ router.pathname ]);
React.useEffect(() => {
const inputEl = inputRef.current;
if (!inputEl) {
......
......@@ -48,10 +48,10 @@ const SearchBarSuggestApp = ({ data, isMobile, searchTerm, onClick }: Props) =>
variant="secondary"
overflow="hidden"
textOverflow="ellipsis"
sx={{
style={{
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
display: '-webkit-box',
'-webkit-box-orient': 'vertical',
'-webkit-line-clamp': '3',
}}
>
{ data.description }
......
import { useRouter } from 'next/router';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import useDebounce from 'lib/hooks/useDebounce';
export default function useQuickSearchQuery() {
const router = useRouter();
const [ searchTerm, setSearchTerm ] = React.useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 300);
const pathname = router.pathname;
const query = useApiQuery('quick_search', {
queryParams: { q: debouncedSearchTerm },
......@@ -30,6 +26,5 @@ export default function useQuickSearchQuery() {
handleSearchTermChange: setSearchTerm,
query,
redirectCheckQuery,
pathname,
}), [ debouncedSearchTerm, pathname, query, redirectCheckQuery, searchTerm ]);
}), [ debouncedSearchTerm, query, redirectCheckQuery, searchTerm ]);
}
import { useColorModeValue } from '@chakra-ui/react';
export default function useMenuColors() {
const themedBackground = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const themedBackgroundOrange = useColorModeValue('orange.100', 'orange.900');
const themedBorderColor = useColorModeValue('gray.300', 'gray.700');
const themedColor = useColorModeValue('blackAlpha.800', 'gray.400');
return { themedBackground, themedBackgroundOrange, themedBorderColor, themedColor };
}
import { Box, Flex, chakra } from '@chakra-ui/react';
import { Box, Flex, chakra, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressIdenticon from 'ui/shared/entities/address/AddressIdenticon';
import IconSvg from 'ui/shared/IconSvg';
import useMenuButtonColors from '../useMenuButtonColors';
type Props = {
address: string;
isAutoConnectDisabled?: boolean;
......@@ -14,8 +12,8 @@ type Props = {
};
const WalletIdenticon = ({ address, isAutoConnectDisabled, className }: Props) => {
const { themedBackgroundOrange } = useMenuButtonColors();
const isMobile = useIsMobile();
const borderColor = useColorModeValue('orange.100', 'orange.900');
return (
<Box className={ className } position="relative">
......@@ -31,7 +29,7 @@ const WalletIdenticon = ({ address, isAutoConnectDisabled, className }: Props) =
backgroundColor="rgba(16, 17, 18, 0.80)"
borderRadius="full"
border="1px solid"
borderColor={ themedBackgroundOrange }
borderColor={ borderColor }
>
<IconSvg
name="integration/partial"
......
import { Box, Button, Text, Flex, IconButton } from '@chakra-ui/react';
import { Box, Button, Text, Flex, IconButton, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import * as mixpanel from 'lib/mixpanel/index';
......@@ -6,8 +6,6 @@ import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import IconSvg from 'ui/shared/IconSvg';
import useMenuButtonColors from '../useMenuButtonColors';
type Props = {
address?: string;
ensDomainName?: string | null;
......@@ -18,7 +16,7 @@ type Props = {
};
const WalletMenuContent = ({ address, ensDomainName, disconnect, isAutoConnectDisabled, openWeb3Modal, closeWalletMenu }: Props) => {
const { themedBackgroundOrange } = useMenuButtonColors();
const bgColor = useColorModeValue('orange.100', 'orange.900');
const [ isModalOpening, setIsModalOpening ] = React.useState(false);
const onAddressClick = React.useCallback(() => {
......@@ -39,7 +37,7 @@ const WalletMenuContent = ({ address, ensDomainName, disconnect, isAutoConnectDi
p={ 3 }
mb={ 3 }
alignItems="center"
backgroundColor={ themedBackgroundOrange }
backgroundColor={ bgColor }
>
<IconSvg
name="integration/partial"
......@@ -70,28 +68,30 @@ const WalletMenuContent = ({ address, ensDomainName, disconnect, isAutoConnectDi
>
Your wallet is used to interact with apps and contracts in the explorer.
</Text>
<Flex alignItems="center" mb={ 6 }>
<AddressEntity
address={{ hash: address, ens_domain_name: ensDomainName }}
noTooltip
truncation="dynamic"
fontSize="sm"
fontWeight={ 700 }
color="text"
onClick={ onAddressClick }
flex={ 1 }
/>
<IconButton
aria-label="open wallet"
icon={ <IconSvg name="gear_slim" boxSize={ 5 }/> }
variant="simple"
h="20px"
w="20px"
ml={ 1 }
onClick={ handleOpenWeb3Modal }
isLoading={ isModalOpening }
/>
</Flex>
{ address && (
<Flex alignItems="center" mb={ 6 }>
<AddressEntity
address={{ hash: address, ens_domain_name: ensDomainName }}
noTooltip
truncation="dynamic"
fontSize="sm"
fontWeight={ 700 }
color="text"
onClick={ onAddressClick }
flex={ 1 }
/>
<IconButton
aria-label="open wallet"
icon={ <IconSvg name="gear_slim" boxSize={ 5 }/> }
variant="simple"
h="20px"
w="20px"
ml={ 1 }
onClick={ handleOpenWeb3Modal }
isLoading={ isModalOpening }
/>
</Flex>
) }
<Button size="sm" width="full" variant="outline" onClick={ disconnect }>
Disconnect
</Button>
......
import type { ButtonProps } from '@chakra-ui/react';
import { PopoverContent, PopoverBody, PopoverTrigger, Button, Box, useBoolean, chakra, useColorModeValue } from '@chakra-ui/react';
import { PopoverContent, PopoverBody, PopoverTrigger, Button, Box, useBoolean, chakra } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
......@@ -13,7 +12,6 @@ import IconSvg from 'ui/shared/IconSvg';
import useWallet from 'ui/snippets/walletMenu/useWallet';
import WalletMenuContent from 'ui/snippets/walletMenu/WalletMenuContent';
import useMenuButtonColors from '../useMenuButtonColors';
import WalletIdenticon from './WalletIdenticon';
import WalletTooltip from './WalletTooltip';
......@@ -37,7 +35,6 @@ export const WalletMenuDesktop = ({
isHomePage, className, size = 'md', isWalletConnected, address, connect,
disconnect, isModalOpening, isModalOpen, openModal,
}: ComponentProps) => {
const { themedBackground, themedBackgroundOrange, themedBorderColor, themedColor } = useMenuButtonColors();
const [ isPopoverOpen, setIsPopoverOpen ] = useBoolean(false);
const isMobile = useIsMobile();
const { isAutoConnectDisabled } = useMarketplaceContext();
......@@ -51,36 +48,6 @@ export const WalletMenuDesktop = ({
},
});
const variant = React.useMemo(() => {
if (isWalletConnected) {
return 'subtle';
}
return isHomePage ? 'solid' : 'outline';
}, [ isWalletConnected, isHomePage ]);
const themedColorForOrangeBg = useColorModeValue('blackAlpha.800', 'whiteAlpha.800');
let buttonStyles: Partial<ButtonProps> = {};
if (isWalletConnected) {
const backgroundColor = isAutoConnectDisabled ? themedBackgroundOrange : themedBackground;
const color = isAutoConnectDisabled ? themedColorForOrangeBg : themedColor;
buttonStyles = {
bg: isHomePage ? 'blue.50' : backgroundColor,
color: isHomePage ? 'blackAlpha.800' : color,
_hover: {
color: isHomePage ? 'blackAlpha.800' : color,
},
};
} else if (isHomePage) {
buttonStyles = {
color: 'white',
};
} else {
buttonStyles = {
borderColor: themedBorderColor,
color: themedColor,
};
}
const openPopover = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.WALLET_ACTION, { Action: 'Open' });
setIsPopoverOpen.toggle();
......@@ -103,8 +70,9 @@ export const WalletMenuDesktop = ({
>
<Button
className={ className }
variant={ variant }
colorScheme="blue"
variant={ isHomePage ? 'hero' : 'header' }
data-selected={ isWalletConnected }
data-warning={ isAutoConnectDisabled }
flexShrink={ 0 }
isLoading={
((isModalOpening || isModalOpen) && !isWalletConnected) ||
......@@ -115,7 +83,6 @@ export const WalletMenuDesktop = ({
fontSize="sm"
size={ size }
px={{ lg: isHomePage ? 2 : 4, xl: 4 }}
{ ...buttonStyles }
>
{ isWalletConnected ? (
<>
......
......@@ -10,7 +10,6 @@ import IconSvg from 'ui/shared/IconSvg';
import useWallet from 'ui/snippets/walletMenu/useWallet';
import WalletMenuContent from 'ui/snippets/walletMenu/WalletMenuContent';
import useMenuButtonColors from '../useMenuButtonColors';
import WalletIdenticon from './WalletIdenticon';
import WalletTooltip from './WalletTooltip';
......@@ -28,7 +27,6 @@ export const WalletMenuMobile = (
{ isWalletConnected, address, connect, disconnect, isModalOpening, isModalOpen, openModal }: ComponentProps,
) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const { themedBackground, themedBackgroundOrange, themedBorderColor, themedColor } = useMenuButtonColors();
const isMobile = useIsMobile();
const { isAutoConnectDisabled } = useMarketplaceContext();
const addressDomainQuery = useApiQuery('address_domain', {
......@@ -46,8 +44,6 @@ export const WalletMenuMobile = (
onOpen();
}, [ onOpen ]);
const themedBg = isAutoConnectDisabled ? themedBackgroundOrange : themedBackground;
return (
<>
<WalletTooltip
......@@ -62,13 +58,11 @@ export const WalletMenuMobile = (
<WalletIdenticon address={ address } isAutoConnectDisabled={ isAutoConnectDisabled }/> :
<IconSvg name="wallet" boxSize={ 6 } p={ 0.5 }/>
}
variant={ isWalletConnected ? 'subtle' : 'outline' }
colorScheme="gray"
variant="header"
data-selected={ isWalletConnected }
data-warning={ isAutoConnectDisabled }
boxSize="40px"
flexShrink={ 0 }
bg={ isWalletConnected ? themedBg : undefined }
color={ themedColor }
borderColor={ !isWalletConnected ? themedBorderColor : undefined }
onClick={ isWalletConnected ? openPopover : connect }
isLoading={
((isModalOpening || isModalOpen) && !isWalletConnected) ||
......
......@@ -100,7 +100,7 @@ const TokenDetails = ({ tokenQuery }: Props) => {
<Grid
columnGap={ 8 }
rowGap={{ base: 1, lg: 3 }}
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(728px, auto)' }} overflow="hidden"
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden"
>
{ exchangeRate && (
<>
......
......@@ -113,13 +113,15 @@ const TokenPageTitle = ({ tokenQuery, addressQuery, hash }: Props) => {
const secondRow = (
<Flex alignItems="center" w="100%" minW={ 0 } columnGap={ 2 } rowGap={ 2 } flexWrap={{ base: 'wrap', lg: 'nowrap' }}>
<AddressEntity
address={{ ...addressQuery.data, name: '' }}
isLoading={ isLoading }
fontFamily="heading"
fontSize="lg"
fontWeight={ 500 }
/>
{ addressQuery.data && (
<AddressEntity
address={{ ...addressQuery.data, name: '' }}
isLoading={ isLoading }
fontFamily="heading"
fontSize="lg"
fontWeight={ 500 }
/>
) }
{ !isLoading && tokenQuery.data && <AddressAddToWallet token={ tokenQuery.data } variant="button"/> }
<AddressQrCode address={ addressQuery.data } isLoading={ isLoading }/>
<AccountActionsMenu isLoading={ isLoading }/>
......@@ -139,7 +141,7 @@ const TokenPageTitle = ({ tokenQuery, addressQuery, hash }: Props) => {
<TokenEntity.Icon
token={ tokenQuery.data }
isLoading={ tokenQuery.isPlaceholderData }
iconSize="lg"
size="lg"
/>
) : null }
contentAfter={ contentAfter }
......
......@@ -89,18 +89,20 @@ const TokenInstancePageTitle = ({ isLoading, token, instance, hash }: Props) =>
const titleSecondRow = (
<Flex alignItems="center" w="100%" minW={ 0 } columnGap={ 2 } rowGap={ 2 } flexWrap={{ base: 'wrap', lg: 'nowrap' }}>
<TokenEntity
token={ token }
isLoading={ isLoading }
noSymbol
noCopy
jointSymbol
fontFamily="heading"
fontSize="lg"
fontWeight={ 500 }
w="auto"
maxW="700px"
/>
{ token && (
<TokenEntity
token={ token }
isLoading={ isLoading }
noSymbol
noCopy
jointSymbol
fontFamily="heading"
fontSize="lg"
fontWeight={ 500 }
w="auto"
maxW="700px"
/>
) }
{ !isLoading && <AddressAddToWallet token={ token } variant="button"/> }
<AddressQrCode address={ address } isLoading={ isLoading }/>
<AccountActionsMenu isLoading={ isLoading } showUpdateMetadataItem={ Boolean(instance?.metadata) }/>
......
......@@ -54,11 +54,7 @@ const TokensTableItem = ({
return (
<Tr
sx={{
'&:hover [aria-label="Add token to wallet"]': {
opacity: 1,
},
}}
role="group"
>
<Td>
<Flex alignItems="flex-start">
......@@ -94,6 +90,7 @@ const TokensTableItem = ({
isLoading={ isLoading }
iconSize={ 5 }
opacity={ 0 }
_groupHover={{ opacity: 1 }}
/>
</Flex>
<Flex columnGap={ 1 }>
......
......@@ -47,16 +47,20 @@ const TxDetailsWrapped = ({ data }: Props) => {
<DetailsInfoItemDivider/>
<DetailsInfoItem.Label
hint="Address (external or contract) receiving the transaction"
>
{ data.to?.is_contract ? 'Interacted with contract' : 'To' }
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Flex flexWrap="nowrap" alignItems="center" maxW="100%">
<AddressEntity address={ data.to }/>
</Flex>
</DetailsInfoItem.Value>
{ data.to && (
<>
<DetailsInfoItem.Label
hint="Address (external or contract) receiving the transaction"
>
{ data.to.is_contract ? 'Interacted with contract' : 'To' }
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Flex flexWrap="nowrap" alignItems="center" maxW="100%">
<AddressEntity address={ data.to }/>
</Flex>
</DetailsInfoItem.Value>
</>
) }
<DetailsInfoItemDivider/>
......
......@@ -113,11 +113,12 @@ test.describe('blockscout provider', () => {
test('no interpretation, has method called', async({ render, mockApiResponse }) => {
// the action button should not render if there is no interpretation
const newTxQuery = { ...txQuery, data: txMock.withRecipientContract } as TxQuery;
const metadataResponse = generateAddressMetadataResponse(protocolTagWithMeta);
await mockApiResponse('address_metadata_info', metadataResponse, { queryParams: addressMetadataQueryParams });
await mockApiResponse('tx_interpretation', { data: { summaries: [] } }, { pathParams: { hash } });
const component = await render(<TxSubHeading hash={ hash } hasTag={ false } txQuery={ txQuery }/>);
const component = await render(<TxSubHeading hash={ hash } hasTag={ false } txQuery={ newTxQuery }/>);
await expect(component).toHaveScreenshot();
});
......
......@@ -19,7 +19,7 @@ import { createNovesSummaryObject } from './assetFlows/utils/createNovesSummaryO
import type { TxQuery } from './useTxQuery';
type Props = {
hash?: string;
hash: string;
hasTag: boolean;
txQuery: TxQuery;
}
......
......@@ -25,9 +25,11 @@ const NovesActionSnippet: FC<Props> = ({ item, isLoaded }) => {
const symbol = action.nft?.symbol || action.token?.symbol;
const token = {
name: name,
symbol: symbol?.toLowerCase() === name?.toLowerCase() ? undefined : symbol,
address: action.nft?.address || action.token?.address,
name: name || '',
symbol: (symbol?.toLowerCase() === name?.toLowerCase() ? undefined : symbol) || '',
address: action.nft?.address || action.token?.address || '',
icon_url: '',
type: action.nft ? 'ERC-721' as const : 'ERC-20' as const,
};
return token;
......
......@@ -41,14 +41,14 @@ const TxDetailsAction = ({ action }: Props) => {
const token0 = {
address: data.symbol0 === 'Ether' ? '' : data.address0,
name: data.symbol0 === 'Ether' ? config.chain.currency.symbol || null : data.symbol0,
type: 'ERC-20',
type: 'ERC-20' as const,
symbol: null,
icon_url: null,
};
const token1 = {
address: data.symbol1 === 'Ether' ? '' : data.address1,
name: data.symbol1 === 'Ether' ? config.chain.currency.symbol || null : data.symbol1,
type: 'ERC-20',
type: 'ERC-20' as const,
symbol: null,
icon_url: null,
};
......@@ -99,7 +99,7 @@ const TxDetailsAction = ({ action }: Props) => {
const token = {
address: data.address,
name: data.name,
type: 'ERC-20',
type: 'ERC-20' as const,
symbol: null,
icon_url: null,
};
......@@ -133,7 +133,7 @@ const TxDetailsAction = ({ action }: Props) => {
<Flex key={ data.address + id } whiteSpace="pre-wrap" columnGap={ 2 }>
<chakra.span flexShrink={ 0 }>1</chakra.span>
<chakra.span color="text_secondary" flexShrink={ 0 }>of token ID</chakra.span>
<NftEntity hash={ data.address } id={ id } w="min-content" iconSize="md"/>
<NftEntity hash={ data.address } id={ id } w="min-content" icon={{ size: 'md' }}/>
</Flex>
);
})
......
import { GridItem } from '@chakra-ui/react';
import React from 'react';
import dayjs from 'lib/date/dayjs';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import OptimisticL2TxnBatchBlobWrapper from './OptimisticL2TxnBatchBlobWrapper';
interface Props {
l1TxHashes: Array<string>;
l1Timestamp: string;
isLoading: boolean;
}
const OptimisticL2TxnBatchBlobCallData = ({ l1TxHashes, l1Timestamp, isLoading }: Props) => {
return (
<OptimisticL2TxnBatchBlobWrapper isLoading={ isLoading }>
<GridItem fontWeight={ 600 }>Timestamp</GridItem>
<GridItem whiteSpace="normal">
{ dayjs(l1Timestamp).fromNow() } | { dayjs(l1Timestamp).format('llll') }
</GridItem>
<GridItem fontWeight={ 600 }>L1 txn hash{ l1TxHashes.length > 1 ? 'es' : '' }</GridItem>
<GridItem overflow="hidden" display="flex" flexDir="column" rowGap={ 2 }>
{ l1TxHashes.map((hash) => <TxEntityL1 key={ hash } hash={ hash } noIcon noCopy={ false }/>) }
</GridItem>
</OptimisticL2TxnBatchBlobWrapper>
);
};
export default React.memo(OptimisticL2TxnBatchBlobCallData);
import { Flex, GridItem, Icon, VStack } from '@chakra-ui/react';
import React from 'react';
import type { OptimisticL2BlobTypeCelestia } from 'types/api/optimisticL2';
// This icon doesn't work properly when it is in the sprite
// Probably because of the gradient
// eslint-disable-next-line no-restricted-imports
import celeniumIcon from 'icons/brands/celenium.svg';
import dayjs from 'lib/date/dayjs';
import hexToBase64 from 'lib/hexToBase64';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/links/LinkExternal';
import OptimisticL2TxnBatchBlobWrapper from './OptimisticL2TxnBatchBlobWrapper';
function getCeleniumUrl(blob: OptimisticL2BlobTypeCelestia) {
const url = new URL('https://mocha.celenium.io/blob');
url.searchParams.set('commitment', hexToBase64(blob.commitment));
url.searchParams.set('hash', hexToBase64(blob.namespace));
url.searchParams.set('height', String(blob.height));
return url.toString();
}
interface Props {
blobs: Array<OptimisticL2BlobTypeCelestia>;
isLoading: boolean;
}
const OptimisticL2TxnBatchBlobCelestia = ({ blobs, isLoading }: Props) => {
return (
<VStack rowGap={ 2 } w="100%">
{ blobs.map((blob) => {
return (
<OptimisticL2TxnBatchBlobWrapper key={ blob.commitment } isLoading={ isLoading } gridTemplateColumns="auto 1fr auto">
<GridItem fontWeight={ 600 }>Commitment</GridItem>
<GridItem overflow="hidden">
<Flex minW="0" w="calc(100% - 20px)">
<HashStringShortenDynamic hash={ blob.commitment }/>
<CopyToClipboard text={ blob.commitment }/>
</Flex>
</GridItem>
<GridItem display="flex" columnGap={ 2 }>
<Icon as={ celeniumIcon } boxSize={ 5 }/>
<LinkExternal href={ getCeleniumUrl(blob) }>Blob page</LinkExternal>
</GridItem>
<GridItem fontWeight={ 600 }>Hight</GridItem>
<GridItem colSpan={ 2 }>
{ blob.height }
</GridItem>
<GridItem fontWeight={ 600 }>Timestamp</GridItem>
<GridItem whiteSpace="normal" colSpan={ 2 }>
{ dayjs(blob.l1_timestamp).fromNow() } | { dayjs(blob.l1_timestamp).format('llll') }
</GridItem>
<GridItem fontWeight={ 600 }>L1 txn hash</GridItem>
<GridItem overflow="hidden" colSpan={ 2 }>
<TxEntityL1 hash={ blob.l1_transaction_hash } noIcon noCopy={ false }/>
</GridItem>
</OptimisticL2TxnBatchBlobWrapper>
);
}) }
</VStack>
);
};
export default React.memo(OptimisticL2TxnBatchBlobCelestia);
import { GridItem, VStack } from '@chakra-ui/react';
import React from 'react';
import type { OptimisticL2BlobTypeEip4844 } from 'types/api/optimisticL2';
import dayjs from 'lib/date/dayjs';
import BlobEntityL1 from 'ui/shared/entities/blob/BlobEntityL1';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import OptimisticL2TxnBatchBlobWrapper from './OptimisticL2TxnBatchBlobWrapper';
interface Props {
blobs: Array<OptimisticL2BlobTypeEip4844>;
isLoading: boolean;
}
const OptimisticL2TxnBatchBlobEip4844 = ({ blobs, isLoading }: Props) => {
return (
<VStack rowGap={ 2 } w="100%">
{ blobs.map((blob) => {
return (
<OptimisticL2TxnBatchBlobWrapper key={ blob.hash } isLoading={ isLoading }>
<GridItem fontWeight={ 600 }>Versioned hash</GridItem>
<GridItem overflow="hidden">
<BlobEntityL1 hash={ blob.hash }/>
</GridItem>
<GridItem fontWeight={ 600 }>Timestamp</GridItem>
<GridItem whiteSpace="normal">
{ dayjs(blob.l1_timestamp).fromNow() } | { dayjs(blob.l1_timestamp).format('llll') }
</GridItem>
<GridItem fontWeight={ 600 }>L1 txn hash</GridItem>
<GridItem overflow="hidden">
<TxEntityL1 hash={ blob.l1_transaction_hash } noIcon noCopy={ false }/>
</GridItem>
</OptimisticL2TxnBatchBlobWrapper>
);
}) }
</VStack>
);
};
export default React.memo(OptimisticL2TxnBatchBlobEip4844);
import { chakra, Grid, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
interface Props {
children: React.ReactNode;
className?: string;
isLoading: boolean;
}
const OptimisticL2TxnBatchBlobWrapper = ({ children, className, isLoading }: Props) => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.100');
return (
<Grid
className={ className }
columnGap={ 3 }
rowGap="10px"
p={ 4 }
bgColor={ bgColor }
gridTemplateColumns="auto 1fr"
borderRadius="base"
w="100%"
h={ isLoading ? '140px' : undefined }
fontSize="sm"
lineHeight={ 5 }
>
{ isLoading ? null : children }
</Grid>
);
};
export default React.memo(chakra(OptimisticL2TxnBatchBlobWrapper));
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { OptimismL2TxnBatch } from 'types/api/optimisticL2';
import type { ResourceError } from 'lib/api/resources';
import * as txnBatchesMock from 'mocks/optimism/txnBatches';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';
import OptimisticL2TxnBatchDetails from './OptimisticL2TxnBatchDetails';
const hooksConfig = {
router: {
query: { number: '1' },
},
};
test.beforeEach(async({ mockEnvs }) => {
await mockEnvs(ENVS_MAP.optimisticRollup);
});
test('call data blob container +@mobile', async({ render }) => {
const query = {
data: txnBatchesMock.txnBatchTypeCallData,
} as UseQueryResult<OptimismL2TxnBatch, ResourceError>;
const component = await render(<OptimisticL2TxnBatchDetails query={ query }/>, { hooksConfig });
await expect(component).toHaveScreenshot();
});
test('celestia blob container +@mobile', async({ render }) => {
const query = {
data: txnBatchesMock.txnBatchTypeCelestia,
} as UseQueryResult<OptimismL2TxnBatch, ResourceError>;
const component = await render(<OptimisticL2TxnBatchDetails query={ query }/>, { hooksConfig });
await expect(component).toHaveScreenshot();
});
test('EIP-4844 blob container +@mobile', async({ render }) => {
const query = {
data: txnBatchesMock.txnBatchTypeEip4844,
} as UseQueryResult<OptimismL2TxnBatch, ResourceError>;
const component = await render(<OptimisticL2TxnBatchDetails query={ query }/>, { hooksConfig });
await expect(component).toHaveScreenshot();
});
import { Grid, Skeleton } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { OptimismL2TxnBatch } from 'types/api/optimisticL2';
import { route } from 'nextjs-routes';
import type { ResourceError } from 'lib/api/resources';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import isCustomAppError from 'ui/shared/AppError/isCustomAppError';
import OptimisticL2TxnBatchDA from 'ui/shared/batch/OptimisticL2TxnBatchDA';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsTimestamp from 'ui/shared/DetailsTimestamp';
import LinkInternal from 'ui/shared/links/LinkInternal';
import PrevNext from 'ui/shared/PrevNext';
import OptimisticL2TxnBatchBlobCallData from './OptimisticL2TxnBatchBlobCallData';
import OptimisticL2TxnBatchBlobCelestia from './OptimisticL2TxnBatchBlobCelestia';
import OptimisticL2TxnBatchBlobEip4844 from './OptimisticL2TxnBatchBlobEip4844';
interface Props {
query: UseQueryResult<OptimismL2TxnBatch, ResourceError>;
}
const OptimisticL2TxnBatchDetails = ({ query }: Props) => {
const router = useRouter();
const { data, isError, error, isPlaceholderData } = query;
const handlePrevNextClick = React.useCallback((direction: 'prev' | 'next') => {
if (!data) {
return;
}
const increment = direction === 'next' ? +1 : -1;
const nextId = String(data.internal_id + increment);
router.push({ pathname: '/batches/[number]', query: { number: nextId } }, undefined);
}, [ data, router ]);
if (isError) {
if (isCustomAppError(error)) {
throwOnResourceLoadError({ isError, error });
}
return <DataFetchAlert/>;
}
if (!data) {
return null;
}
const blocksCount = data.l2_block_end - data.l2_block_start + 1;
return (
<Grid
columnGap={ 8 }
rowGap={{ base: 3, lg: 3 }}
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'minmax(min-content, 200px) minmax(0, 1fr)' }}
overflow="hidden"
>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="Batch ID indicates the length of batches produced by grouping L2 blocks to be proven on L1"
>
Batch ID
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isPlaceholderData }>
{ data.internal_id }
</Skeleton>
<PrevNext
ml={ 6 }
onClick={ handlePrevNextClick }
prevLabel="View previous tx batch"
nextLabel="View next tx batch"
isPrevDisabled={ data.internal_id === 0 }
isLoading={ isPlaceholderData }
/>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="Date and time at which batch is submitted to L1"
>
Timestamp
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
{ data.l1_timestamp ?
<DetailsTimestamp timestamp={ data.l1_timestamp }isLoading={ isPlaceholderData }/> :
'Undefined'
}
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="Number of transactions in this batch"
>
Transactions
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isPlaceholderData }>
<LinkInternal href={ route({ pathname: '/batches/[number]', query: { number: data.internal_id.toString(), tab: 'txs' } }) }>
{ data.tx_count.toLocaleString() } transaction{ data.tx_count === 1 ? '' : 's' }
</LinkInternal>
{ ' ' }in this batch
</Skeleton>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="Number of L2 blocks in this batch"
>
Blocks
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isPlaceholderData }>
<LinkInternal href={ route({ pathname: '/batches/[number]', query: { number: data.internal_id.toString(), tab: 'blocks' } }) }>
{ blocksCount.toLocaleString() } block{ blocksCount === 1 ? '' : 's' }
</LinkInternal>
{ ' ' }in this batch
</Skeleton>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="Where the batch data is stored"
>
Batch data container
</DetailsInfoItem.Label>
<DetailsInfoItem.Value flexDir="column" alignItems="flex-start" rowGap={ 2 }>
<OptimisticL2TxnBatchDA container={ data.batch_data_container } isLoading={ isPlaceholderData }/>
{ data.batch_data_container === 'in_blob4844' && data.blobs &&
<OptimisticL2TxnBatchBlobEip4844 blobs={ data.blobs } isLoading={ isPlaceholderData }/> }
{ data.batch_data_container === 'in_calldata' && (
<OptimisticL2TxnBatchBlobCallData
l1TxHashes={ data.l1_tx_hashes }
l1Timestamp={ data.l1_timestamp }
isLoading={ isPlaceholderData }
/>
) }
{ data.batch_data_container === 'in_celestia' && data.blobs &&
<OptimisticL2TxnBatchBlobCelestia blobs={ data.blobs } isLoading={ isPlaceholderData }/> }
</DetailsInfoItem.Value>
</Grid>
);
};
export default OptimisticL2TxnBatchDetails;
import { Skeleton, VStack } from '@chakra-ui/react';
import { Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2';
......@@ -6,8 +6,8 @@ import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import OptimisticL2TxnBatchDA from 'ui/shared/batch/OptimisticL2TxnBatchDA';
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
......@@ -24,52 +24,60 @@ const OptimisticL2TxnBatchesListItem = ({ item, isLoading }: Props) => {
return (
<ListItemMobileGrid.Container gridTemplateColumns="100px auto">
<ListItemMobileGrid.Label isLoading={ isLoading }>L2 block #</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py="3px">
<BlockEntityL2
<ListItemMobileGrid.Label isLoading={ isLoading }>Batch ID</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<BatchEntityL2 number={ item.internal_id } isLoading={ isLoading }/>
</ListItemMobileGrid.Value>
{ item.batch_data_container && (
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>
Storage
</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<OptimisticL2TxnBatchDA container={ item.batch_data_container } isLoading={ isLoading }/>
</ListItemMobileGrid.Value>
</>
) }
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<TimeAgoWithTooltip
timestamp={ item.l1_timestamp }
isLoading={ isLoading }
number={ item.l2_block_number }
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
display="inline-block"
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>L2 block txn count</ListItemMobileGrid.Label>
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn count</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } minW="40px">
{ item.l1_tx_hashes.length }
</Skeleton>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>L2 blocks</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<LinkInternal
href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString(), tab: 'txs' } }) }
href={ route({ pathname: '/batches/[number]', query: { number: item.internal_id.toString(), tab: 'blocks' } }) }
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading } minW="40px">
{ item.tx_count }
{ item.l2_block_end - item.l2_block_start + 1 }
</Skeleton>
</LinkInternal>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py="3px">
<VStack spacing={ 3 } w="100%" overflow="hidden" alignItems="flex-start">
{ item.l1_tx_hashes.map(hash => (
<TxEntityL1
key={ hash }
isLoading={ isLoading }
hash={ hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
/>
)) }
</VStack>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Label isLoading={ isLoading }>L2 blocks</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<TimeAgoWithTooltip
timestamp={ item.l1_timestamp }
<LinkInternal
href={ route({ pathname: '/batches/[number]', query: { number: item.internal_id.toString(), tab: 'txs' } }) }
isLoading={ isLoading }
display="inline-block"
/>
>
<Skeleton isLoaded={ !isLoading } minW="40px">
{ item.tx_count }
</Skeleton>
</LinkInternal>
</ListItemMobileGrid.Value>
</ListItemMobileGrid.Container>
......
......@@ -15,19 +15,21 @@ type Props = {
const OptimisticL2TxnBatchesTable = ({ items, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm" minW="850px">
<Table variant="simple" size="sm" minW="850px" layout="auto">
<Thead top={ top }>
<Tr>
<Th width="170px">L2 block #</Th>
<Th width="170px">L2 block txn count</Th>
<Th width="100%">L1 txn hash</Th>
<Th width="150px">Age</Th>
<Th>Batch ID</Th>
<Th >Storage</Th>
<Th >Age</Th>
<Th isNumeric>L1 txn count</Th>
<Th isNumeric>L2 blocks</Th>
<Th isNumeric>Txn</Th>
</Tr>
</Thead>
<Tbody>
{ items.map((item, index) => (
<OptimisticL2TxnBatchesTableItem
key={ item.l2_block_number + (isLoading ? String(index) : '') }
key={ item.internal_id + (isLoading ? String(index) : '') }
item={ item }
isLoading={ isLoading }
/>
......
import { Td, Tr, VStack, Skeleton } from '@chakra-ui/react';
import { Td, Tr, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2';
......@@ -6,8 +6,8 @@ import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import OptimisticL2TxnBatchDA from 'ui/shared/batch/OptimisticL2TxnBatchDA';
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import LinkInternal from 'ui/shared/links/LinkInternal';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
......@@ -22,49 +22,47 @@ const OptimisticL2TxnBatchesTableItem = ({ item, isLoading }: Props) => {
return (
<Tr>
<Td>
<BlockEntityL2
<Td verticalAlign="middle">
<BatchEntityL2 number={ item.internal_id } isLoading={ isLoading }/>
</Td>
<Td verticalAlign="middle">
{ item.batch_data_container ? <OptimisticL2TxnBatchDA container={ item.batch_data_container } isLoading={ isLoading }/> : '-' }
</Td>
<Td verticalAlign="middle">
<TimeAgoWithTooltip
timestamp={ item.l1_timestamp }
isLoading={ isLoading }
number={ item.l2_block_number }
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
noIcon
display="inline-block"
color="text_secondary"
my={ 1 }
/>
</Td>
<Td>
<Td verticalAlign="middle" isNumeric>
<Skeleton isLoaded={ !isLoading } minW="40px" display="inline-block">
{ item.l1_tx_hashes.length }
</Skeleton>
</Td>
<Td verticalAlign="middle" isNumeric>
<LinkInternal
href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString(), tab: 'txs' } }) }
href={ route({ pathname: '/batches/[number]', query: { number: item.internal_id.toString(), tab: 'blocks' } }) }
isLoading={ isLoading }
justifyContent="flex-end"
>
<Skeleton isLoaded={ !isLoading } minW="40px" my={ 1 }>
{ item.tx_count }
<Skeleton isLoaded={ !isLoading } minW="40px" display="inline-block">
{ item.l2_block_end - item.l2_block_start + 1 }
</Skeleton>
</LinkInternal>
</Td>
<Td pr={ 12 }>
<VStack spacing={ 3 } alignItems="flex-start">
{ item.l1_tx_hashes.map(hash => (
<TxEntityL1
key={ hash }
isLoading={ isLoading }
hash={ hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
noIcon
/>
)) }
</VStack>
</Td>
<Td>
<TimeAgoWithTooltip
timestamp={ item.l1_timestamp }
<Td verticalAlign="middle" isNumeric>
<LinkInternal
href={ route({ pathname: '/batches/[number]', query: { number: item.internal_id.toString(), tab: 'txs' } }) }
isLoading={ isLoading }
display="inline-block"
color="text_secondary"
my={ 1 }
/>
justifyContent="flex-end"
>
<Skeleton isLoaded={ !isLoading } minW="40px" display="inline-block">
{ item.tx_count }
</Skeleton>
</LinkInternal>
</Td>
</Tr>
);
......
......@@ -54,7 +54,9 @@ const TxsListItem = ({ tx, isLoading, showBlockInfo, currentAddress, enableTimeI
hash={ tx.hash }
truncation="constant_long"
fontWeight="700"
iconName={ tx.tx_types.includes('blob_transaction') ? 'blob' : undefined }
icon={{
name: tx.tx_types.includes('blob_transaction') ? 'blob' : undefined,
}}
/>
<TimeAgoWithTooltip
timestamp={ tx.timestamp }
......
......@@ -198,7 +198,7 @@ const UserOpDetails = ({ query }: Props) => {
Block
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<BlockEntity number={ data.block_number } isLoading={ isPlaceholderData }/>
<BlockEntity number={ Number(data.block_number) } isLoading={ isPlaceholderData }/>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
......
......@@ -13,7 +13,7 @@ import UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity';
import TxInterpretation from 'ui/shared/tx/interpretation/TxInterpretation';
type Props = {
hash?: string;
hash: string;
// userOpQuery: UseQueryResult<UserOp, ResourceError<unknown>>;
}
......
......@@ -74,7 +74,7 @@ const UserOpsListItem = ({ item, isLoading, showTx, showSender }: Props) => {
<ListItemMobileGrid.Label isLoading={ isLoading }>Block</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<BlockEntity
number={ item.block_number }
number={ Number(item.block_number) }
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
......
......@@ -57,7 +57,7 @@ const UserOpsTableItem = ({ item, isLoading, showTx, showSender }: Props) => {
) }
<Td verticalAlign="middle">
<BlockEntity
number={ item.block_number }
number={ Number(item.block_number) }
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
......
......@@ -48,6 +48,7 @@ const VerifiedAddressesListItem = ({ item, application, onAdd, onEdit, isLoading
}
const token = {
type: 'ERC-20' as const,
icon_url: application.iconUrl,
address: application.tokenAddress,
name: item.metadata.tokenName,
......
......@@ -48,6 +48,7 @@ const VerifiedAddressesTableItem = ({ item, application, onAdd, onEdit, isLoadin
}
const token = {
type: 'ERC-20' as const,
icon_url: application.iconUrl,
address: application.tokenAddress,
name: item.metadata.tokenName,
......
......@@ -100,7 +100,7 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd
if (!isAdd && data) {
// edit address
return apiFetch('watchlist', {
pathParams: { id: data?.id || '' },
pathParams: { id: data?.id ? String(data.id) : '' },
fetchParams: { method: 'PUT', body },
});
......
......@@ -20,7 +20,7 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, onSuccess, data
const mutationFn = useCallback(() => {
return apiFetch('watchlist', {
pathParams: { id: data.id },
pathParams: { id: String(data.id) },
fetchParams: { method: 'DELETE' },
});
}, [ data?.id, apiFetch ]);
......
......@@ -65,7 +65,7 @@ const WatchListItem = ({ item, isLoading, onEditClick, onDeleteClick }: Props) =
const body = { ...item, notification_methods: { email: !notificationEnabled } };
setNotificationEnabled(prevState => !prevState);
return apiFetch('watchlist', {
pathParams: { id: item.id },
pathParams: { id: String(item.id) },
fetchParams: { method: 'PUT', body },
});
},
......
......@@ -69,7 +69,7 @@ const WatchlistTableItem = ({ item, isLoading, onEditClick, onDeleteClick }: Pro
const body = { ...item, notification_methods: { email: !notificationEnabled } };
setNotificationEnabled(prevState => !prevState);
return apiFetch('watchlist', {
pathParams: { id: item.id },
pathParams: { id: String(item.id) },
fetchParams: { method: 'PUT', body },
});
},
......
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment