Merge pull request #2960 from blockscout/release/v2-3-0
Fixes for release v2.3.0
@@ -14,6 +14,7 @@ on:
|
||||
- arbitrum_nova
|
||||
- arbitrum_sepolia
|
||||
- base
|
||||
- celo
|
||||
- celo_alfajores
|
||||
- garnet
|
||||
- gnosis
|
||||
|
||||
@@ -14,6 +14,7 @@ on:
|
||||
- arbitrum_nova
|
||||
- arbitrum_sepolia
|
||||
- base
|
||||
- celo
|
||||
- celo_alfajores
|
||||
- garnet
|
||||
- gnosis
|
||||
|
||||
@@ -364,6 +364,7 @@
|
||||
"arbitrum_sepolia",
|
||||
"base",
|
||||
"blackfort_testnet",
|
||||
"celo",
|
||||
"celo_alfajores",
|
||||
"garnet",
|
||||
"gnosis",
|
||||
|
||||
@@ -4,12 +4,13 @@ import { getEnvValue } from '../utils';
|
||||
|
||||
const title = 'Celo chain';
|
||||
|
||||
const config: Feature<{ }> = (() => {
|
||||
const config: Feature<{ nativeTokenAddress?: string }> = (() => {
|
||||
|
||||
if (getEnvValue('NEXT_PUBLIC_CELO_ENABLED') === 'true') {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
nativeTokenAddress: getEnvValue('NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS'),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# Set of ENVs for Celo Mainnet network explorer
|
||||
# https://celo.blockscout.com
|
||||
# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=celo"
|
||||
|
||||
# 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
|
||||
|
||||
NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS=0x471EcE3750Da237f93B8E339c536989b8978a438
|
||||
|
||||
# Instance ENVs
|
||||
NEXT_PUBLIC_ADDRESS_3RD_PARTY_WIDGETS=['talentprotocol', 'efp', 'webacy', 'deepdao', 'humanpassport', 'bankless', 'blockscoutbadges']
|
||||
NEXT_PUBLIC_ADDRESS_3RD_PARTY_WIDGETS_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/widgets/config.json
|
||||
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
|
||||
NEXT_PUBLIC_API_BASE_PATH=/
|
||||
NEXT_PUBLIC_API_HOST=celo.blockscout.com
|
||||
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
|
||||
NEXT_PUBLIC_CELO_ENABLED=true
|
||||
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_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
|
||||
NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swapscout','icon':'swap','dappId':'swapscout'},{'text':'Revokescout','icon':'integration/partial','dappId':'revokescout'}]
|
||||
NEXT_PUBLIC_DEX_POOLS_ENABLED=true
|
||||
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/celo.json
|
||||
NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge
|
||||
NEXT_PUBLIC_GAS_TRACKER_ENABLED=false
|
||||
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xa214ff978b8535434d5806d8e9d0d5f8c946905fbba1def121014e7af36c1e8f
|
||||
NEXT_PUBLIC_HAS_USER_OPS=true
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
|
||||
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgba(252, 255, 82, 1)'],'text_color':['rgba(0, 0, 0, 1)']}
|
||||
NEXT_PUBLIC_HOMEPAGE_STATS=['total_blocks','average_block_time','total_txs','wallet_addresses','current_epoch']
|
||||
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/0xdeval/b27a4aecaad513fa033e37430a4f9a47/raw/3a2fa70068ea27c3e6d58dc4cdbeb732968d62f3/revokescout-banner.html
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://revoke.blockscout.com?utm_source=blockscout&utm_medium=celo
|
||||
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
|
||||
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
|
||||
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
|
||||
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
|
||||
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
|
||||
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps']
|
||||
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
|
||||
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=CELO
|
||||
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=CELO
|
||||
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/celo-icon-light.svg
|
||||
NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/celo-icon-dark.svg
|
||||
NEXT_PUBLIC_NETWORK_ID=42220
|
||||
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/celo-logo-light.svg
|
||||
NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/celo-logo-dark.svg
|
||||
NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES=true
|
||||
NEXT_PUBLIC_NETWORK_NAME=Celo Mainnet
|
||||
NEXT_PUBLIC_NETWORK_RPC_URL=https://forno.celo.org
|
||||
NEXT_PUBLIC_NETWORK_SHORT_NAME=Mainnet
|
||||
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_PUZZLE_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/capyPuzzleBadge
|
||||
NEXT_PUBLIC_STATS_API_BASE_PATH=/stats-service
|
||||
NEXT_PUBLIC_STATS_API_HOST=https://celo.blockscout.com
|
||||
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
|
||||
NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=['burnt_fees']
|
||||
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
|
||||
NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true
|
||||
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
|
||||
@@ -371,6 +371,16 @@ const celoSchema = yup
|
||||
.object()
|
||||
.shape({
|
||||
NEXT_PUBLIC_CELO_ENABLED: yup.boolean(),
|
||||
NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS: yup
|
||||
.string()
|
||||
.min(42)
|
||||
.max(42)
|
||||
.matches(regexp.HEX_REGEXP_WITH_0X)
|
||||
.when('NEXT_PUBLIC_CELO_ENABLED', {
|
||||
is: (value: boolean) => value,
|
||||
then: (schema) => schema,
|
||||
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS can only be used if NEXT_PUBLIC_CELO_ENABLED is set to \'true\''),
|
||||
}),
|
||||
});
|
||||
|
||||
const apiDocsScheme = yup
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
NEXT_PUBLIC_CELO_ENABLED=true
|
||||
NEXT_PUBLIC_CELO_ENABLED=true
|
||||
NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS=0x471EcE3750Da237f93B8E339c536989b8978a438
|
||||
@@ -793,6 +793,7 @@ For blockchains that use the Celo platform. _Note_, that once the Celo mainnet b
|
||||
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| NEXT_PUBLIC_CELO_ENABLED | `boolean` | Indicates that it is a Celo-based chain. | - | - | `true` | v1.37.0+ |
|
||||
| NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS | `string` | The address of the CELO ERC-20 token. Used to exclude its balance from the net worth value of user tokens. | - | - | `0x471EcE3750Da237f93B8E339c536989b8978a438` | v2.3.0+ |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import config from 'configs/app';
|
||||
import * as cookies from 'lib/cookies';
|
||||
import type * as metadata from 'lib/metadata';
|
||||
|
||||
import detectBotRequest from '../utils/detectBotRequest';
|
||||
import { isLikelyHumanBrowser, isKnownBotRequest } from '../utils/checkRealBrowser';
|
||||
|
||||
const adBannerFeature = config.features.adsBanner;
|
||||
|
||||
@@ -45,28 +45,30 @@ Promise<GetServerSidePropsResult<Props<Pathname>>> => {
|
||||
}
|
||||
|
||||
const isTrackingDisabled = process.env.DISABLE_TRACKING === 'true';
|
||||
const isBot = Boolean(detectBotRequest(req));
|
||||
|
||||
if (!isTrackingDisabled && !isBot) {
|
||||
if (!isTrackingDisabled) {
|
||||
const isRealUser = isLikelyHumanBrowser(req) && !isKnownBotRequest(req);
|
||||
if (isRealUser) {
|
||||
// log pageview
|
||||
const hostname = req.headers.host;
|
||||
const timestamp = new Date().toISOString();
|
||||
const chainId = process.env.NEXT_PUBLIC_NETWORK_ID;
|
||||
const chainName = process.env.NEXT_PUBLIC_NETWORK_NAME;
|
||||
const publicRPC = process.env.NEXT_PUBLIC_NETWORK_RPC_URL;
|
||||
const hostname = req.headers.host;
|
||||
const timestamp = new Date().toISOString();
|
||||
const chainId = process.env.NEXT_PUBLIC_NETWORK_ID;
|
||||
const chainName = process.env.NEXT_PUBLIC_NETWORK_NAME;
|
||||
const publicRPC = process.env.NEXT_PUBLIC_NETWORK_RPC_URL;
|
||||
|
||||
fetch('https://monitor.blockscout.com/count', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
hostname,
|
||||
timestamp,
|
||||
chainId,
|
||||
chainName,
|
||||
publicRPC,
|
||||
uuid,
|
||||
}),
|
||||
});
|
||||
fetch('https://monitor.blockscout.com/count', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
hostname,
|
||||
timestamp,
|
||||
chainId,
|
||||
chainName,
|
||||
publicRPC,
|
||||
uuid,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { GetServerSidePropsContext } from 'next';
|
||||
|
||||
const GENERIC_BOT_MARKERS = [
|
||||
'bot',
|
||||
'spider',
|
||||
'crawler',
|
||||
'httpclient',
|
||||
'headlesschrome',
|
||||
'phantomjs',
|
||||
'puppeteer',
|
||||
'playwright',
|
||||
'node-fetch',
|
||||
'axios',
|
||||
'curl',
|
||||
'wget',
|
||||
'python-requests',
|
||||
'go-http-client',
|
||||
'libwww-perl',
|
||||
'okhttp',
|
||||
'postman',
|
||||
'insomnia',
|
||||
];
|
||||
|
||||
function toLower(input: string | null | undefined): string {
|
||||
return (input || '').toLowerCase();
|
||||
}
|
||||
|
||||
function isGenericBotUA(userAgent: string): boolean {
|
||||
const ua = toLower(userAgent);
|
||||
return GENERIC_BOT_MARKERS.some((m) => ua.includes(m));
|
||||
}
|
||||
|
||||
function hasBrowserHeuristics(req: GetServerSidePropsContext['req']): boolean {
|
||||
const acceptLanguage = req.headers['accept-language'];
|
||||
const secChUa = req.headers['sec-ch-ua'];
|
||||
const secFetchSite = req.headers['sec-fetch-site'];
|
||||
const secFetchMode = req.headers['sec-fetch-mode'];
|
||||
const upgradeInsecure = req.headers['upgrade-insecure-requests'];
|
||||
|
||||
const hasLang = Boolean(acceptLanguage);
|
||||
const hasSecHeaders = Boolean(secChUa || secFetchSite || secFetchMode || upgradeInsecure);
|
||||
|
||||
return hasLang && hasSecHeaders;
|
||||
}
|
||||
|
||||
export function isLikelyHumanBrowser(req: GetServerSidePropsContext['req']): boolean {
|
||||
const userAgent = req.headers['user-agent'];
|
||||
if (!userAgent) return false;
|
||||
|
||||
if (isGenericBotUA(userAgent)) return false;
|
||||
if (!hasBrowserHeuristics(req)) return false;
|
||||
|
||||
// Inclusive check for common engines + brands across desktop and mobile
|
||||
const hasEngineToken = /applewebkit|gecko/i.test(userAgent);
|
||||
const hasKnownBrowserBrand = /chrome|safari|firefox|crios|fxios|edg|opr|samsungbrowser|vivaldi|whale|duckduckgo/i.test(userAgent);
|
||||
|
||||
return hasEngineToken && hasKnownBrowserBrand;
|
||||
}
|
||||
|
||||
export function isKnownBotRequest(req: GetServerSidePropsContext['req']): boolean {
|
||||
const ua = toLower(req.headers['user-agent']);
|
||||
if (!ua) return false;
|
||||
|
||||
// Social preview bots
|
||||
if (ua.includes('twitter')) return true;
|
||||
if (ua.includes('facebook')) return true;
|
||||
if (ua.includes('telegram')) return true;
|
||||
if (ua.includes('slack')) return true;
|
||||
|
||||
// Search engine bots
|
||||
if (ua.includes('googlebot')) return true;
|
||||
if (ua.includes('bingbot')) return true;
|
||||
if (ua.includes('yahoo')) return true;
|
||||
if (ua.includes('duckduck')) return true;
|
||||
|
||||
// Generic markers
|
||||
if (isGenericBotUA(ua)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -113,6 +113,7 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
|
||||
],
|
||||
celo: [
|
||||
[ 'NEXT_PUBLIC_CELO_ENABLED', 'true' ],
|
||||
[ 'NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS', '0x471EcE3750Da237f93B8E339c536989b8978a438' ],
|
||||
],
|
||||
opSuperchain: [
|
||||
[ 'NEXT_PUBLIC_OP_SUPERCHAIN_ENABLED', 'true' ],
|
||||
|
||||
@@ -7,6 +7,7 @@ const PRESETS = {
|
||||
arbitrum_sepolia: 'https://arbitrum-sepolia.blockscout.com',
|
||||
base: 'https://base.blockscout.com',
|
||||
blackfort_testnet: 'https://blackfort-testnet.blockscout.com',
|
||||
celo: 'https://celo.blockscout.com',
|
||||
celo_alfajores: 'https://celo-alfajores.blockscout.com',
|
||||
eth: 'https://eth.blockscout.com',
|
||||
eth_goerli: 'https://eth-goerli.blockscout.com',
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { AddressTokensResponse } from 'types/api/address';
|
||||
|
||||
import * as addressMock from 'mocks/address/address';
|
||||
import * as tokensMock from 'mocks/address/tokens';
|
||||
import * as tokenInfo from 'mocks/tokens/tokenInfo';
|
||||
import * as tokenInstance from 'mocks/tokens/tokenInstance';
|
||||
import * as socketServer from 'playwright/fixtures/socketServer';
|
||||
import { test, expect, devices } from 'playwright/lib';
|
||||
@@ -268,3 +269,25 @@ test.describe('update balances via socket', () => {
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
});
|
||||
|
||||
test('native token', async({ render, mockEnvs }) => {
|
||||
await mockEnvs([
|
||||
[ 'NEXT_PUBLIC_CELO_ENABLED', 'true' ],
|
||||
[ 'NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS', tokenInfo.tokenInfoERC20c.address_hash ],
|
||||
]);
|
||||
const hooksConfig = {
|
||||
router: {
|
||||
query: { hash: ADDRESS_HASH, tab: 'tokens_erc20' },
|
||||
isReady: true,
|
||||
},
|
||||
};
|
||||
|
||||
const component = await render(
|
||||
<Box pt={{ base: '134px', lg: 6 }}>
|
||||
<AddressTokens/>
|
||||
</Box>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 45 KiB |
@@ -1,4 +1,4 @@
|
||||
import { Flex, Text, chakra, Separator, Box } from '@chakra-ui/react';
|
||||
import { Flex, Text, chakra, Separator } from '@chakra-ui/react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import type { Address3rdPartyWidget } from 'types/views/address';
|
||||
@@ -125,25 +125,15 @@ const Address3rdPartyWidgetCard = ({ name, config, address, isLoading }: Props)
|
||||
flexDirection="column"
|
||||
p={ 3 }
|
||||
cursor={ isLoading ? 'default' : 'pointer' }
|
||||
position="relative"
|
||||
zIndex={ 0 }
|
||||
borderRadius="md"
|
||||
border="1px solid"
|
||||
borderColor={ isLoading ? { _light: 'blackAlpha.50', _dark: 'whiteAlpha.100' } : 'transparent' }
|
||||
bgColor={ isLoading ? 'transparent' : { _light: 'blackAlpha.50', _dark: 'whiteAlpha.100' } }
|
||||
transition="border-color 0.2s ease-in-out"
|
||||
_hover={ isLoading ? {} : {
|
||||
borderColor: { _light: 'blackAlpha.200', _dark: 'whiteAlpha.200' },
|
||||
} }
|
||||
>
|
||||
<Box
|
||||
aria-hidden
|
||||
position="absolute"
|
||||
inset={ 0 }
|
||||
borderRadius="md"
|
||||
border="1px solid"
|
||||
borderColor={ isLoading ? { _light: 'theme.stats.bg._light', _dark: 'theme.stats.bg._dark' } : 'transparent' }
|
||||
bgColor={ isLoading ? 'transparent' : { _light: 'theme.stats.bg._light', _dark: 'theme.stats.bg._dark' } }
|
||||
transform="scale(1)"
|
||||
transition="transform 0.2s ease-in-out, border-color 0.2s ease-in-out"
|
||||
zIndex={ -1 }
|
||||
_groupHover={{
|
||||
transform: 'scale(1.02)',
|
||||
borderColor: { _light: 'blackAlpha.50', _dark: 'whiteAlpha.100' },
|
||||
}}
|
||||
/>
|
||||
{ content }
|
||||
</LinkBox>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
|
||||
import * as addressMock from 'mocks/address/address';
|
||||
import * as tokensMock from 'mocks/address/tokens';
|
||||
import { tokenInfoERC20a } from 'mocks/tokens/tokenInfo';
|
||||
import { tokenInfoERC20c, tokenInfoERC20a } from 'mocks/tokens/tokenInfo';
|
||||
import { test, expect, devices } from 'playwright/lib';
|
||||
import MockAddressPage from 'ui/address/testUtils/MockAddressPage';
|
||||
|
||||
@@ -126,3 +126,22 @@ test('long values', async({ render, page, mockApiResponse }) => {
|
||||
|
||||
await expect(page).toHaveScreenshot({ clip: CLIPPING_AREA });
|
||||
});
|
||||
|
||||
test('native token', async({ render, mockEnvs, page }) => {
|
||||
await mockEnvs([
|
||||
[ 'NEXT_PUBLIC_CELO_ENABLED', 'true' ],
|
||||
[ 'NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS', tokenInfoERC20c.address_hash ],
|
||||
]);
|
||||
|
||||
await render(
|
||||
<MockAddressPage>
|
||||
<Flex>
|
||||
<TokenSelect/>
|
||||
</Flex>
|
||||
</MockAddressPage>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
await page.getByRole('button', { name: /select/i }).click();
|
||||
|
||||
await expect(page).toHaveScreenshot({ clip: CLIPPING_AREA });
|
||||
});
|
||||
|
||||
@@ -4,19 +4,25 @@ import React from 'react';
|
||||
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
import config from 'configs/app';
|
||||
import getCurrencyValue from 'lib/getCurrencyValue';
|
||||
import { Link } from 'toolkit/chakra/link';
|
||||
import NativeTokenTag from 'ui/shared/celo/NativeTokenTag';
|
||||
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
|
||||
import TruncatedValue from 'ui/shared/TruncatedValue';
|
||||
|
||||
import type { TokenEnhancedData } from '../utils/tokenUtils';
|
||||
|
||||
const celoFeature = config.features.celo;
|
||||
|
||||
interface Props {
|
||||
data: TokenEnhancedData;
|
||||
}
|
||||
|
||||
const TokenSelectItem = ({ data }: Props) => {
|
||||
|
||||
const isNativeToken = celoFeature.isEnabled && data.token.address_hash.toLowerCase() === celoFeature.nativeTokenAddress?.toLowerCase();
|
||||
|
||||
const secondRow = (() => {
|
||||
switch (data.token.type) {
|
||||
case 'ERC-20': {
|
||||
@@ -93,13 +99,22 @@ const TokenSelectItem = ({ data }: Props) => {
|
||||
noCopy
|
||||
noLink
|
||||
fontWeight={ 700 }
|
||||
width="auto"
|
||||
mr={ 2 }
|
||||
/>
|
||||
{ isNativeToken && <NativeTokenTag mr={ 2 }/> }
|
||||
{ data.usd && (
|
||||
<TruncatedValue value={ `$${ data.usd.toFormat(2) }` } fontWeight={ 700 } minW="120px" ml="auto" textAlign="right"/>
|
||||
<TruncatedValue
|
||||
value={ `$${ data.usd.toFormat(2) }` }
|
||||
fontWeight={ 700 }
|
||||
minW="120px"
|
||||
ml="auto"
|
||||
textAlign="right"
|
||||
color={ isNativeToken ? 'text.secondary' : undefined }
|
||||
/>
|
||||
) }
|
||||
</Flex>
|
||||
<Flex alignItems="center" justifyContent="space-between" w="100%" whiteSpace="nowrap">
|
||||
<Flex alignItems="center" justifyContent="space-between" w="100%" whiteSpace="nowrap" color={ isNativeToken ? 'text.secondary' : undefined }>
|
||||
{ secondRow }
|
||||
</Flex>
|
||||
</Link>
|
||||
|
||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 34 KiB |
@@ -3,13 +3,17 @@ import React from 'react';
|
||||
|
||||
import type { AddressTokenBalance } from 'types/api/address';
|
||||
|
||||
import config from 'configs/app';
|
||||
import getCurrencyValue from 'lib/getCurrencyValue';
|
||||
import { Skeleton } from 'toolkit/chakra/skeleton';
|
||||
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
|
||||
import NativeTokenTag from 'ui/shared/celo/NativeTokenTag';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
|
||||
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
|
||||
|
||||
const celoFeature = config.features.celo;
|
||||
|
||||
type Props = AddressTokenBalance & { isLoading: boolean };
|
||||
|
||||
const ERC20TokensListItem = ({ token, value, isLoading }: Props) => {
|
||||
@@ -19,16 +23,20 @@ const ERC20TokensListItem = ({ token, value, isLoading }: Props) => {
|
||||
usd: tokenValue,
|
||||
} = getCurrencyValue({ value: value, exchangeRate: token.exchange_rate, decimals: token.decimals, accuracy: 8, accuracyUsd: 2 });
|
||||
|
||||
const isNativeToken = celoFeature.isEnabled && token.address_hash.toLowerCase() === celoFeature.nativeTokenAddress?.toLowerCase();
|
||||
|
||||
return (
|
||||
<ListItemMobile rowGap={ 2 }>
|
||||
<Flex alignItems="center" width="100%">
|
||||
<Flex alignItems="center" width="100%" columnGap={ 2 }>
|
||||
<TokenEntity
|
||||
token={ token }
|
||||
isLoading={ isLoading }
|
||||
noCopy
|
||||
jointSymbol
|
||||
fontWeight="700"
|
||||
width="auto"
|
||||
/>
|
||||
{ isNativeToken && <NativeTokenTag/> }
|
||||
</Flex>
|
||||
<Flex alignItems="center" pl={ 8 }>
|
||||
<AddressEntity
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { Flex, HStack } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { AddressTokenBalance } from 'types/api/address';
|
||||
|
||||
import config from 'configs/app';
|
||||
import getCurrencyValue from 'lib/getCurrencyValue';
|
||||
import { Skeleton } from 'toolkit/chakra/skeleton';
|
||||
import { TableCell, TableRow } from 'toolkit/chakra/table';
|
||||
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
|
||||
import NativeTokenTag from 'ui/shared/celo/NativeTokenTag';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
|
||||
|
||||
type Props = AddressTokenBalance & { isLoading: boolean };
|
||||
|
||||
const celoFeature = config.features.celo;
|
||||
|
||||
const ERC20TokensTableItem = ({
|
||||
token,
|
||||
value,
|
||||
@@ -23,16 +27,22 @@ const ERC20TokensTableItem = ({
|
||||
usd: tokenValue,
|
||||
} = getCurrencyValue({ value: value, exchangeRate: token.exchange_rate, decimals: token.decimals, accuracy: 8, accuracyUsd: 2 });
|
||||
|
||||
const isNativeToken = celoFeature.isEnabled && token.address_hash.toLowerCase() === celoFeature.nativeTokenAddress?.toLowerCase();
|
||||
|
||||
return (
|
||||
<TableRow role="group" >
|
||||
<TableCell verticalAlign="middle">
|
||||
<TokenEntity
|
||||
token={ token }
|
||||
isLoading={ isLoading }
|
||||
noCopy
|
||||
jointSymbol
|
||||
fontWeight="700"
|
||||
/>
|
||||
<HStack gap={ 2 }>
|
||||
<TokenEntity
|
||||
token={ token }
|
||||
isLoading={ isLoading }
|
||||
noCopy
|
||||
jointSymbol
|
||||
fontWeight="700"
|
||||
width="auto"
|
||||
/>
|
||||
{ isNativeToken && <NativeTokenTag/> }
|
||||
</HStack>
|
||||
</TableCell>
|
||||
<TableCell verticalAlign="middle">
|
||||
<Flex alignItems="center" width="150px" justifyContent="space-between">
|
||||
@@ -46,17 +56,17 @@ const ERC20TokensTableItem = ({
|
||||
</Flex>
|
||||
</TableCell>
|
||||
<TableCell isNumeric verticalAlign="middle">
|
||||
<Skeleton loading={ isLoading } display="inline-block">
|
||||
<Skeleton loading={ isLoading } display="inline-block" color={ isNativeToken ? 'text.secondary' : undefined }>
|
||||
{ token.exchange_rate && `$${ Number(token.exchange_rate).toLocaleString() }` }
|
||||
</Skeleton>
|
||||
</TableCell>
|
||||
<TableCell isNumeric verticalAlign="middle">
|
||||
<Skeleton loading={ isLoading } display="inline">
|
||||
<Skeleton loading={ isLoading } display="inline" color={ isNativeToken ? 'text.secondary' : undefined }>
|
||||
{ tokenQuantity }
|
||||
</Skeleton>
|
||||
</TableCell>
|
||||
<TableCell isNumeric verticalAlign="middle">
|
||||
<Skeleton loading={ isLoading } display="inline">
|
||||
<Skeleton loading={ isLoading } display="inline" color={ isNativeToken ? 'text.secondary' : undefined }>
|
||||
{ tokenValue && `$${ tokenValue }` }
|
||||
</Skeleton>
|
||||
</TableCell>
|
||||
|
||||
@@ -3,9 +3,15 @@ import BigNumber from 'bignumber.js';
|
||||
import type { AddressTokenBalance } from 'types/api/address';
|
||||
import type { TokenType } from 'types/api/token';
|
||||
|
||||
import config from 'configs/app';
|
||||
import sumBnReducer from 'lib/bigint/sumBnReducer';
|
||||
import { ZERO } from 'toolkit/utils/consts';
|
||||
|
||||
const celoFeature = config.features.celo;
|
||||
|
||||
const isNativeToken = (token: TokenEnhancedData) =>
|
||||
celoFeature.isEnabled && token.token.address_hash.toLowerCase() === celoFeature.nativeTokenAddress?.toLowerCase();
|
||||
|
||||
export type TokenEnhancedData = AddressTokenBalance & {
|
||||
usd?: BigNumber ;
|
||||
};
|
||||
@@ -94,7 +100,7 @@ export const calculateUsdValue = (data: AddressTokenBalance): TokenEnhancedData
|
||||
|
||||
export const getTokensTotalInfo = (data: TokenSelectData) => {
|
||||
const usd = Object.values(data)
|
||||
.map(({ items }) => items.reduce(usdValueReducer, ZERO))
|
||||
.map(({ items }) => items.filter((item) => !isNativeToken(item)).reduce(usdValueReducer, ZERO))
|
||||
.reduce(sumBnReducer, ZERO);
|
||||
|
||||
const num = Object.values(data)
|
||||
|
||||
@@ -26,9 +26,14 @@ export const REST_API_SECTIONS = [
|
||||
url: feature.coreApiSwaggerUrl,
|
||||
requestInterceptor: (req: SwaggerRequest) => {
|
||||
const DEFAULT_SERVER = 'blockscout.com/poa/core';
|
||||
const DEFAULT_SERVER_NEW = 'eth.blockscout.com';
|
||||
|
||||
if (!req.loadSpec) {
|
||||
const newUrl = new URL(req.url.replace(DEFAULT_SERVER, config.apis.general.host));
|
||||
const newUrl = new URL(
|
||||
req.url
|
||||
.replace(DEFAULT_SERVER, config.apis.general.host)
|
||||
.replace(DEFAULT_SERVER_NEW, config.apis.general.host),
|
||||
);
|
||||
|
||||
newUrl.protocol = config.apis.general.protocol + ':';
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import EthRpcApi from 'ui/apiDocs/EthRpcApi';
|
||||
import GraphQL from 'ui/apiDocs/GraphQL';
|
||||
import RestApi from 'ui/apiDocs/RestApi';
|
||||
import RpcApi from 'ui/apiDocs/RpcApi';
|
||||
import { REST_API_SECTIONS } from 'ui/apiDocs/utils';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
|
||||
const feature = config.features.apiDocs;
|
||||
@@ -17,7 +16,7 @@ const feature = config.features.apiDocs;
|
||||
const ApiDocs = () => {
|
||||
|
||||
const tabs: Array<TabItemRegular> = [
|
||||
{ id: 'rest_api', title: 'REST API', component: <RestApi/>, count: REST_API_SECTIONS.length },
|
||||
{ id: 'rest_api', title: 'REST API', component: <RestApi/> },
|
||||
{ id: 'eth_rpc_api', title: 'ETH RPC API', component: <EthRpcApi/> },
|
||||
{ id: 'rpc_api', title: 'RPC API endpoints', component: <RpcApi/> },
|
||||
{ id: 'graphql_api', title: 'GraphQL API', component: <GraphQL/> },
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
|
||||
import type { TokenType } from 'types/api/token';
|
||||
|
||||
import { apos } from 'toolkit/utils/htmlEntities';
|
||||
import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
|
||||
import DataListDisplay from 'ui/shared/DataListDisplay';
|
||||
import PopoverFilter from 'ui/shared/filters/PopoverFilter';
|
||||
@@ -61,6 +62,10 @@ const TokenTransfers = () => {
|
||||
itemsNum={ query.data?.items.length }
|
||||
emptyText="There are no token transfers."
|
||||
actionBar={ actionBar }
|
||||
filterProps={{
|
||||
hasActiveFilters: Boolean(typeFilter.length),
|
||||
emptyFilteredText: `Couldn${ apos }t find any token transfer that matches your query.`,
|
||||
}}
|
||||
>
|
||||
{ content }
|
||||
</DataListDisplay>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { Link } from 'toolkit/chakra/link';
|
||||
import { Tag, type TagProps } from 'toolkit/chakra/tag';
|
||||
import { Tooltip } from 'toolkit/chakra/tooltip';
|
||||
|
||||
const feature = config.features.celo;
|
||||
|
||||
interface Props extends TagProps {}
|
||||
|
||||
const NativeTokenTag = (props: Props) => {
|
||||
|
||||
const handleLinkClick = React.useCallback((event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
|
||||
if (!feature.isEnabled || !feature.nativeTokenAddress) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tooltipContent = (
|
||||
<>
|
||||
<span>This ERC-20 token represents the native CELO balance for this address and isn’t counted twice. </span>
|
||||
<Link href="https://docs.celo.org/what-is-celo/using-celo/protocol/celo-token" external onClick={ handleLinkClick }>
|
||||
Learn more
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip content={ tooltipContent } interactive>
|
||||
<Tag { ...props }>Native token</Tag>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NativeTokenTag);
|
||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
@@ -11,7 +11,7 @@ import NavigationPromoBannerContent from './NavigationPromoBannerContent';
|
||||
|
||||
const PROMO_BANNER_CLOSED_HASH_KEY = 'nav-promo-banner-closed-hash';
|
||||
const promoBanner = config.UI.navigation.promoBanner;
|
||||
const isHorizontalNavigation = config.UI.navigation.layout === 'horizontal';
|
||||
const isHorizontal = config.UI.navigation.layout === 'horizontal';
|
||||
|
||||
type Props = {
|
||||
isCollapsed?: boolean;
|
||||
@@ -20,6 +20,7 @@ type Props = {
|
||||
const NavigationPromoBanner = ({ isCollapsed }: Props) => {
|
||||
const isMobile = useIsMobile();
|
||||
const isXLScreen = useBreakpointValue({ base: false, xl: true });
|
||||
const isHorizontalNavigation = isHorizontal && !isMobile;
|
||||
|
||||
const [ isShown, setIsShown ] = useState(false);
|
||||
const [ promoBannerHash, setPromoBannerHash ] = useState('');
|
||||
@@ -98,10 +99,11 @@ const NavigationPromoBanner = ({ isCollapsed }: Props) => {
|
||||
color={{ _light: 'gray.300', _dark: 'gray.600' }}
|
||||
bgColor="bg.primary"
|
||||
borderBottomLeftRadius="sm"
|
||||
borderTopRightRadius="sm"
|
||||
position="absolute"
|
||||
top="0"
|
||||
right="0"
|
||||
display="none"
|
||||
display={ isMobile ? 'block' : 'none' }
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
|
||||
@@ -26,84 +26,118 @@ const NetworkMenuContent = ({ items, tabs }: Props) => {
|
||||
setValue(value as NetworkGroup);
|
||||
}, []);
|
||||
|
||||
const content = !items || items.length === 0 ? (
|
||||
<>
|
||||
<Flex alignItems="center">
|
||||
<Flex h="32px" w="105px" bgColor={{ base: 'blackAlpha.50', _dark: 'whiteAlpha.50' }} borderRadius="base" px={ 4 } py={ 2 }>
|
||||
<Skeleton loading h="16px" w="100%"/>
|
||||
</Flex>
|
||||
<Skeleton loading h="16px" w="68px" mx={ 4 }/>
|
||||
<Skeleton loading h="16px" w="45px" mx={ 4 }/>
|
||||
</Flex>
|
||||
<Flex mt={ 3 } flexDir="column" rowGap={ 2 }>
|
||||
<Flex mx={ 3 } my={ 2 } alignItems="center">
|
||||
<Skeleton loading h="30px" w="30px" borderRadius="full"/>
|
||||
<Skeleton loading h="16px" w="120px" ml={ 3 }/>
|
||||
</Flex>
|
||||
<Flex mx={ 3 } my={ 2 } alignItems="center">
|
||||
<Skeleton loading h="30px" w="30px" borderRadius="full"/>
|
||||
<Skeleton loading h="16px" w="180px" ml={ 3 }/>
|
||||
</Flex>
|
||||
<Flex mx={ 3 } my={ 2 } alignItems="center">
|
||||
<Skeleton loading h="30px" w="30px" borderRadius="full"/>
|
||||
<Skeleton loading h="16px" w="150px" ml={ 3 }/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</>
|
||||
) : (
|
||||
<TabsRoot
|
||||
variant="segmented"
|
||||
width="full"
|
||||
fitted
|
||||
size="sm"
|
||||
lazyMount
|
||||
value={ value }
|
||||
onValueChange={ handleTabChange }
|
||||
>
|
||||
{ tabs.length > 1 && (
|
||||
<TabsList mb={ 2 } width="full">
|
||||
{ tabs.map((tab) => (
|
||||
<TabsTrigger
|
||||
key={ tab }
|
||||
textTransform="capitalize"
|
||||
value={ tab }
|
||||
const content = (() => {
|
||||
if (!items || items.length === 0) {
|
||||
return (
|
||||
<>
|
||||
<Flex alignItems="center">
|
||||
<Flex h="32px" w="105px" bgColor={{ base: 'blackAlpha.50', _dark: 'whiteAlpha.50' }} borderRadius="base" px={ 4 } py={ 2 }>
|
||||
<Skeleton loading h="16px" w="100%"/>
|
||||
</Flex>
|
||||
<Skeleton loading h="16px" w="68px" mx={ 4 }/>
|
||||
<Skeleton loading h="16px" w="45px" mx={ 4 }/>
|
||||
</Flex>
|
||||
<Flex mt={ 3 } flexDir="column" rowGap={ 2 }>
|
||||
<Flex mx={ 3 } my={ 2 } alignItems="center">
|
||||
<Skeleton loading h="30px" w="30px" borderRadius="full"/>
|
||||
<Skeleton loading h="16px" w="120px" ml={ 3 }/>
|
||||
</Flex>
|
||||
<Flex mx={ 3 } my={ 2 } alignItems="center">
|
||||
<Skeleton loading h="30px" w="30px" borderRadius="full"/>
|
||||
<Skeleton loading h="16px" w="180px" ml={ 3 }/>
|
||||
</Flex>
|
||||
<Flex mx={ 3 } my={ 2 } alignItems="center">
|
||||
<Skeleton loading h="30px" w="30px" borderRadius="full"/>
|
||||
<Skeleton loading h="16px" w="150px" ml={ 3 }/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (tabs.length === 1) {
|
||||
return (
|
||||
<VStack as="ul" gap={ 1 } alignItems="stretch" overflowY="scroll" maxH="516px">
|
||||
{ items
|
||||
.filter((network) => network.group === tabs[0])
|
||||
.map((network) => (
|
||||
<NetworkMenuLink
|
||||
key={ network.title }
|
||||
{ ...network }
|
||||
/>
|
||||
)) }
|
||||
{ config.UI.featuredNetworks.allLink && (
|
||||
<Link
|
||||
href={ config.UI.featuredNetworks.allLink }
|
||||
external
|
||||
noIcon
|
||||
variant="secondary"
|
||||
my={ 2 }
|
||||
px={ 2 }
|
||||
fontSize="xs"
|
||||
>
|
||||
{ tab }
|
||||
</TabsTrigger>
|
||||
View all chains
|
||||
</Link>
|
||||
) }
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TabsRoot
|
||||
variant="segmented"
|
||||
width="full"
|
||||
fitted
|
||||
size="sm"
|
||||
lazyMount
|
||||
value={ value }
|
||||
onValueChange={ handleTabChange }
|
||||
>
|
||||
{ tabs.length > 1 && (
|
||||
<TabsList mb={ 2 } width="full">
|
||||
{ tabs.map((tab) => (
|
||||
<TabsTrigger
|
||||
key={ tab }
|
||||
textTransform="capitalize"
|
||||
value={ tab }
|
||||
>
|
||||
{ tab }
|
||||
</TabsTrigger>
|
||||
)) }
|
||||
</TabsList>
|
||||
) }
|
||||
<Box>
|
||||
{ tabs.map((tab) => (
|
||||
<TabsContent key={ tab } value={ tab } p={ 0 }>
|
||||
<VStack as="ul" gap={ 1 } alignItems="stretch" overflowY="scroll" maxH="516px">
|
||||
{ items
|
||||
.filter((network) => network.group === tab)
|
||||
.map((network) => (
|
||||
<NetworkMenuLink
|
||||
key={ network.title }
|
||||
{ ...network }
|
||||
/>
|
||||
)) }
|
||||
{ config.UI.featuredNetworks.allLink && (
|
||||
<Link
|
||||
href={ config.UI.featuredNetworks.allLink }
|
||||
external
|
||||
noIcon
|
||||
variant="secondary"
|
||||
my={ 2 }
|
||||
px={ 2 }
|
||||
fontSize="xs"
|
||||
>
|
||||
View all chains
|
||||
</Link>
|
||||
) }
|
||||
</VStack>
|
||||
</TabsContent>
|
||||
)) }
|
||||
</TabsList>
|
||||
) }
|
||||
<Box>
|
||||
{ tabs.map((tab) => (
|
||||
<TabsContent key={ tab } value={ tab } p={ 0 }>
|
||||
<VStack as="ul" gap={ 1 } alignItems="stretch" overflowY="scroll" maxH="516px">
|
||||
{ items
|
||||
.filter((network) => network.group === tab)
|
||||
.map((network) => (
|
||||
<NetworkMenuLink
|
||||
key={ network.title }
|
||||
{ ...network }
|
||||
/>
|
||||
)) }
|
||||
{ config.UI.featuredNetworks.allLink && (
|
||||
<Link
|
||||
href={ config.UI.featuredNetworks.allLink }
|
||||
external
|
||||
noIcon
|
||||
variant="secondary"
|
||||
my={ 2 }
|
||||
px={ 2 }
|
||||
fontSize="xs"
|
||||
>
|
||||
View all chains
|
||||
</Link>
|
||||
) }
|
||||
</VStack>
|
||||
</TabsContent>
|
||||
)) }
|
||||
</Box>
|
||||
</TabsRoot>
|
||||
);
|
||||
</Box>
|
||||
</TabsRoot>
|
||||
);
|
||||
})();
|
||||
|
||||
return (
|
||||
<PopoverContent w="290px" maxH="unset">
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Flex, Separator, Box } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { CONTENT_MAX_WIDTH } from 'ui/shared/layout/utils';
|
||||
|
||||
import DeFiDropdown from './DeFiDropdown';
|
||||
import NetworkMenu from './NetworkMenu';
|
||||
@@ -18,6 +19,7 @@ const TopBar = () => {
|
||||
m="0 auto"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
maxW={ `${ CONTENT_MAX_WIDTH }px` }
|
||||
>
|
||||
{ !config.features.opSuperchain.isEnabled ? <TopBarStats/> : <div/> }
|
||||
<Flex alignItems="center">
|
||||
|
||||
@@ -14,14 +14,17 @@ interface Props {
|
||||
const TokenHoldersList = ({ data, token, isLoading }: Props) => {
|
||||
return (
|
||||
<Box>
|
||||
{ data.map((item, index) => (
|
||||
<TokenHoldersListItem
|
||||
key={ item.address.hash + (isLoading ? index : '') }
|
||||
token={ token }
|
||||
holder={ item }
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
)) }
|
||||
{ data.map((item, index) => {
|
||||
const tokenId = 'token_id' in item ? item.token_id : null;
|
||||
return (
|
||||
<TokenHoldersListItem
|
||||
key={ item.address.hash + tokenId + (isLoading ? index : '') }
|
||||
token={ token }
|
||||
holder={ item }
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
);
|
||||
}) }
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { TokenHolder, TokenInfo } from 'types/api/token';
|
||||
import { Skeleton } from 'toolkit/chakra/skeleton';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
|
||||
import TruncatedValue from 'ui/shared/TruncatedValue';
|
||||
import Utilization from 'ui/shared/Utilization/Utilization';
|
||||
|
||||
interface Props {
|
||||
@@ -33,9 +34,7 @@ const TokenHoldersListItem = ({ holder, token, isLoading }: Props) => {
|
||||
<>
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>ID#</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<Skeleton loading={ isLoading } display="inline-block">
|
||||
{ holder.token_id }
|
||||
</Skeleton>
|
||||
<TruncatedValue value={ holder.token_id } isLoading={ isLoading } w="100%"/>
|
||||
</ListItemMobileGrid.Value>
|
||||
</>
|
||||
) }
|
||||
|
||||
@@ -24,9 +24,12 @@ const TokenHoldersTable = ({ data, token, top, isLoading }: Props) => {
|
||||
</TableRow>
|
||||
</TableHeaderSticky>
|
||||
<TableBody>
|
||||
{ data.map((item, index) => (
|
||||
<TokenHoldersTableItem key={ item.address.hash + (isLoading ? index : '') } holder={ item } token={ token } isLoading={ isLoading }/>
|
||||
)) }
|
||||
{ data.map((item, index) => {
|
||||
const tokenId = 'token_id' in item ? item.token_id : null;
|
||||
return (
|
||||
<TokenHoldersTableItem key={ item.address.hash + tokenId + (isLoading ? index : '') } holder={ item } token={ token } isLoading={ isLoading }/>
|
||||
);
|
||||
}) }
|
||||
</TableBody>
|
||||
</TableRoot>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { TokenHolder, TokenInfo } from 'types/api/token';
|
||||
import { Skeleton } from 'toolkit/chakra/skeleton';
|
||||
import { TableCell, TableRow } from 'toolkit/chakra/table';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import TruncatedValue from 'ui/shared/TruncatedValue';
|
||||
import Utilization from 'ui/shared/Utilization/Utilization';
|
||||
|
||||
type Props = {
|
||||
@@ -29,9 +30,7 @@ const TokenTransferTableItem = ({ holder, token, isLoading }: Props) => {
|
||||
</TableCell>
|
||||
{ (token.type === 'ERC-1155' || token.type === 'ERC-404') && 'token_id' in holder && (
|
||||
<TableCell verticalAlign="middle">
|
||||
<Skeleton loading={ isLoading } display="inline-block">
|
||||
{ 'token_id' in holder && holder.token_id }
|
||||
</Skeleton>
|
||||
<TruncatedValue value={ holder.token_id } isLoading={ isLoading } w="100%"/>
|
||||
</TableCell>
|
||||
) }
|
||||
<TableCell verticalAlign="middle" isNumeric>
|
||||
|
||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 19 KiB |
@@ -22,7 +22,7 @@ const TxDetails = ({ txQuery, tacOperationQuery }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<TestnetWarning mb={ 6 } isLoading={ txQuery.isPlaceholderData }/>
|
||||
<TestnetWarning mb={{ base: 3, lg: 6 }} isLoading={ txQuery.isPlaceholderData }/>
|
||||
<TxInfo
|
||||
data={ txQuery.data }
|
||||
tacOperations={ tacOperationQuery?.data?.items }
|
||||
|
||||
@@ -152,7 +152,7 @@ const TxDetailsDegraded = ({ hash, txQuery }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex rowGap={ 2 } mb={ 6 } flexDir="column">
|
||||
<Flex rowGap={{ base: 1, lg: 2 }} mb={{ base: 3, lg: 6 }} flexDir="column">
|
||||
<TestnetWarning isLoading={ query.isPlaceholderData }/>
|
||||
{ originalError?.status !== 404 && <ServiceDegradationWarning isLoading={ query.isPlaceholderData }/> }
|
||||
</Flex>
|
||||
|
||||
@@ -141,9 +141,9 @@ const TxInfo = ({ data, tacOperations, isLoading, socketStatus }: Props) => {
|
||||
|
||||
{ config.features.metasuites.isEnabled && (
|
||||
<>
|
||||
<Box display="none" id="meta-suites__tx-info-label" data-status={ data.status } data-ready={ !isLoading }/>
|
||||
<Box display="none" id="meta-suites__tx-info-value"/>
|
||||
<DetailedInfo.ItemDivider display="none" id="meta-suites__details-info-item-divider"/>
|
||||
<Box display="none" as="p" id="meta-suites__tx-info-label" data-status={ data.status } data-ready={ !isLoading }/>
|
||||
<Box display="none" as="p" id="meta-suites__tx-info-value"/>
|
||||
<DetailedInfo.ItemDivider display="none" as="p" id="meta-suites__details-info-item-divider"/>
|
||||
</>
|
||||
) }
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { Transaction, TransactionsSortingField, TransactionsSortingValue }
|
||||
import type { PaginationParams } from 'ui/shared/pagination/types';
|
||||
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import { apos } from 'toolkit/utils/htmlEntities';
|
||||
import AddressCsvExportLink from 'ui/address/AddressCsvExportLink';
|
||||
import { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
|
||||
import DataListDisplay from 'ui/shared/DataListDisplay';
|
||||
@@ -119,6 +120,10 @@ const TxsContent = ({
|
||||
itemsNum={ itemsWithTranslation?.length }
|
||||
emptyText="There are no transactions."
|
||||
actionBar={ actionBar }
|
||||
filterProps={{
|
||||
hasActiveFilters: Boolean(filterValue),
|
||||
emptyFilteredText: `Couldn${ apos }t find any transaction that matches your query.`,
|
||||
}}
|
||||
>
|
||||
{ content }
|
||||
</DataListDisplay>
|
||||
|
||||