diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index dcd5ca7609..82faf7cb34 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -95,6 +95,8 @@ jobs: steps: - name: Checkout repo uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup node uses: actions/setup-node@v4 @@ -115,7 +117,7 @@ jobs: run: yarn --frozen-lockfile --ignore-optional - name: Run Jest - run: yarn test:jest --onlyChanged=${{ github.event_name == 'pull_request' }} --passWithNoTests + run: yarn test:jest ${{ github.event_name == 'pull_request' && '--changedSince=origin/main' || '' }} --passWithNoTests pw_affected_tests: name: Resolve affected Playwright tests diff --git a/.vscode/tasks.json b/.vscode/tasks.json index d8682527e6..c0b016dabe 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -338,14 +338,15 @@ "options": [ "main", "main.L2", - "poa_core", + "eth", "eth_goerli", "sepolia", - "eth", - "rootstock", "polygon", "zkevm", "gnosis", + "rootstock", + "stability", + "poa_core", "localhost", ], "default": "main" diff --git a/configs/app/features/gasTracker.ts b/configs/app/features/gasTracker.ts new file mode 100644 index 0000000000..c20242e602 --- /dev/null +++ b/configs/app/features/gasTracker.ts @@ -0,0 +1,37 @@ +import type { Feature } from './types'; +import { GAS_UNITS } from 'types/client/gasTracker'; +import type { GasUnit } from 'types/client/gasTracker'; + +import { getEnvValue, parseEnvJson } from '../utils'; + +const isDisabled = getEnvValue('NEXT_PUBLIC_GAS_TRACKER_ENABLED') === 'false'; + +const units = ((): Array => { + const envValue = getEnvValue('NEXT_PUBLIC_GAS_TRACKER_UNITS'); + if (!envValue) { + return [ 'usd', 'gwei' ]; + } + + const units = parseEnvJson>(envValue)?.filter((type) => GAS_UNITS.includes(type)) || []; + + return units; +})(); + +const title = 'Gas tracker'; + +const config: Feature<{ units: Array }> = (() => { + if (!isDisabled && units.length > 0) { + return Object.freeze({ + title, + isEnabled: true, + units, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/index.ts b/configs/app/features/index.ts index c504f4de7d..e2aadfda04 100644 --- a/configs/app/features/index.ts +++ b/configs/app/features/index.ts @@ -6,10 +6,12 @@ export { default as beaconChain } from './beaconChain'; export { default as bridgedTokens } from './bridgedTokens'; export { default as blockchainInteraction } from './blockchainInteraction'; export { default as csvExport } from './csvExport'; +export { default as gasTracker } from './gasTracker'; export { default as googleAnalytics } from './googleAnalytics'; export { default as graphqlApiDocs } from './graphqlApiDocs'; export { default as growthBook } from './growthBook'; export { default as marketplace } from './marketplace'; +export { default as metasuites } from './metasuites'; export { default as mixpanel } from './mixpanel'; export { default as nameService } from './nameService'; export { default as restApiDocs } from './restApiDocs'; @@ -22,5 +24,6 @@ export { default as suave } from './suave'; export { default as swapButton } from './swapButton'; export { default as txInterpretation } from './txInterpretation'; export { default as userOps } from './userOps'; +export { default as validators } from './validators'; export { default as verifiedTokens } from './verifiedTokens'; export { default as web3Wallet } from './web3Wallet'; diff --git a/configs/app/features/marketplace.ts b/configs/app/features/marketplace.ts index e831df7a6c..288c14a125 100644 --- a/configs/app/features/marketplace.ts +++ b/configs/app/features/marketplace.ts @@ -4,6 +4,7 @@ import chain from '../chain'; import { getEnvValue, getExternalAssetFilePath } from '../utils'; // config file will be downloaded at run-time and saved in the public folder +const enabled = getEnvValue('NEXT_PUBLIC_MARKETPLACE_ENABLED'); const configUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL'); const submitFormUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM'); const suggestIdeasFormUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM'); @@ -17,7 +18,7 @@ const config: Feature<( { api: { endpoint: string; basePath: string } } ) & { submitFormUrl: string; categoriesUrl: string | undefined; suggestIdeasFormUrl: string | undefined } > = (() => { - if (chain.rpcUrl && submitFormUrl) { + if (enabled === 'true' && chain.rpcUrl && submitFormUrl) { if (configUrl) { return Object.freeze({ title, diff --git a/configs/app/features/metasuites.ts b/configs/app/features/metasuites.ts new file mode 100644 index 0000000000..333e7d5a8a --- /dev/null +++ b/configs/app/features/metasuites.ts @@ -0,0 +1,21 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; + +const title = 'MetaSuites extension'; + +const config: Feature<{ isEnabled: true }> = (() => { + if (getEnvValue('NEXT_PUBLIC_METASUITES_ENABLED') === 'true') { + return Object.freeze({ + title, + isEnabled: true, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/safe.ts b/configs/app/features/safe.ts index bed8bf14a6..b2762a78da 100644 --- a/configs/app/features/safe.ts +++ b/configs/app/features/safe.ts @@ -1,35 +1,14 @@ import type { Feature } from './types'; -import chain from '../chain'; - -// https://docs.safe.global/safe-core-api/available-services -const SAFE_API_MAP: Record = { - '42161': 'https://safe-transaction-arbitrum.safe.global', - '1313161554': 'https://safe-transaction-aurora.safe.global', - '43114': 'https://safe-transaction-avalanche.safe.global', - '8453': 'https://safe-transaction-base.safe.global', - '84531': 'https://safe-transaction-base-testnet.safe.global', - '56': 'https://safe-transaction-bsc.safe.global', - '42220': 'https://safe-transaction-celo.safe.global', - '1': 'https://safe-transaction-mainnet.safe.global', - '100': 'https://safe-transaction-gnosis-chain.safe.global', - '5': 'https://safe-transaction-goerli.safe.global', - '10': 'https://safe-transaction-optimism.safe.global', - '137': 'https://safe-transaction-polygon.safe.global', -}; +import { getEnvValue } from '../utils'; function getApiUrl(): string | undefined { - if (!chain.id) { - return; - } - - const apiHost = SAFE_API_MAP[chain.id]; - - if (!apiHost) { + try { + const envValue = getEnvValue('NEXT_PUBLIC_SAFE_TX_SERVICE_URL'); + return new URL('/api/v1/safes', envValue).toString(); + } catch (error) { return; } - - return `${ apiHost }/api/v1/safes/`; } const title = 'Safe address tags'; diff --git a/configs/app/features/validators.ts b/configs/app/features/validators.ts new file mode 100644 index 0000000000..668501e28c --- /dev/null +++ b/configs/app/features/validators.ts @@ -0,0 +1,29 @@ +import type { Feature } from './types'; +import { VALIDATORS_CHAIN_TYPE } from 'types/client/validators'; +import type { ValidatorsChainType } from 'types/client/validators'; + +import { getEnvValue } from '../utils'; + +const chainType = ((): ValidatorsChainType | undefined => { + const envValue = getEnvValue('NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE') as ValidatorsChainType | undefined; + return envValue && VALIDATORS_CHAIN_TYPE.includes(envValue) ? envValue : undefined; +})(); + +const title = 'Validators list'; + +const config: Feature<{ chainType: ValidatorsChainType }> = (() => { + if (chainType) { + return Object.freeze({ + title, + isEnabled: true, + chainType, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/ui.ts b/configs/app/ui.ts index 5b7f0ae991..7f6832bf1f 100644 --- a/configs/app/ui.ts +++ b/configs/app/ui.ts @@ -49,7 +49,6 @@ const UI = Object.freeze({ background: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND') || HOMEPAGE_PLATE_BACKGROUND_DEFAULT, textColor: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR') || 'white', }, - showGasTracker: getEnvValue('NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER') === 'false' ? false : true, showAvgBlockTime: getEnvValue('NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME') === 'false' ? false : true, }, views, @@ -70,6 +69,7 @@ const UI = Object.freeze({ ides: { items: parseEnvJson>(getEnvValue('NEXT_PUBLIC_CONTRACT_CODE_IDES')) || [], }, + hasContractAuditReports: getEnvValue('NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS') === 'true' ? true : false, }); export default UI; diff --git a/configs/app/ui/views/tx.ts b/configs/app/ui/views/tx.ts index f725363504..4c45a565f0 100644 --- a/configs/app/ui/views/tx.ts +++ b/configs/app/ui/views/tx.ts @@ -1,5 +1,5 @@ -import type { TxAdditionalFieldsId, TxFieldsId } from 'types/views/tx'; -import { TX_ADDITIONAL_FIELDS_IDS, TX_FIELDS_IDS } from 'types/views/tx'; +import type { TxAdditionalFieldsId, TxFieldsId, TxViewId } from 'types/views/tx'; +import { TX_ADDITIONAL_FIELDS_IDS, TX_FIELDS_IDS, TX_VIEWS_IDS } from 'types/views/tx'; import { getEnvValue, parseEnvJson } from 'configs/app/utils'; @@ -33,9 +33,31 @@ const additionalFields = (() => { return result; })(); +const hiddenViews = (() => { + const envValue = getEnvValue('NEXT_PUBLIC_VIEWS_TX_HIDDEN_VIEWS'); + + if (!envValue) { + return undefined; + } + + const parsedValue = parseEnvJson>(envValue); + + if (!Array.isArray(parsedValue)) { + return undefined; + } + + const result = TX_VIEWS_IDS.reduce((result, item) => { + result[item] = parsedValue.includes(item); + return result; + }, {} as Record); + + return result; +})(); + const config = Object.freeze({ hiddenFields, additionalFields, + hiddenViews, }); export default config; diff --git a/configs/envs/.env.eth b/configs/envs/.env.eth index 8093dff88d..2715974d4b 100644 --- a/configs/envs/.env.eth +++ b/configs/envs/.env.eth @@ -38,12 +38,20 @@ NEXT_PUBLIC_HAS_BEACON_CHAIN=true NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true NEXT_PUBLIC_AUTH_URL=http://localhost:3000 NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout -NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s.blockscout.com +NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s-prod-1.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout NEXT_PUBLIC_AD_BANNER_PROVIDER=hype +NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-mainnet.safe.global +NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com +NEXT_PUBLIC_MARKETPLACE_ENABLED=true +NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json +NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json +NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL +NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true #meta NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth.jpg?raw=true diff --git a/configs/envs/.env.eth_goerli b/configs/envs/.env.eth_goerli index d6359dbb3f..918ab5905d 100644 --- a/configs/envs/.env.eth_goerli +++ b/configs/envs/.env.eth_goerli @@ -33,6 +33,7 @@ NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-c NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'LooksRare','collection_url':'https://goerli.looksrare.org/collections/{hash}','instance_url':'https://goerli.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}] ## misc NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}] +NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS=true # app features NEXT_PUBLIC_APP_ENV=development @@ -52,6 +53,7 @@ NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED='true' NEXT_PUBLIC_HAS_BEACON_CHAIN=true NEXT_PUBLIC_HAS_USER_OPS=true NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout=eth-goerli.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout #meta -NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth-goerli.png?raw=true +NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth-goerli.png?raw=true \ No newline at end of file diff --git a/configs/envs/.env.jest b/configs/envs/.env.jest index 52f14201e3..e1f80c7e75 100644 --- a/configs/envs/.env.jest +++ b/configs/envs/.env.jest @@ -25,7 +25,6 @@ NEXT_PUBLIC_API_BASE_PATH=/ ## homepage NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap'] NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true -NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND= ## sidebar NEXT_PUBLIC_NETWORK_LOGO= diff --git a/configs/envs/.env.main b/configs/envs/.env.main index 837f72fefb..2d003f5930 100644 --- a/configs/envs/.env.main +++ b/configs/envs/.env.main @@ -45,6 +45,7 @@ NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c18099 NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true NEXT_PUBLIC_AUTH_URL=http://localhost:3000 NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout +NEXT_PUBLIC_MARKETPLACE_ENABLED=true NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace-categories/default.json NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C diff --git a/configs/envs/.env.optimism_goerli b/configs/envs/.env.optimism_goerli index bed8f83e36..02dabe815b 100644 --- a/configs/envs/.env.optimism_goerli +++ b/configs/envs/.env.optimism_goerli @@ -44,6 +44,6 @@ NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com # rollup -NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK=true -NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL=https://app.optimism.io/bridge/withdraw -NEXT_PUBLIC_L1_BASE_URL=https://eth-goerli.blockscout.com/ +NEXT_PUBLIC_ROLLUP_TYPE='optimistic' +NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://app.optimism.io/bridge/withdraw +NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth-goerli.blockscout.com/ \ No newline at end of file diff --git a/configs/envs/.env.pw b/configs/envs/.env.pw index f8b8ac0c8d..1b4a4349e5 100644 --- a/configs/envs/.env.pw +++ b/configs/envs/.env.pw @@ -25,7 +25,6 @@ NEXT_PUBLIC_API_BASE_PATH=/ ## homepage NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap'] NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true -NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true ## sidebar ## footer NEXT_PUBLIC_GIT_TAG=v1.0.11 @@ -39,8 +38,10 @@ NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE= # app features NEXT_PUBLIC_APP_ENV=testing NEXT_PUBLIC_APP_INSTANCE=pw +NEXT_PUBLIC_MARKETPLACE_ENABLED=true NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://localhost:3000/marketplace-config.json NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://localhost:3000/marketplace-submit-form +NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://localhost:3000/marketplace-suggest-ideas-form NEXT_PUBLIC_AD_BANNER_PROVIDER=slise NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true NEXT_PUBLIC_AUTH_URL=http://localhost:3100 diff --git a/configs/envs/.env.sepolia b/configs/envs/.env.sepolia index ac5fc85463..8af677915c 100644 --- a/configs/envs/.env.sepolia +++ b/configs/envs/.env.sepolia @@ -55,6 +55,8 @@ NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask'] NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true NEXT_PUBLIC_HAS_BEACON_CHAIN=true +NEXT_PUBLIC_HAS_USER_OPS=true +NEXT_PUBLIC_AD_BANNER_PROVIDER=getit #meta NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/sepolia-testnet.png diff --git a/configs/envs/.env.stability b/configs/envs/.env.stability new file mode 100644 index 0000000000..a395322118 --- /dev/null +++ b/configs/envs/.env.stability @@ -0,0 +1,60 @@ +# Set of ENVs for Ethereum network explorer +# https://eth.blockscout.com/ + +# app configuration +NEXT_PUBLIC_APP_PROTOCOL=http +NEXT_PUBLIC_APP_HOST=localhost +NEXT_PUBLIC_APP_PORT=3000 + +# blockchain parameters +NEXT_PUBLIC_NETWORK_NAME=Stability Testnet +NEXT_PUBLIC_NETWORK_SHORT_NAME=Stability +NEXT_PUBLIC_NETWORK_ID=20180427 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=FREE +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=FREE +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation +NEXT_PUBLIC_NETWORK_RPC_URL=https://free.testnet.stabilityprotocol.com +NEXT_PUBLIC_IS_TESTNET=true + +# api configuration +NEXT_PUBLIC_API_HOST=stability-testnet.blockscout.com +NEXT_PUBLIC_API_BASE_PATH=/ + +# ui config +## homepage +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND="rgb(255, 145, 0)" +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND="rgba(46, 51, 81, 1)" +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR="rgba(122, 235, 246, 1)" +## sidebar +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/rsk-testnet.json +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/stability.svg +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/stability-dark.svg +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/stability-short.svg +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/stability-short-dark.svg +## footer +## views +NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS="['top_accounts']" +NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS="['value','fee_currency','gas_price','gas_fees','burnt_fees']" +NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS="['fee_per_gas']" +NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS="['burnt_fees','total_reward']" +## misc + +# app features +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_HAS_BEACON_CHAIN=false +NEXT_PUBLIC_STATS_API_HOST=https://stats-stability-testnet.k8s.blockscout.com +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +NEXT_PUBLIC_CONTRACT_CODE_IDES="[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]" +NEXT_PUBLIC_MARKETPLACE_ENABLED=true +NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json +NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C +NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form +NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com/ +NEXT_PUBLIC_GAS_TRACKER_ENABLED=false +NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE='stability' + +#meta +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/stability.png diff --git a/deploy/tools/envs-validator/index.ts b/deploy/tools/envs-validator/index.ts index 02918350d6..6bb9a03092 100644 --- a/deploy/tools/envs-validator/index.ts +++ b/deploy/tools/envs-validator/index.ts @@ -41,7 +41,9 @@ async function validateEnvs(appEnvs: Record) { ]; for await (const envName of envsWithJsonConfig) { - appEnvs[envName] = await(appEnvs[envName] ? getExternalJsonContent(envName) : Promise.resolve()) || '[]'; + if (appEnvs[envName]) { + appEnvs[envName] = await getExternalJsonContent(envName) || '[]'; + } } await schema.validate(appEnvs, { stripUnknown: false, abortEarly: false }); diff --git a/deploy/tools/envs-validator/schema.ts b/deploy/tools/envs-validator/schema.ts index 10a5c0bec5..5e4f7712e4 100644 --- a/deploy/tools/envs-validator/schema.ts +++ b/deploy/tools/envs-validator/schema.ts @@ -12,12 +12,16 @@ import type { AdButlerConfig } from '../../../types/client/adButlerConfig'; import { SUPPORTED_AD_TEXT_PROVIDERS, SUPPORTED_AD_BANNER_PROVIDERS } from '../../../types/client/adProviders'; import type { AdTextProviders, AdBannerProviders } from '../../../types/client/adProviders'; import type { ContractCodeIde } from '../../../types/client/contract'; +import { GAS_UNITS } from '../../../types/client/gasTracker'; +import type { GasUnit } from '../../../types/client/gasTracker'; import type { MarketplaceAppOverview } from '../../../types/client/marketplace'; import { NAVIGATION_LINK_IDS } from '../../../types/client/navigation-items'; import type { NavItemExternal, NavigationLinkId } from '../../../types/client/navigation-items'; import { ROLLUP_TYPES } from '../../../types/client/rollup'; import type { BridgedTokenChain, TokenBridge } from '../../../types/client/token'; import { PROVIDERS as TX_INTERPRETATION_PROVIDERS } from '../../../types/client/txInterpretation'; +import { VALIDATORS_CHAIN_TYPE } from '../../../types/client/validators'; +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'; @@ -28,8 +32,8 @@ import { ADDRESS_VIEWS_IDS, IDENTICON_TYPES } from '../../../types/views/address import { BLOCK_FIELDS_IDS } from '../../../types/views/block'; import type { BlockFieldId } from '../../../types/views/block'; import type { NftMarketplaceItem } from '../../../types/views/nft'; -import type { TxAdditionalFieldsId, TxFieldsId } from '../../../types/views/tx'; -import { TX_ADDITIONAL_FIELDS_IDS, TX_FIELDS_IDS } from '../../../types/views/tx'; +import type { TxAdditionalFieldsId, TxFieldsId, TxViewId } from '../../../types/views/tx'; +import { TX_ADDITIONAL_FIELDS_IDS, TX_FIELDS_IDS, TX_VIEWS_IDS } from '../../../types/views/tx'; import { replaceQuotes } from '../../../configs/app/utils'; import * as regexp from '../../../lib/regexp'; @@ -71,7 +75,12 @@ const marketplaceAppSchema: yup.ObjectSchema = yup site: yup.string().test(urlTest), twitter: yup.string().test(urlTest), telegram: yup.string().test(urlTest), - github: yup.string().test(urlTest), + github: yup.lazy(value => + Array.isArray(value) ? + yup.array().of(yup.string().required().test(urlTest)) : + yup.string().test(urlTest), + ), + discord: yup.string().test(urlTest), internalWallet: yup.boolean(), priority: yup.number(), }); @@ -79,29 +88,42 @@ const marketplaceAppSchema: yup.ObjectSchema = yup const marketplaceSchema = yup .object() .shape({ + NEXT_PUBLIC_MARKETPLACE_ENABLED: yup.boolean(), NEXT_PUBLIC_MARKETPLACE_CONFIG_URL: yup .array() .json() - .of(marketplaceAppSchema), + .of(marketplaceAppSchema) + .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', { + is: true, + then: (schema) => schema, + // eslint-disable-next-line max-len + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), + }), NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL: yup .array() .json() - .of(yup.string()), + .of(yup.string()) + .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', { + is: true, + then: (schema) => schema, + // eslint-disable-next-line max-len + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), + }), NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: yup .string() - .when([ 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', 'NEXT_PUBLIC_ADMIN_SERVICE_API_HOST' ], { - is: (config: Array, apiHost: string) => config.length > 0 || Boolean(apiHost), + .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', { + is: true, then: (schema) => schema.test(urlTest).required(), // eslint-disable-next-line max-len - otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM cannot not be used without NEXT_PUBLIC_MARKETPLACE_CONFIG_URL or NEXT_PUBLIC_ADMIN_SERVICE_API_HOST'), + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), }), NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM: yup .string() - .when([ 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', 'NEXT_PUBLIC_ADMIN_SERVICE_API_HOST' ], { - is: (config: Array, apiHost: string) => config.length > 0 || Boolean(apiHost), + .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', { + is: true, then: (schema) => schema.test(urlTest), // eslint-disable-next-line max-len - otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM cannot not be used without NEXT_PUBLIC_MARKETPLACE_CONFIG_URL or NEXT_PUBLIC_ADMIN_SERVICE_API_HOST'), + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), }), }); @@ -375,7 +397,6 @@ const schema = yup .of(yup.string().oneOf([ 'daily_txs', 'coin_price', 'market_cap', 'tvl' ])), NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR: yup.string(), NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND: yup.string(), - NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER: yup.boolean(), NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME: yup.boolean(), // b. sidebar @@ -427,6 +448,11 @@ const schema = yup .transform(replaceQuotes) .json() .of(yup.string().oneOf(TX_ADDITIONAL_FIELDS_IDS)), + NEXT_PUBLIC_VIEWS_TX_HIDDEN_VIEWS: yup + .array() + .transform(replaceQuotes) + .json() + .of(yup.string().oneOf(TX_VIEWS_IDS)), NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES: yup .array() .transform(replaceQuotes) @@ -444,6 +470,7 @@ const schema = yup .transform(replaceQuotes) .json() .of(contractCodeIdeSchema), + NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS: yup.boolean(), NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS: yup.boolean(), NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS: yup.boolean(), NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE: yup.string(), @@ -473,9 +500,14 @@ const schema = yup NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE: yup.boolean(), NEXT_PUBLIC_OG_DESCRIPTION: yup.string(), NEXT_PUBLIC_OG_IMAGE_URL: yup.string().test(urlTest), + NEXT_PUBLIC_SAFE_TX_SERVICE_URL: yup.string().test(urlTest), NEXT_PUBLIC_IS_SUAVE_CHAIN: yup.boolean(), NEXT_PUBLIC_HAS_USER_OPS: yup.boolean(), + NEXT_PUBLIC_METASUITES_ENABLED: yup.boolean(), NEXT_PUBLIC_SWAP_BUTTON_URL: yup.string(), + NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE: yup.string().oneOf(VALIDATORS_CHAIN_TYPE), + NEXT_PUBLIC_GAS_TRACKER_ENABLED: yup.boolean(), + NEXT_PUBLIC_GAS_TRACKER_UNITS: yup.array().transform(replaceQuotes).json().of(yup.string().oneOf(GAS_UNITS)), // 6. External services envs NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(), diff --git a/deploy/tools/envs-validator/test/.env.base b/deploy/tools/envs-validator/test/.env.base index 220bb815e7..6c6f417c96 100644 --- a/deploy/tools/envs-validator/test/.env.base +++ b/deploy/tools/envs-validator/test/.env.base @@ -19,14 +19,12 @@ NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=false NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR='#fff' NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND='rgb(255, 145, 0)' -NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true +NEXT_PUBLIC_GAS_TRACKER_ENABLED=true +NEXT_PUBLIC_GAS_TRACKER_UNITS=['gwei'] NEXT_PUBLIC_IS_TESTNET=true -NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://example.com -NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://example.com -NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://example.com -NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://example.com NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE='Hello' +NEXT_PUBLIC_METASUITES_ENABLED=true NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH @@ -43,6 +41,7 @@ NEXT_PUBLIC_OG_DESCRIPTION='Hello world!' NEXT_PUBLIC_OG_IMAGE_URL=https://example.com/image.png NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://blockscout.com','text':'Blockscout'}] NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE=true +NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-mainnet.safe.global NEXT_PUBLIC_STATS_API_HOST=https://example.com NEXT_PUBLIC_USE_NEXT_JS_PROXY=false NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE=gradient_avatar @@ -51,7 +50,9 @@ NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=['burnt_fees','total_reward'] NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'NFT Marketplace','collection_url':'https://example.com/{hash}','instance_url':'https://example.com/{hash}/{id}','logo_url':'https://example.com/logo.png'}] NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS=['fee_per_gas'] NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS=['value','fee_currency','gas_price','tx_fee','gas_fees','burnt_fees'] +NEXT_PUBLIC_VIEWS_TX_HIDDEN_VIEWS=['blob_txs'] NEXT_PUBLIC_VISUALIZE_API_HOST=https://example.com NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET=false NEXT_PUBLIC_WEB3_WALLETS=['coinbase','metamask','token_pocket'] NEXT_PUBLIC_SWAP_BUTTON_URL=uniswap +NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE=stability \ No newline at end of file diff --git a/deploy/tools/envs-validator/test/.env.marketplace b/deploy/tools/envs-validator/test/.env.marketplace new file mode 100644 index 0000000000..316dd70bd1 --- /dev/null +++ b/deploy/tools/envs-validator/test/.env.marketplace @@ -0,0 +1,6 @@ +NEXT_PUBLIC_MARKETPLACE_ENABLED=true +NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://example.com +NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://example.com +NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://example.com +NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://example.com +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://example.com diff --git a/deploy/values/l2-optimism-goerli/values.yaml b/deploy/values/l2-optimism-goerli/values.yaml index b0382e94a4..afd8dcc1a0 100644 --- a/deploy/values/l2-optimism-goerli/values.yaml +++ b/deploy/values/l2-optimism-goerli/values.yaml @@ -179,6 +179,7 @@ frontend: NEXT_PUBLIC_NETWORK_LOGO: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/base.svg NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/base.svg NEXT_PUBLIC_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/base-goerli.json + NEXT_PUBLIC_MARKETPLACE_ENABLED: true NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: https://airtable.com/shrqUAcjgGJ4jU88C NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM: https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form NEXT_PUBLIC_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout diff --git a/deploy/values/main/values.yaml b/deploy/values/main/values.yaml index 742f773709..493941ad0a 100644 --- a/deploy/values/main/values.yaml +++ b/deploy/values/main/values.yaml @@ -148,6 +148,7 @@ frontend: NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/goerli.svg NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: validation + NEXT_PUBLIC_MARKETPLACE_ENABLED: true NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: https://airtable.com/shrqUAcjgGJ4jU88C NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM: https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form NEXT_PUBLIC_APP_ENV: development diff --git a/deploy/values/review-l2/values.yaml.gotmpl b/deploy/values/review-l2/values.yaml.gotmpl index 2c8a0b79a0..eea25de5e8 100644 --- a/deploy/values/review-l2/values.yaml.gotmpl +++ b/deploy/values/review-l2/values.yaml.gotmpl @@ -30,10 +30,12 @@ frontend: kubernetes.io/ingress.class: internal-and-public nginx.ingress.kubernetes.io/proxy-body-size: 500m nginx.ingress.kubernetes.io/client-max-body-size: "500M" - nginx.ingress.kubernetes.io/proxy-buffering: "off" + nginx.ingress.kubernetes.io/proxy-buffering: "on" nginx.ingress.kubernetes.io/proxy-connect-timeout: "15m" nginx.ingress.kubernetes.io/proxy-send-timeout: "15m" nginx.ingress.kubernetes.io/proxy-read-timeout: "15m" + nginx.ingress.kubernetes.io/proxy-buffer-size: "128k" + nginx.ingress.kubernetes.io/proxy-buffers-number: "8" cert-manager.io/cluster-issuer: "zerossl-prod" hostname: review-l2-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}.k8s-dev.blockscout.com @@ -52,6 +54,7 @@ frontend: NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/base.svg NEXT_PUBLIC_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/base-goerli.json NEXT_PUBLIC_API_HOST: blockscout-optimism-goerli.k8s-dev.blockscout.com + NEXT_PUBLIC_MARKETPLACE_ENABLED: true NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: https://airtable.com/shrqUAcjgGJ4jU88C NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM: https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form NEXT_PUBLIC_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout diff --git a/deploy/values/review/values.yaml.gotmpl b/deploy/values/review/values.yaml.gotmpl index 85d0e57486..a703bbc4ae 100644 --- a/deploy/values/review/values.yaml.gotmpl +++ b/deploy/values/review/values.yaml.gotmpl @@ -30,10 +30,12 @@ frontend: kubernetes.io/ingress.class: internal-and-public nginx.ingress.kubernetes.io/proxy-body-size: 500m nginx.ingress.kubernetes.io/client-max-body-size: "500M" - nginx.ingress.kubernetes.io/proxy-buffering: "off" + nginx.ingress.kubernetes.io/proxy-buffering: "on" nginx.ingress.kubernetes.io/proxy-connect-timeout: "15m" nginx.ingress.kubernetes.io/proxy-send-timeout: "15m" nginx.ingress.kubernetes.io/proxy-read-timeout: "15m" + nginx.ingress.kubernetes.io/proxy-buffer-size: "128k" + nginx.ingress.kubernetes.io/proxy-buffers-number: "8" cert-manager.io/cluster-issuer: "zerossl-prod" hostname: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}.k8s-dev.blockscout.com @@ -51,13 +53,14 @@ frontend: NEXT_PUBLIC_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json NEXT_PUBLIC_NETWORK_LOGO: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/goerli.svg NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/goerli.svg - NEXT_PUBLIC_API_HOST: eth-goerli.blockscout.com + NEXT_PUBLIC_API_HOST: eth-sepolia.blockscout.com NEXT_PUBLIC_STATS_API_HOST: https://stats-goerli.k8s-dev.blockscout.com/ NEXT_PUBLIC_VISUALIZE_API_HOST: http://visualizer-svc.visualizer-testing.svc.cluster.local/ NEXT_PUBLIC_CONTRACT_INFO_API_HOST: https://contracts-info-test.k8s-dev.blockscout.com NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: https://admin-rs-test.k8s-dev.blockscout.com NEXT_PUBLIC_NAME_SERVICE_API_HOST: https://bens-rs-test.k8s-dev.blockscout.com NEXT_PUBLIC_AUTH_URL: https://blockscout-main.k8s-dev.blockscout.com + NEXT_PUBLIC_MARKETPLACE_ENABLED: true NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: https://airtable.com/shrqUAcjgGJ4jU88C NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM: https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form NEXT_PUBLIC_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout @@ -77,6 +80,8 @@ frontend: NEXT_PUBLIC_HAS_USER_OPS: true NEXT_PUBLIC_CONTRACT_CODE_IDES: "[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout=eth-goerli.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]" NEXT_PUBLIC_SWAP_BUTTON_URL: uniswap + NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS: true + NEXT_PUBLIC_AD_BANNER_PROVIDER: getit envFromSecret: NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 08dc19eaf2..58dcae80c9 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -57,7 +57,7 @@ B. Pre-defined configuration: 1. Optionally, clone `.env.example` file into `configs/envs/.env.secrets`. Fill it with necessary secrets for integrating with [external services](./ENVS.md#external-services-configuration). Include only secrets your need. 2. Choose one of the predefined configurations located in the `/configs/envs` folder. -3. Start your local dev server using the `yarn dev:` command. +3. Start your local dev server using the `yarn dev:preset ` command. 4. Open your browser and navigate to the URL provided in the command line output (by default, it is `http://localhost:3000`). @@ -79,18 +79,21 @@ These are the steps that you have to follow to make everything work: 2. Make sure that you have added a property to React app config (`configs/app/index.ts`) in appropriate section that is associated with this variable; do not use ENV variable values directly in the application code; decide where this variable belongs to and place it under the certain section: - `app` - the front-end app itself - `api` - the main API configuration + - `chain` - the Blockchain parameters - `UI` - the app UI customization + - `meta` - SEO and meta-tags customization - `features` - the particular feature of the app - - `services` - some 3rd party service integration which is not related to one particular feature -3. For local development purposes add the variable with its appropriate values to pre-defined ENV configs `configs/envs` where it is needed -4. Add the variable to CI configs where it is needed + - `services` - some 3rd party service integration which is not related to one particular feature +3. If a new variable is meant to store the URL of an external API service, remember to include its value in the Content-Security-Policy document header. Refer to `nextjs/csp/policies/app.ts` for details. +4. For local development purposes add the variable with its appropriate values to pre-defined ENV configs `configs/envs` where it is needed +5. Add the variable to CI configs where it is needed - `deploy/values/review/values.yaml.gotmpl` - review development environment - `deploy/values/main/values.yaml` - main development environment - `deploy/values/review-l2/values.yaml.gotmpl` - review development environment for L2 networks - `deploy/values/l2-optimism-goerli/values.yaml` - main development environment -5. If your variable is meant to receive a link to some external resource (image or JSON-config file), extend the array `ASSETS_ENVS` in `deploy/scripts/download_assets.sh` with your variable name -6. Add validation schema for the new variable into the file `deploy/tools/envs-validator/schema.ts` -7. Check if modified validation schema is valid by doing the following steps: +6. If your variable is meant to receive a link to some external resource (image or JSON-config file), extend the array `ASSETS_ENVS` in `deploy/scripts/download_assets.sh` with your variable name +7. Add validation schema for the new variable into the file `deploy/tools/envs-validator/schema.ts` +8. Check if modified validation schema is valid by doing the following steps: - change your current directory to `deploy/tools/envs-validator` - install deps with `yarn` command - add your variable into `./test/.env.base` test preset or create a new test preset if needed @@ -98,7 +101,7 @@ These are the steps that you have to follow to make everything work: - add example of file content into `./test/assets` directory; the file name should be constructed by stripping away prefix `NEXT_PUBLIC_` and postfix `_URL` if any, and converting the remaining string to lowercase (for example, `NEXT_PUBLIC_MARKETPLACE_CONFIG_URL` will become `marketplace_config.json`) - in the main script `index.ts` extend array `envsWithJsonConfig` with your variable name - run `yarn test` command to see the validation result -8. Don't forget to mention in the PR notes that new ENV variable was added +9. Don't forget to mention in the PR notes that new ENV variable was added   diff --git a/docs/DEPRECATED_ENVS.md b/docs/DEPRECATED_ENVS.md index c7e305d3cf..b46ea04108 100644 --- a/docs/DEPRECATED_ENVS.md +++ b/docs/DEPRECATED_ENVS.md @@ -6,3 +6,4 @@ | NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK | `boolean` | Set to true for zkevm L2 solutions | Required | - | `true` | v1.24.0 | Replaced by NEXT_PUBLIC_ROLLUP_TYPE | | NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL | `string` | URL for optimistic L2 -> L1 withdrawals | Required | - | `https://app.optimism.io/bridge/withdraw` | v1.24.0 | Renamed to NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL | | NEXT_PUBLIC_L1_BASE_URL | `string` | Blockscout base URL for L1 network | Required | - | `'http://eth-goerli.blockscout.com'` | v1.24.0 | Renamed to NEXT_PUBLIC_ROLLUP_L1_BASE_URL | +| 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 | \ No newline at end of file diff --git a/docs/ENVS.md b/docs/ENVS.md index 041f21d790..fbdff1e706 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -28,6 +28,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will - [Misc](ENVS.md#misc) - [App features](ENVS.md#app-features) - [My account](ENVS.md#my-account) + - [Gas tracker](ENVS.md#gas-tracker) - [Address verification](ENVS.md#address-verification-in-my-account) in "My account" - [Blockchain interaction](ENVS.md#blockchain-interaction-writing-to-contract-etc) (writing to contract, etc.) - [Banner ads](ENVS.md#banner-ads) @@ -51,6 +52,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will - [Bridged tokens](ENVS.md#bridged-tokens) - [Safe{Core} address tags](ENVS.md#safecore-address-tags) - [SUAVE chain](ENVS.md#suave-chain) + - [MetaSuites extension](ENVS.md#metasuites-extension) - [Sentry error monitoring](ENVS.md#sentry-error-monitoring) - [OpenTelemetry](ENVS.md#opentelemetry) - [Swap button](ENVS.md#swap-button) @@ -108,7 +110,6 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will | NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'market_cap' \| 'tvl'>` | List of charts displayed on the home page | - | - | `['daily_txs','coin_price','market_cap']` | | 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` | | 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)` | -| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` | Set to false if network doesn't have gas tracker | - | `true` | `false` | | NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` |   @@ -218,6 +219,7 @@ Settings for meta tags and OG tags | --- | --- | --- | --- | --- | --- | | NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS | `Array` | Array of the transaction fields ids that should be hidden. See below the list of the possible id values. | - | - | `'["value","tx_fee"]'` | | NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS | `Array` | Array of the additional fields ids that should be added to the transaction details. See below the list of the possible id values. | - | - | `'["fee_per_gas"]'` | +| NEXT_PUBLIC_VIEWS_TX_HIDDEN_VIEWS | `Array` | Transaction views that should be hidden. See below the list of the possible id values. | - | - | `'["blob_txs"]'` | ##### Transaction fields list | Id | Description | @@ -234,6 +236,11 @@ Settings for meta tags and OG tags | --- | --- | | `fee_per_gas` | Amount of total fee divided by total amount of gas used by transaction | +##### Transaction view list +| Id | Description | +| --- | --- | +| `blob_txs` | List of all transactions that contain blob data | +   #### NFT views @@ -261,6 +268,7 @@ Settings for meta tags and OG tags | --- | --- | --- | --- | --- | --- | | NEXT_PUBLIC_NETWORK_EXPLORERS | `Array` where `NetworkExplorer` can have following [properties](#network-explorer-configuration-properties) | Used to build up links to transactions, blocks, addresses in other chain explorers. | - | - | `[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/tx'}}]` | | NEXT_PUBLIC_CONTRACT_CODE_IDES | `Array` where `ContractCodeIde` can have following [properties](#contract-code-ide-configuration-properties) | Used to build up links to IDEs with contract source code. | - | - | `[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout={domain}','icon_url':'https://example.com/icon.svg'}]` | +| NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS | `boolean` | Set to `true` to enable Submit Audit form on the contract page | - | `false` | `true` | | NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS | `boolean` | Set to `true` to hide indexing alert in the page header about indexing chain's blocks | - | `false` | `true` | | 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` | | 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! 🤪` | @@ -301,6 +309,17 @@ Settings for meta tags and OG tags   +### Gas tracker + +This feature is **enabled by default**. To switch it off pass `NEXT_PUBLIC_GAS_TRACKER_ENABLED=false`. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | +| --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_GAS_TRACKER_ENABLED | `boolean` | Set to true to enable "Gas tracker" in the app | Required | `true` | `false` | +| NEXT_PUBLIC_GAS_TRACKER_UNITS | Array<`usd` \| `gwei`> | Array of units for displaying gas prices on the Gas Tracker page, in the stats snippet on the Home page, and in the top bar. The first value in the array will take priority over the second one in all mentioned views. If only one value is provided, gas prices will be displayed only in that unit. | - | `[ 'usd', 'gwei' ]` | `[ 'gwei' ]` | + +  + ### Address verification in "My account" *Note* all ENV variables required for [My account](ENVS.md#my-account) feature should be passed alongside the following ones: @@ -316,7 +335,7 @@ Settings for meta tags and OG tags | Variable | Type| Description | Compulsoriness | Default value | Example value | | --- | --- | --- | --- | --- | --- | -| NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID | `string` | Project id for [WalletConnect](https://docs.walletconnect.com/2.0/web3modal/react/installation#obtain-project-id) integration | Required | - | `` | +| NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID | `string` | Project id for [WalletConnect](https://cloud.walletconnect.com/) integration | Required | - | `` | | NEXT_PUBLIC_NETWORK_RPC_URL | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `https://core.poa.network` | | NEXT_PUBLIC_NETWORK_NAME | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `Gnosis Chain` | | NEXT_PUBLIC_NETWORK_ID | `number` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `99` | @@ -332,7 +351,7 @@ This feature is **enabled by default** with the `slise` ads provider. To switch | Variable | Type| Description | Compulsoriness | Default value | Example value | | --- | --- | --- | --- | --- | --- | -| NEXT_PUBLIC_AD_BANNER_PROVIDER | `slise` \| `adbutler` \| `coinzilla` \| `hype` \| `none` | Ads provider | - | `slise` | `coinzilla` | +| NEXT_PUBLIC_AD_BANNER_PROVIDER | `slise` \| `adbutler` \| `coinzilla` \| `hype` \| `getit` \| `none` | Ads provider | - | `slise` | `coinzilla` | | NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP | `{ id: string; width: string; height: string }` | Placement config for desktop Adbutler banner | - | - | `{'id':'123456','width':'728','height':'90'}` | | NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE | `{ id: string; width: number; height: number }` | Placement config for mobile Adbutler banner | - | - | `{'id':'654321','width':'300','height':'100'}` | @@ -369,7 +388,7 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi | Variable | Type| Description | Compulsoriness | Default value | Example value | | --- | --- | --- | --- | --- | --- | -| NEXT_PUBLIC_ROLLUP_TYPE | `'optimistic' \| 'zkEvm' ` | Rollup chain type | Required | - | `'optimistic'` | +| NEXT_PUBLIC_ROLLUP_TYPE | `'optimistic' \| 'shibarium' \| 'zkEvm' ` | Rollup chain type | Required | - | `'optimistic'` | | NEXT_PUBLIC_ROLLUP_L1_BASE_URL | `string` | Blockscout base URL for L1 network | Required | - | `'http://eth-goerli.blockscout.com'` | | NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL | `string` | URL for L2 -> L1 withdrawals | - | - | `https://app.optimism.io/bridge/withdraw` | @@ -429,12 +448,13 @@ This feature is **always enabled**, but you can configure its behavior by passin | Variable | Type| Description | Compulsoriness | Default value | Example value | | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_MARKETPLACE_ENABLED | `boolean` | `true` means that the marketplace page will be enabled | - | - | `true` | | NEXT_PUBLIC_MARKETPLACE_CONFIG_URL | `string` | URL of configuration file (`.json` format only) which contains list of apps that will be shown on the marketplace page. See [below](#marketplace-app-configuration-properties) list of available properties for an app. Can be replaced with NEXT_PUBLIC_ADMIN_SERVICE_API_HOST | Required | - | `https://example.com/marketplace_config.json` | | NEXT_PUBLIC_ADMIN_SERVICE_API_HOST | `string` | Admin Service API endpoint url. Can be used instead of NEXT_PUBLIC_MARKETPLACE_CONFIG_URL | - | - | `https://admin-rs.services.blockscout.com` | | NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | Required | - | `https://airtable.com/shrqUAcjgGJ4jU88C` | | NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM | `string` | Link to form where users can suggest ideas for the marketplace | - | - | `https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form` | | NEXT_PUBLIC_NETWORK_RPC_URL | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `https://core.poa.network` | -| NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL | `string` | URL of configuration file (`.json` format only) which contains the list of categories to be displayed on the markeplace page in the specified order. If no URL is provided, then the list of categories will be compiled based on the `categories` fields from the marketplace (apps) configuration file | - | - | `https://example.com/marketplace_categories.json` | +| NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL | `string` | URL of configuration file (`.json` format only) which contains the list of categories to be displayed on the marketplace page in the specified order. If no URL is provided, then the list of categories will be compiled based on the `categories` fields from the marketplace (apps) configuration file | - | - | `https://example.com/marketplace_categories.json` | #### Marketplace app configuration properties @@ -558,7 +578,11 @@ This feature allows users to view tokens that have been bridged from other EVM c ### Safe{Core} address tags -For the smart contract addresses which are [Safe{Core} accounts](https://safe.global/) public tag "Multisig: Safe" will be displayed in the address page header alongside to Safe logo. The Safe service is available only for certain networks, see full list [here](https://docs.safe.global/safe-core-api/available-services). Based on provided value of `NEXT_PUBLIC_NETWORK_ID`, the feature will be enabled or disabled. +For the smart contract addresses which are [Safe{Core} accounts](https://safe.global/) public tag "Multisig: Safe" will be displayed in the address page header alongside to Safe logo. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | +| --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_SAFE_TX_SERVICE_URL | `string` | The Safe transaction service URL. See full list of supported networks [here](https://docs.safe.global/api-supported-networks). | - | - | `uniswap` |   @@ -572,6 +596,26 @@ For blockchains that implement SUAVE architecture additional fields will be show   +### MetaSuites extension + +Enables [MetaSuites browser extension](https://github.com/blocksecteam/metasuites) to integrate with the app views. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | +| --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_METASUITES_ENABLED | `boolean` | Set to true to enable integration | Required | - | `true` | + +  + +### Validators list + +The feature enables the Validators page which provides detailed information about the validators of the PoS chains. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | +| --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE | `'stability'` | Chain type | Required | - | `'stability'` | + +  + ### Sentry error monitoring | Variable | Type| Description | Compulsoriness | Default value | Example value | diff --git a/icons/blob.svg b/icons/blob.svg new file mode 100644 index 0000000000..9dc2b542ff --- /dev/null +++ b/icons/blob.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/icons/blobs/image.svg b/icons/blobs/image.svg new file mode 100644 index 0000000000..be08dd269c --- /dev/null +++ b/icons/blobs/image.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/icons/blobs/raw.svg b/icons/blobs/raw.svg new file mode 100644 index 0000000000..8a97401ff5 --- /dev/null +++ b/icons/blobs/raw.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/icons/blobs/text.svg b/icons/blobs/text.svg new file mode 100644 index 0000000000..08ec8801bf --- /dev/null +++ b/icons/blobs/text.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/icons/brands/safe.svg b/icons/brands/safe.svg index 9e596a3821..8369513837 100644 --- a/icons/brands/safe.svg +++ b/icons/brands/safe.svg @@ -1,3 +1,3 @@ - - + + diff --git a/icons/brands/solidity_scan.svg b/icons/brands/solidity_scan.svg new file mode 100644 index 0000000000..ac5747c69a --- /dev/null +++ b/icons/brands/solidity_scan.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/icons/gas_xl.svg b/icons/gas_xl.svg new file mode 100644 index 0000000000..5a3913ac16 --- /dev/null +++ b/icons/gas_xl.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/rocket_xl.svg b/icons/rocket_xl.svg new file mode 100644 index 0000000000..8b3f4ccdbf --- /dev/null +++ b/icons/rocket_xl.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/swap.svg b/icons/swap.svg index 2d32eb363f..63d915c99b 100644 --- a/icons/swap.svg +++ b/icons/swap.svg @@ -1,3 +1,3 @@ - + diff --git a/icons/up.svg b/icons/up.svg new file mode 100644 index 0000000000..375381a790 --- /dev/null +++ b/icons/up.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/validator.svg b/icons/validator.svg new file mode 100644 index 0000000000..e77bb0ba5d --- /dev/null +++ b/icons/validator.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/api/resources.ts b/lib/api/resources.ts index dd8641e724..a124e65bbb 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -31,10 +31,18 @@ import type { AddressNFTTokensFilter, } from 'types/api/address'; import type { AddressesResponse } from 'types/api/addresses'; +import type { TxBlobs, Blob } from 'types/api/blobs'; import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse } from 'types/api/block'; import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts'; import type { BackendVersionConfig } from 'types/api/configs'; -import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod, SmartContractVerificationConfig, SolidityscanReport } from 'types/api/contract'; +import type { + SmartContract, + SmartContractReadMethod, + SmartContractWriteMethod, + SmartContractVerificationConfig, + SolidityscanReport, + SmartContractSecurityAudits, +} from 'types/api/contract'; import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts'; import type { EnsAddressLookupFilters, @@ -57,6 +65,7 @@ import type { } from 'types/api/optimisticL2'; import type { RawTracesResponse } from 'types/api/rawTrace'; import type { SearchRedirectResult, SearchResult, SearchResultFilters, SearchResultItem } from 'types/api/search'; +import type { ShibariumWithdrawalsResponse, ShibariumDepositsResponse } from 'types/api/shibarium'; import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats'; import type { TokenCounters, @@ -76,11 +85,13 @@ import type { Transaction, TransactionsResponseWatchlist, TransactionsSorting, + TransactionsResponseWithBlobs, } from 'types/api/transaction'; import type { TxInterpretationResponse } from 'types/api/txInterpretation'; -import type { TTxsFilters } from 'types/api/txsFilters'; +import type { TTxsFilters, TTxsWithBlobsFilters } from 'types/api/txsFilters'; import type { TxStateChanges } from 'types/api/txStateChanges'; import type { UserOpsResponse, UserOp, UserOpsFilters, UserOpsAccount } from 'types/api/userOps'; +import type { ValidatorsCountersResponse, ValidatorsFilters, ValidatorsResponse, ValidatorsSorting } from 'types/api/validators'; import type { VerifiedContractsSorting } from 'types/api/verifiedContracts'; import type { VisualizedContract } from 'types/api/visualization'; import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals'; @@ -99,6 +110,7 @@ export interface ApiResource { basePath?: string; pathParams?: Array; needAuth?: boolean; // for external APIs which require authentication + headers?: RequestInit['headers']; } export const SORTING_FIELDS = [ 'sort', 'order' ]; @@ -175,7 +187,7 @@ export const RESOURCES = { needAuth: true, }, - // STATS + // STATS MICROSERVICE API stats_counters: { path: '/api/v1/counters', endpoint: getFeaturePayload(config.features.stats)?.api.endpoint, @@ -254,7 +266,7 @@ export const RESOURCES = { block_txs: { path: '/api/v2/blocks/:height_or_hash/transactions', pathParams: [ 'height_or_hash' as const ], - filterFields: [], + filterFields: [ 'type' as const ], }, block_withdrawals: { path: '/api/v2/blocks/:height_or_hash/withdrawals', @@ -269,6 +281,10 @@ export const RESOURCES = { path: '/api/v2/transactions', filterFields: [ 'filter' as const, 'type' as const, 'method' as const ], }, + txs_with_blobs: { + path: '/api/v2/transactions', + filterFields: [ 'type' as const ], + }, txs_watchlist: { path: '/api/v2/transactions/watchlist', filterFields: [ ], @@ -306,6 +322,10 @@ export const RESOURCES = { pathParams: [ 'hash' as const ], filterFields: [], }, + tx_blobs: { + path: '/api/v2/transactions/:hash/blobs', + pathParams: [ 'hash' as const ], + }, tx_interpretation: { path: '/api/v2/transactions/:hash/summary', pathParams: [ 'hash' as const ], @@ -432,6 +452,10 @@ export const RESOURCES = { path: '/api/v2/smart-contracts/:hash/solidityscan-report', pathParams: [ 'hash' as const ], }, + contract_security_audits: { + path: '/api/v2/smart-contracts/:hash/audit-reports', + pathParams: [ 'hash' as const ], + }, verified_contracts: { path: '/api/v2/smart-contracts', @@ -500,16 +524,21 @@ export const RESOURCES = { filterFields: [], }, - // HOMEPAGE - homepage_stats: { + // APP STATS + stats: { path: '/api/v2/stats', + headers: { + 'updated-gas-oracle': 'true', + }, }, - homepage_chart_txs: { + stats_charts_txs: { path: '/api/v2/stats/charts/transactions', }, - homepage_chart_market: { + stats_charts_market: { path: '/api/v2/stats/charts/market', }, + + // HOMEPAGE homepage_blocks: { path: '/api/v2/main-page/blocks', }, @@ -601,6 +630,25 @@ export const RESOURCES = { filterFields: [], }, + // SHIBARIUM L2 + shibarium_deposits: { + path: '/api/v2/shibarium/deposits', + filterFields: [], + }, + + shibarium_deposits_count: { + path: '/api/v2/shibarium/deposits/count', + }, + + shibarium_withdrawals: { + path: '/api/v2/shibarium/withdrawals', + filterFields: [], + }, + + shibarium_withdrawals_count: { + path: '/api/v2/shibarium/withdrawals/count', + }, + // USER OPS user_ops: { path: '/api/v2/proxy/account-abstraction/operations', @@ -614,6 +662,27 @@ export const RESOURCES = { path: '/api/v2/proxy/account-abstraction/accounts/:hash', pathParams: [ 'hash' as const ], }, + user_op_interpretation: { + path: '/api/v2/proxy/account-abstraction/operations/:hash/summary', + pathParams: [ 'hash' as const ], + }, + + // VALIDATORS + validators: { + path: '/api/v2/validators/:chainType', + pathParams: [ 'chainType' as const ], + filterFields: [ 'address_hash' as const, 'state_filter' as const ], + }, + validators_counters: { + path: '/api/v2/validators/:chainType/counters', + pathParams: [ 'chainType' as const ], + }, + + // BLOBS + blob: { + path: '/api/v2/blobs/:hash', + pathParams: [ 'hash' as const ], + }, // CONFIGS config_backend_version: { @@ -674,8 +743,8 @@ export interface ResourceError { export type ResourceErrorAccount = ResourceError<{ errors: T }> export type PaginatedResources = 'blocks' | 'block_txs' | -'txs_validated' | 'txs_pending' | 'txs_watchlist' | 'txs_execution_node' | -'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' | 'tx_state_changes' | +'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' | 'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' | 'search' | @@ -684,10 +753,11 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'token_instance_transfers' | 'token_instance_holders' | 'verified_contracts' | 'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' | +'shibarium_deposits' | 'shibarium_withdrawals' | 'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' | 'withdrawals' | 'address_withdrawals' | 'block_withdrawals' | 'watchlist' | 'private_tags_address' | 'private_tags_tx' | -'domains_lookup' | 'addresses_lookup' | 'user_ops'; +'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'validators'; export type PaginatedResponse = ResourcePayload; @@ -706,9 +776,9 @@ Q extends 'watchlist' ? WatchlistResponse : Q extends 'verified_addresses' ? VerifiedAddressResponse : Q extends 'token_info_applications_config' ? TokenInfoApplicationConfig : Q extends 'token_info_applications' ? TokenInfoApplications : -Q extends 'homepage_stats' ? HomeStats : -Q extends 'homepage_chart_txs' ? ChartTransactionResponse : -Q extends 'homepage_chart_market' ? ChartMarketResponse : +Q extends 'stats' ? HomeStats : +Q extends 'stats_charts_txs' ? ChartTransactionResponse : +Q extends 'stats_charts_market' ? ChartMarketResponse : Q extends 'homepage_blocks' ? Array : Q extends 'homepage_txs' ? Array : Q extends 'homepage_txs_watchlist' ? Array : @@ -725,6 +795,7 @@ Q extends 'block_txs' ? BlockTransactionsResponse : Q extends 'block_withdrawals' ? BlockWithdrawalsResponse : Q extends 'txs_validated' ? TransactionsResponseValidated : Q extends 'txs_pending' ? TransactionsResponsePending : +Q extends 'txs_with_blobs' ? TransactionsResponseWithBlobs : Q extends 'txs_watchlist' ? TransactionsResponseWatchlist : Q extends 'txs_execution_node' ? TransactionsResponseValidated : Q extends 'tx' ? Transaction : @@ -733,6 +804,7 @@ Q extends 'tx_logs' ? LogsResponseTx : Q extends 'tx_token_transfers' ? TokenTransferResponse : Q extends 'tx_raw_trace' ? RawTracesResponse : Q extends 'tx_state_changes' ? TxStateChanges : +Q extends 'tx_blobs' ? TxBlobs : Q extends 'tx_interpretation' ? TxInterpretationResponse : Q extends 'addresses' ? AddressesResponse : Q extends 'address' ? Address : @@ -789,13 +861,6 @@ Q extends 'zkevm_l2_txn_batches_count' ? number : Q extends 'zkevm_l2_txn_batch' ? ZkEvmL2TxnBatch : Q extends 'zkevm_l2_txn_batch_txs' ? ZkEvmL2TxnBatchTxs : Q extends 'config_backend_version' ? BackendVersionConfig : -Q extends 'addresses_lookup' ? EnsAddressLookupResponse : -Q extends 'domain_info' ? EnsDomainDetailed : -Q extends 'domain_events' ? EnsDomainEventsResponse : -Q extends 'domains_lookup' ? EnsDomainLookupResponse : -Q extends 'user_ops' ? UserOpsResponse : -Q extends 'user_op' ? UserOp : -Q extends 'user_ops_account' ? UserOpsAccount : never; // !!! IMPORTANT !!! // See comment above @@ -803,22 +868,39 @@ never; /* eslint-disable @typescript-eslint/indent */ export type ResourcePayloadB = +Q extends 'blob' ? Blob : Q extends 'marketplace_dapps' ? Array : Q extends 'marketplace_dapp' ? MarketplaceAppOverview : +Q extends 'validators' ? ValidatorsResponse : +Q extends 'validators_counters' ? ValidatorsCountersResponse : +Q extends 'shibarium_withdrawals' ? ShibariumWithdrawalsResponse : +Q extends 'shibarium_deposits' ? ShibariumDepositsResponse : +Q extends 'shibarium_withdrawals_count' ? number : +Q extends 'shibarium_deposits_count' ? number : +Q extends 'contract_security_audits' ? SmartContractSecurityAudits : +Q extends 'addresses_lookup' ? EnsAddressLookupResponse : +Q extends 'domain_info' ? EnsDomainDetailed : +Q extends 'domain_events' ? EnsDomainEventsResponse : +Q extends 'domains_lookup' ? EnsDomainLookupResponse : +Q extends 'user_ops' ? UserOpsResponse : +Q extends 'user_op' ? UserOp : +Q extends 'user_ops_account' ? UserOpsAccount : +Q extends 'user_op_interpretation'? TxInterpretationResponse : never; /* eslint-enable @typescript-eslint/indent */ export type ResourcePayload = ResourcePayloadA | ResourcePayloadB; - -// Right now there is no paginated resources in B-part -// Add "| ResourcePayloadB[...]" if it is not true anymore -export type PaginatedResponseItems = Q extends PaginatedResources ? ResourcePayloadA['items'] : never; -export type PaginatedResponseNextPageParams = Q extends PaginatedResources ? ResourcePayloadA['next_page_params'] : never; +export type PaginatedResponseItems = Q extends PaginatedResources ? ResourcePayloadA['items'] | ResourcePayloadB['items'] : never; +export type PaginatedResponseNextPageParams = Q extends PaginatedResources ? + ResourcePayloadA['next_page_params'] | ResourcePayloadB['next_page_params'] : + never; /* eslint-disable @typescript-eslint/indent */ export type PaginationFilters = Q extends 'blocks' ? BlockFilters : +Q extends 'block_txs' ? TTxsWithBlobsFilters : Q extends 'txs_validated' | 'txs_pending' ? TTxsFilters : +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 : @@ -834,6 +916,7 @@ Q extends 'verified_contracts' ? VerifiedContractsFilters : Q extends 'addresses_lookup' ? EnsAddressLookupFilters : Q extends 'domains_lookup' ? EnsDomainLookupFilters : Q extends 'user_ops' ? UserOpsFilters : +Q extends 'validators' ? ValidatorsFilters : never; /* eslint-enable @typescript-eslint/indent */ @@ -845,5 +928,6 @@ Q extends 'verified_contracts' ? VerifiedContractsSorting : Q extends 'address_txs' ? TransactionsSorting : Q extends 'addresses_lookup' ? EnsLookupSorting : Q extends 'domains_lookup' ? EnsLookupSorting : +Q extends 'validators' ? ValidatorsSorting : never; /* eslint-enable @typescript-eslint/indent */ diff --git a/lib/api/useApiFetch.tsx b/lib/api/useApiFetch.tsx index 8f23ebade7..85da773ab1 100644 --- a/lib/api/useApiFetch.tsx +++ b/lib/api/useApiFetch.tsx @@ -41,6 +41,7 @@ export default function useApiFetch() { 'x-endpoint': resource.endpoint && isNeedProxy() ? resource.endpoint : undefined, Authorization: resource.endpoint && resource.needAuth ? apiToken : undefined, 'x-csrf-token': withBody && csrfToken ? csrfToken : undefined, + ...resource.headers, ...fetchParams?.headers, }, Boolean) as HeadersInit; diff --git a/lib/blob/guessDataType.ts b/lib/blob/guessDataType.ts new file mode 100644 index 0000000000..fb409019e3 --- /dev/null +++ b/lib/blob/guessDataType.ts @@ -0,0 +1,12 @@ +import filetype from 'magic-bytes.js'; + +import hexToBytes from 'lib/hexToBytes'; + +import removeNonSignificantZeroBytes from './removeNonSignificantZeroBytes'; + +export default function guessDataType(data: string) { + const bytes = new Uint8Array(hexToBytes(data)); + const filteredBytes = removeNonSignificantZeroBytes(bytes); + + return filetype(filteredBytes)[0]; +} diff --git a/lib/blob/index.ts b/lib/blob/index.ts new file mode 100644 index 0000000000..ab178e8231 --- /dev/null +++ b/lib/blob/index.ts @@ -0,0 +1 @@ +export { default as guessDataType } from './guessDataType'; diff --git a/lib/blob/removeNonSignificantZeroBytes.ts b/lib/blob/removeNonSignificantZeroBytes.ts new file mode 100644 index 0000000000..9b25287478 --- /dev/null +++ b/lib/blob/removeNonSignificantZeroBytes.ts @@ -0,0 +1,20 @@ +export default function removeNonSignificantZeroBytes(bytes: Uint8Array) { + return shouldRemoveBytes(bytes) ? bytes.filter((item, index) => index % 32) : bytes; +} + +// check if every 0, 32, 64, etc byte is 0 in the provided array +function shouldRemoveBytes(bytes: Uint8Array) { + let result = true; + + for (let index = 0; index < bytes.length; index += 32) { + const element = bytes[index]; + if (element === 0) { + continue; + } else { + result = false; + break; + } + } + + return result; +} diff --git a/lib/bytesToBase64.ts b/lib/bytesToBase64.ts new file mode 100644 index 0000000000..60b23ad437 --- /dev/null +++ b/lib/bytesToBase64.ts @@ -0,0 +1,10 @@ +export default function bytesToBase64(bytes: Uint8Array) { + let binary = ''; + for (const byte of bytes) { + binary += String.fromCharCode(byte); + } + + const base64String = btoa(binary); + + return base64String; +} diff --git a/lib/contexts/addressHighlight.tsx b/lib/contexts/addressHighlight.tsx index f4bb79e8ff..84f5f896ec 100644 --- a/lib/contexts/addressHighlight.tsx +++ b/lib/contexts/addressHighlight.tsx @@ -5,7 +5,6 @@ interface AddressHighlightProviderProps { } interface TAddressHighlightContext { - highlightedAddress: string | null; onMouseEnter: (event: React.MouseEvent) => void; onMouseLeave: (event: React.MouseEvent) => void; } @@ -13,30 +12,40 @@ interface TAddressHighlightContext { export const AddressHighlightContext = React.createContext(null); export function AddressHighlightProvider({ children }: AddressHighlightProviderProps) { - const [ highlightedAddress, setHighlightedAddress ] = React.useState(null); const timeoutId = React.useRef(null); + const hashRef = React.useRef(null); const onMouseEnter = React.useCallback((event: React.MouseEvent) => { const hash = event.currentTarget.getAttribute('data-hash'); if (hash) { + hashRef.current = hash; timeoutId.current = window.setTimeout(() => { - setHighlightedAddress(hash); + // for better performance we update DOM-nodes directly bypassing React reconciliation + const nodes = window.document.querySelectorAll(`[data-hash="${ hashRef.current }"]`); + for (const node of nodes) { + node.classList.add('address-entity_highlighted'); + } }, 100); } }, []); const onMouseLeave = React.useCallback(() => { - setHighlightedAddress(null); + if (hashRef.current) { + const nodes = window.document.querySelectorAll(`[data-hash="${ hashRef.current }"]`); + for (const node of nodes) { + node.classList.remove('address-entity_highlighted'); + } + hashRef.current = null; + } typeof timeoutId.current === 'number' && window.clearTimeout(timeoutId.current); }, []); const value = React.useMemo(() => { return { - highlightedAddress, onMouseEnter, onMouseLeave, }; - }, [ highlightedAddress, onMouseEnter, onMouseLeave ]); + }, [ onMouseEnter, onMouseLeave ]); React.useEffect(() => { return () => { diff --git a/lib/contracts/licenses.ts b/lib/contracts/licenses.ts new file mode 100644 index 0000000000..123149e294 --- /dev/null +++ b/lib/contracts/licenses.ts @@ -0,0 +1,88 @@ +import type { ContractLicense } from 'types/client/contract'; + +export const CONTRACT_LICENSES: Array = [ + { + type: 'none', + label: 'None', + title: 'No License', + url: 'https://choosealicense.com/no-permission/', + }, + { + type: 'unlicense', + label: 'Unlicense', + title: 'The Unlicense', + url: 'https://choosealicense.com/licenses/unlicense/', + }, + { + type: 'mit', + label: 'MIT', + title: 'MIT License', + url: 'https://choosealicense.com/licenses/mit/', + }, + { + type: 'gnu_gpl_v2', + label: 'GNU GPLv2', + title: 'GNU General Public License v2.0', + url: 'https://choosealicense.com/licenses/gpl-2.0/', + }, + { + type: 'gnu_gpl_v3', + label: 'GNU GPLv3', + title: 'GNU General Public License v3.0', + url: 'https://choosealicense.com/licenses/gpl-3.0/', + }, + { + type: 'gnu_lgpl_v2_1', + label: 'GNU LGPLv2.1', + title: 'GNU Lesser General Public License v2.1', + url: 'https://choosealicense.com/licenses/lgpl-2.1/', + }, + { + type: 'gnu_lgpl_v3', + label: 'GNU LGPLv3', + title: 'GNU Lesser General Public License v3.0', + url: 'https://choosealicense.com/licenses/lgpl-3.0/', + }, + { + type: 'bsd_2_clause', + label: 'BSD-2-Clause', + title: 'BSD 2-clause "Simplified" license', + url: 'https://choosealicense.com/licenses/bsd-2-clause/', + }, + { + type: 'bsd_3_clause', + label: 'BSD-3-Clause', + title: 'BSD 3-clause "New" Or "Revised" license', + url: 'https://choosealicense.com/licenses/bsd-3-clause/', + }, + { + type: 'mpl_2_0', + label: 'MPL-2.0', + title: 'Mozilla Public License 2.0', + url: 'https://choosealicense.com/licenses/mpl-2.0/', + }, + { + type: 'osl_3_0', + label: 'OSL-3.0', + title: 'Open Software License 3.0', + url: 'https://choosealicense.com/licenses/osl-3.0/', + }, + { + type: 'apache_2_0', + label: 'Apache', + title: 'Apache 2.0', + url: 'https://choosealicense.com/licenses/apache-2.0/', + }, + { + type: 'gnu_agpl_v3', + label: 'GNU AGPLv3', + title: 'GNU Affero General Public License', + url: 'https://choosealicense.com/licenses/agpl-3.0/', + }, + { + type: 'bsl_1_1', + label: 'BSL 1.1', + title: 'Business Source License', + url: 'https://mariadb.com/bsl11/', + }, +]; diff --git a/lib/growthbook/init.ts b/lib/growthbook/init.ts index 15188c8c7c..d98b2b94b7 100644 --- a/lib/growthbook/init.ts +++ b/lib/growthbook/init.ts @@ -7,7 +7,6 @@ import { STORAGE_KEY, STORAGE_LIMIT } from './consts'; export interface GrowthBookFeatures { test_value: string; - marketplace_exp: boolean; } export const growthBook = (() => { diff --git a/lib/hexToBase64.ts b/lib/hexToBase64.ts new file mode 100644 index 0000000000..5b1366a6da --- /dev/null +++ b/lib/hexToBase64.ts @@ -0,0 +1,8 @@ +import bytesToBase64 from './bytesToBase64'; +import hexToBytes from './hexToBytes'; + +export default function hexToBase64(hex: string) { + const bytes = new Uint8Array(hexToBytes(hex)); + + return bytesToBase64(bytes); +} diff --git a/lib/hexToBytes.ts b/lib/hexToBytes.ts index 382fd777d3..e34435fbf4 100644 --- a/lib/hexToBytes.ts +++ b/lib/hexToBytes.ts @@ -1,6 +1,8 @@ +// hex can be with prefix - `0x{string}` - or without it - `{string}` export default function hexToBytes(hex: string) { const bytes = []; - for (let c = 0; c < hex.length; c += 2) { + const startIndex = hex.startsWith('0x') ? 2 : 0; + for (let c = startIndex; c < hex.length; c += 2) { bytes.push(parseInt(hex.substring(c, c + 2), 16)); } return bytes; diff --git a/lib/hooks/useNavItems.tsx b/lib/hooks/useNavItems.tsx index 036ae88328..52f8b0ca2d 100644 --- a/lib/hooks/useNavItems.tsx +++ b/lib/hooks/useNavItems.tsx @@ -66,6 +66,12 @@ export default function useNavItems(): ReturnType { icon: 'ENS', isActive: pathname === '/name-domains' || pathname === '/name-domains/[name]', } : null; + const validators = config.features.validators.isEnabled ? { + text: 'Top validators', + nextRoute: { pathname: '/validators' as const }, + icon: 'validator', + isActive: pathname === '/validators', + } : null; const rollupFeature = config.features.rollup; @@ -84,6 +90,7 @@ export default function useNavItems(): ReturnType { ].filter(Boolean), [ topAccounts, + validators, verifiedContracts, ensLookup, ].filter(Boolean), @@ -105,6 +112,24 @@ export default function useNavItems(): ReturnType { { text: 'Output roots', nextRoute: { pathname: '/output-roots' as const }, icon: 'output_roots', isActive: pathname === '/output-roots' }, ], [ + userOps, + topAccounts, + validators, + verifiedContracts, + ensLookup, + ].filter(Boolean), + ]; + } else if (rollupFeature.isEnabled && rollupFeature.type === 'shibarium') { + blockchainNavItems = [ + [ + txs, + // eslint-disable-next-line max-len + { text: `Deposits (L1${ rightLineArrow }L2)`, nextRoute: { pathname: '/deposits' as const }, icon: 'arrows/south-east', isActive: pathname === '/deposits' }, + // eslint-disable-next-line max-len + { text: `Withdrawals (L2${ rightLineArrow }L1)`, nextRoute: { pathname: '/withdrawals' as const }, icon: 'arrows/north-east', isActive: pathname === '/withdrawals' }, + ], + [ + blocks, userOps, topAccounts, verifiedContracts, @@ -117,6 +142,7 @@ export default function useNavItems(): ReturnType { userOps, blocks, topAccounts, + validators, verifiedContracts, ensLookup, config.features.beaconChain.isEnabled && { @@ -167,7 +193,7 @@ export default function useNavItems(): ReturnType { isActive: pathname.startsWith('/token'), }, config.features.marketplace.isEnabled ? { - text: 'Apps', + text: 'DApps', nextRoute: { pathname: '/apps' as const }, icon: 'apps', isActive: pathname.startsWith('/app'), @@ -193,8 +219,13 @@ export default function useNavItems(): ReturnType { nextRoute: { pathname: '/contract-verification' as const }, isActive: pathname.startsWith('/contract-verification'), }, + config.features.gasTracker.isEnabled && { + text: 'Gas tracker', + nextRoute: { pathname: '/gas-tracker' as const }, + isActive: pathname.startsWith('/gas-tracker'), + }, ...config.UI.sidebar.otherLinks, - ], + ].filter(Boolean), }, ].filter(Boolean); diff --git a/lib/hooks/useNotifyOnNavigation.tsx b/lib/hooks/useNotifyOnNavigation.tsx new file mode 100644 index 0000000000..a3b7a7c92f --- /dev/null +++ b/lib/hooks/useNotifyOnNavigation.tsx @@ -0,0 +1,24 @@ +import { usePathname } from 'next/navigation'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import config from 'configs/app'; +import getQueryParamString from 'lib/router/getQueryParamString'; + +export default function useNotifyOnNavigation() { + const router = useRouter(); + const pathname = usePathname(); + const tab = getQueryParamString(router.query.tab); + + React.useEffect(() => { + if (config.features.metasuites.isEnabled) { + window.postMessage({ source: 'APP_ROUTER', type: 'PATHNAME_CHANGED' }, window.location.origin); + } + }, [ pathname ]); + + React.useEffect(() => { + if (config.features.metasuites.isEnabled) { + window.postMessage({ source: 'APP_ROUTER', type: 'TAB_CHANGED' }, window.location.origin); + } + }, [ tab ]); +} diff --git a/lib/metadata/getPageOgType.ts b/lib/metadata/getPageOgType.ts index e0b68d9334..ef7ef24e26 100644 --- a/lib/metadata/getPageOgType.ts +++ b/lib/metadata/getPageOgType.ts @@ -37,11 +37,14 @@ const OG_TYPE_DICT: Record = { '/output-roots': 'Root page', '/batches': 'Root page', '/batches/[number]': 'Regular page', + '/blobs/[hash]': 'Regular page', '/ops': 'Root page', '/op/[hash]': 'Regular page', '/404': 'Regular page', '/name-domains': 'Root page', '/name-domains/[name]': 'Regular page', + '/validators': 'Root page', + '/gas-tracker': 'Root page', // service routes, added only to make typescript happy '/login': 'Regular page', diff --git a/lib/metadata/templates/description.ts b/lib/metadata/templates/description.ts index 064d5e79f6..c064709b6f 100644 --- a/lib/metadata/templates/description.ts +++ b/lib/metadata/templates/description.ts @@ -40,11 +40,14 @@ const TEMPLATE_MAP: Record = { '/output-roots': DEFAULT_TEMPLATE, '/batches': DEFAULT_TEMPLATE, '/batches/[number]': DEFAULT_TEMPLATE, + '/blobs/[hash]': DEFAULT_TEMPLATE, '/ops': DEFAULT_TEMPLATE, '/op/[hash]': DEFAULT_TEMPLATE, '/404': DEFAULT_TEMPLATE, '/name-domains': DEFAULT_TEMPLATE, '/name-domains/[name]': DEFAULT_TEMPLATE, + '/validators': DEFAULT_TEMPLATE, + '/gas-tracker': DEFAULT_TEMPLATE, // service routes, added only to make typescript happy '/login': DEFAULT_TEMPLATE, diff --git a/lib/metadata/templates/title.ts b/lib/metadata/templates/title.ts index 1bd6c7a933..9a667bb9ce 100644 --- a/lib/metadata/templates/title.ts +++ b/lib/metadata/templates/title.ts @@ -14,7 +14,7 @@ const TEMPLATE_MAP: Record = { '/address/[hash]/contract-verification': 'contract verification for %hash%', '/tokens': 'tokens', '/token/[hash]': '%symbol% token details', - '/token/[hash]/instance/[id]': 'token instance for %symbol%', + '/token/[hash]/instance/[id]': 'NFT instance', '/apps': 'apps marketplace', '/apps/[id]': '- %app_name%', '/stats': 'statistics', @@ -35,11 +35,14 @@ const TEMPLATE_MAP: Record = { '/output-roots': 'output roots', '/batches': 'tx batches (L2 blocks)', '/batches/[number]': 'L2 tx batch %number%', + '/blobs/[hash]': 'blob %hash% details', '/ops': 'user operations', '/op/[hash]': 'user operation %hash%', '/404': 'error - page not found', '/name-domains': 'domains search and resolve', '/name-domains/[name]': '%name% domain details', + '/validators': 'validators list', + '/gas-tracker': 'gas tracker', // service routes, added only to make typescript happy '/login': 'login', diff --git a/lib/metadata/update.ts b/lib/metadata/update.ts index 0171973cb7..f6168c1ae6 100644 --- a/lib/metadata/update.ts +++ b/lib/metadata/update.ts @@ -5,11 +5,8 @@ import type { Route } from 'nextjs-routes'; import generate from './generate'; export default function update(route: R, apiData: ApiData) { - const { title, description, opengraph } = generate(route, apiData); + const { title, description } = generate(route, apiData); window.document.title = title; window.document.querySelector('meta[name="description"]')?.setAttribute('content', description); - window.document.querySelector('meta[property="og:title"]')?.setAttribute('content', opengraph.title); - opengraph.description && - window.document.querySelector('meta[property="og:description"]')?.setAttribute('content', opengraph.description); } diff --git a/lib/mixpanel/getPageType.ts b/lib/mixpanel/getPageType.ts index fa1745a510..fb4c48e7fe 100644 --- a/lib/mixpanel/getPageType.ts +++ b/lib/mixpanel/getPageType.ts @@ -15,8 +15,8 @@ export const PAGE_TYPE_DICT: Record = { '/tokens': 'Tokens', '/token/[hash]': 'Token details', '/token/[hash]/instance/[id]': 'Token Instance', - '/apps': 'Apps', - '/apps/[id]': 'App', + '/apps': 'DApps', + '/apps/[id]': 'DApp', '/stats': 'Stats', '/api-docs': 'REST API', '/graphiql': 'GraphQL', @@ -35,11 +35,14 @@ export const PAGE_TYPE_DICT: Record = { '/output-roots': 'Output roots', '/batches': 'Tx batches (L2 blocks)', '/batches/[number]': 'L2 tx batch details', + '/blobs/[hash]': 'Blob details', '/ops': 'User operations', '/op/[hash]': 'User operation details', '/404': '404', '/name-domains': 'Domains search and resolve', '/name-domains/[name]': 'Domain details', + '/validators': 'Validators list', + '/gas-tracker': 'Gas tracker', // service routes, added only to make typescript happy '/login': 'Login', diff --git a/lib/mixpanel/utils.ts b/lib/mixpanel/utils.ts index db7a89b34a..3015e472db 100644 --- a/lib/mixpanel/utils.ts +++ b/lib/mixpanel/utils.ts @@ -17,7 +17,8 @@ export enum EventTypes { PAGE_WIDGET = 'Page widget', TX_INTERPRETATION_INTERACTION = 'Transaction interpratetion interaction', EXPERIMENT_STARTED = 'Experiment started', - FILTERS = 'Filters' + FILTERS = 'Filters', + BUTTON_CLICK = 'Button click', } /* eslint-disable @typescript-eslint/indent */ @@ -73,9 +74,15 @@ Type extends EventTypes.WALLET_CONNECT ? { 'Source': 'Header' | 'Smart contracts' | 'Swap button'; 'Status': 'Started' | 'Connected'; } : -Type extends EventTypes.WALLET_ACTION ? { - 'Action': 'Open' | 'Address click'; -} : +Type extends EventTypes.WALLET_ACTION ? ( + { + 'Action': 'Open' | 'Address click'; + } | { + 'Action': 'Send Transaction' | 'Sign Message' | 'Sign Typed Data'; + 'Address': string | undefined; + 'AppId': string; + } +) : Type extends EventTypes.CONTRACT_INTERACTION ? { 'Method type': 'Read' | 'Write'; 'Method name': string; @@ -107,5 +114,9 @@ Type extends EventTypes.FILTERS ? { 'Source': 'Marketplace'; 'Filter name': string; } : +Type extends EventTypes.BUTTON_CLICK ? { + 'Content': 'Swap button'; + 'Source': string; +} : undefined; /* eslint-enable @typescript-eslint/indent */ diff --git a/lib/token/metadata/attributesParser.ts b/lib/token/metadata/attributesParser.ts index c863000366..00119abe90 100644 --- a/lib/token/metadata/attributesParser.ts +++ b/lib/token/metadata/attributesParser.ts @@ -19,7 +19,7 @@ function formatValue(value: string | number, display: string | undefined, trait: } case 'date': { return { - value: dayjs(value).format('YYYY-MM-DD'), + value: dayjs(Number(value) * 1000).format('YYYY-MM-DD'), }; } default: { diff --git a/mocks/address/tokens.ts b/mocks/address/tokens.ts index c8c7386134..9378525817 100644 --- a/mocks/address/tokens.ts +++ b/mocks/address/tokens.ts @@ -38,6 +38,17 @@ export const erc20LongSymbol: AddressTokenBalance = { token_instance: null, }; +export const erc20BigAmount: AddressTokenBalance = { + token: { + ...tokens.tokenInfoERC20LongSymbol, + exchange_rate: '4200000000', + name: 'DuckDuckGoose Stable Coin', + }, + token_id: null, + value: '39000000000000000000', + token_instance: null, +}; + export const erc721a: AddressTokenBalance = { token: tokens.tokenInfoERC721a, token_id: null, diff --git a/mocks/apps/app.html b/mocks/apps/app.html new file mode 100644 index 0000000000..c7c675b977 --- /dev/null +++ b/mocks/apps/app.html @@ -0,0 +1,32 @@ + + + + + Mock HTML Content + + + +

Full view app

+ + diff --git a/mocks/apps/apps.ts b/mocks/apps/apps.ts index a8b27a70a4..2f748c625a 100644 --- a/mocks/apps/apps.ts +++ b/mocks/apps/apps.ts @@ -11,6 +11,9 @@ export const apps = [ description: 'Hop is a scalable rollup-to-rollup general token bridge. It allows users to send tokens from one rollup or sidechain to another almost immediately without having to wait for the networks challenge period.', external: true, url: 'https://goerli.hop.exchange/send?token=ETH&sourceNetwork=ethereum', + github: [ 'https://github.com/hop-protocol/hop', 'https://github.com/hop-protocol/hop-ui' ], + discord: 'https://discord.gg/hopprotocol', + twitter: 'https://twitter.com/HopProtocol', }, { author: 'Blockscout', diff --git a/mocks/blobs/blobs.ts b/mocks/blobs/blobs.ts new file mode 100644 index 0000000000..24d25b465f --- /dev/null +++ b/mocks/blobs/blobs.ts @@ -0,0 +1,36 @@ +import type { Blob, TxBlobs } from 'types/api/blobs'; + +export const base1: Blob = { + blob_data: '0x004242004242004242004242004242004242', + hash: '0x016316f61a259aa607096440fc3eeb90356e079be01975d2fb18347bd50df33c', + kzg_commitment: '0xa95caabd009e189b9f205e0328ff847ad886e4f8e719bd7219875fbb9688fb3fbe7704bb1dfa7e2993a3dea8d0cf767d', + kzg_proof: '0x89cf91c4c8be6f2a390d4262425f79dffb74c174fb15a210182184543bf7394e5a7970a774ee8e0dabc315424c22df0f', + transaction_hashes: [ + { block_consensus: true, transaction_hash: '0x970d8c45c713a50a1fa351b00ca29a8890cac474c59cc8eee4eddec91a1729f0' }, + ], +}; + +export const base2: Blob = { + blob_data: '0x89504E470D0A1A0A0000000D494844520000003C0000003C0403', + hash: '0x0197fdb17195c176b23160f335daabd4b6a231aaaadd73ec567877c66a3affd1', + kzg_commitment: '0x89b0d8ac715ee134135471994a161ef068a784f51982fcd7161aa8e3e818eb83017ccfbfc30c89b796a2743d77554e2f', + kzg_proof: '0x8255a6c6a236483814b8e68992e70f3523f546866a9fed6b8e0ecfef314c65634113b8aa02d6c5c6e91b46e140f17a07', + transaction_hashes: [ + { block_consensus: true, transaction_hash: '0x22d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193' }, + ], +}; + +export const withoutData: Blob = { + blob_data: null, + hash: '0x0197fdb17195c176b23160f335daabd4b6a231aaaadd73ec567877c66a3affd3', + kzg_commitment: null, + kzg_proof: null, + transaction_hashes: [ + { block_consensus: true, transaction_hash: '0x22d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193' }, + ], +}; + +export const txBlobs: TxBlobs = { + items: [ base1, base2, withoutData ], + next_page_params: null, +}; diff --git a/mocks/blocks/block.ts b/mocks/blocks/block.ts index c386d16916..eb182d2ebf 100644 --- a/mocks/blocks/block.ts +++ b/mocks/blocks/block.ts @@ -135,6 +135,15 @@ export const rootstock: Block = { minimum_gas_price: '59240000', }; +export const withBlobTxs: Block = { + ...base, + blob_gas_price: '21518435987', + blob_gas_used: '393216', + burnt_blob_fees: '8461393325064192', + excess_blob_gas: '79429632', + blob_tx_count: 1, +}; + export const baseListResponse: BlocksResponse = { items: [ base, diff --git a/mocks/contract/audits.ts b/mocks/contract/audits.ts new file mode 100644 index 0000000000..a2a6644229 --- /dev/null +++ b/mocks/contract/audits.ts @@ -0,0 +1,16 @@ +import type { SmartContractSecurityAudits } from 'types/api/contract'; + +export const contractAudits: SmartContractSecurityAudits = { + items: [ + { + audit_company_name: 'OpenZeppelin', + audit_publish_date: '2023-03-01', + audit_report_url: 'https://blog.openzeppelin.com/eip-4337-ethereum-account-abstraction-incremental-audit', + }, + { + audit_company_name: 'OpenZeppelin', + audit_publish_date: '2023-03-01', + audit_report_url: 'https://blog.openzeppelin.com/eip-4337-ethereum-account-abstraction-incremental-audit', + }, + ], +}; diff --git a/mocks/contract/info.ts b/mocks/contract/info.ts index c5e1a7e040..a797383474 100644 --- a/mocks/contract/info.ts +++ b/mocks/contract/info.ts @@ -31,6 +31,7 @@ export const verified: Partial = { { address_hash: '0xa62744BeE8646e237441CDbfdedD3458861748A8', name: 'math' }, ], language: 'solidity', + license_type: 'gnu_gpl_v3', }; export const withMultiplePaths: Partial = { diff --git a/mocks/contract/methods.ts b/mocks/contract/methods.ts index 745c19770a..6c1bdf367e 100644 --- a/mocks/contract/methods.ts +++ b/mocks/contract/methods.ts @@ -132,6 +132,7 @@ export const write: Array = [ payable: false, stateMutability: 'nonpayable', type: 'function', + method_id: '0x01', }, { constant: false, @@ -146,6 +147,7 @@ export const write: Array = [ payable: true, stateMutability: 'payable', type: 'function', + method_id: '0x02', }, { stateMutability: 'payable', @@ -159,6 +161,7 @@ export const write: Array = [ payable: false, stateMutability: 'nonpayable', type: 'function', + method_id: '0x03', }, { constant: false, @@ -173,6 +176,7 @@ export const write: Array = [ payable: false, stateMutability: 'nonpayable', type: 'function', + method_id: '0x04', }, { constant: false, @@ -190,6 +194,7 @@ export const write: Array = [ payable: false, stateMutability: 'nonpayable', type: 'function', + method_id: '0x05', }, { constant: false, @@ -208,5 +213,6 @@ export const write: Array = [ payable: false, stateMutability: 'nonpayable', type: 'function', + method_id: '0x06', }, ]; diff --git a/mocks/contracts/index.ts b/mocks/contracts/index.ts index 6db06926ba..bc3b4ecfb2 100644 --- a/mocks/contracts/index.ts +++ b/mocks/contracts/index.ts @@ -20,6 +20,7 @@ export const contract1: VerifiedContract = { optimization_enabled: false, tx_count: 7334224, verified_at: '2022-09-16T18:49:29.605179Z', + license_type: 'mit', }; export const contract2: VerifiedContract = { @@ -42,6 +43,7 @@ export const contract2: VerifiedContract = { optimization_enabled: true, tx_count: 440, verified_at: '2021-09-07T20:01:56.076979Z', + license_type: 'bsd_3_clause', }; export const baseResponse: VerifiedContractsResponse = { diff --git a/mocks/search/index.ts b/mocks/search/index.ts index ef384d1d70..af2555aa28 100644 --- a/mocks/search/index.ts +++ b/mocks/search/index.ts @@ -6,6 +6,7 @@ import type { SearchResultLabel, SearchResult, SearchResultUserOp, + SearchResultBlob, } from 'types/api/search'; export const token1: SearchResultToken = { @@ -116,6 +117,12 @@ export const userOp1: SearchResultUserOp = { url: '/op/0xcb560d77b0f3af074fa05c1e5c691bcdfe457e630062b5907e9e71fc74b2ec61', }; +export const blob1: SearchResultBlob = { + blob_hash: '0x0108dd3e414da9f3255f7a831afa606e8dfaea93d082dfa9b15305583cbbdbbe', + type: 'blob' as const, + timestamp: null, +}; + export const baseResponse: SearchResult = { items: [ token1, @@ -124,6 +131,7 @@ export const baseResponse: SearchResult = { address1, contract1, tx1, + blob1, ], next_page_params: null, }; diff --git a/mocks/shibarium/deposits.ts b/mocks/shibarium/deposits.ts new file mode 100644 index 0000000000..98bf9d925d --- /dev/null +++ b/mocks/shibarium/deposits.ts @@ -0,0 +1,61 @@ +import type { ShibariumDepositsResponse } from 'types/api/shibarium'; + +export const data: ShibariumDepositsResponse = { + items: [ + { + l1_block_number: 8382841, + timestamp: '2022-05-27T01:13:48.000000Z', + l1_transaction_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e', + user: { + hash: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5', + implementation_name: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + l2_transaction_hash: '0xb9212c76069b926917816767e4c5a0ef80e519b1ac1c3d3fb5818078f4984667', + }, + { + l1_block_number: 8382841, + timestamp: '2022-05-27T01:13:48.000000Z', + l1_transaction_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e', + user: { + hash: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5', + implementation_name: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + l2_transaction_hash: '0xb9212c76069b926917816767e4c5a0ef80e519b1ac1c3d3fb5818078f4984667', + }, + { + l1_block_number: 8382841, + timestamp: '2022-05-27T01:13:48.000000Z', + l1_transaction_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e', + user: { + hash: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5', + implementation_name: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + l2_transaction_hash: '0xb9212c76069b926917816767e4c5a0ef80e519b1ac1c3d3fb5818078f4984667', + }, + ], + next_page_params: { + items_count: 50, + block_number: 8382363, + }, +}; diff --git a/mocks/shibarium/withdrawals.ts b/mocks/shibarium/withdrawals.ts new file mode 100644 index 0000000000..79851f3d1b --- /dev/null +++ b/mocks/shibarium/withdrawals.ts @@ -0,0 +1,61 @@ +import type { ShibariumWithdrawalsResponse } from 'types/api/shibarium'; + +export const data: ShibariumWithdrawalsResponse = { + items: [ + { + l2_block_number: 8382841, + timestamp: '2022-05-27T01:13:48.000000Z', + l1_transaction_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e', + user: { + hash: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5', + implementation_name: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + l2_transaction_hash: '0xb9212c76069b926917816767e4c5a0ef80e519b1ac1c3d3fb5818078f4984667', + }, + { + l2_block_number: 8382841, + timestamp: '2022-05-27T01:13:48.000000Z', + l1_transaction_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e', + user: { + hash: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5', + implementation_name: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + l2_transaction_hash: '0xb9212c76069b926917816767e4c5a0ef80e519b1ac1c3d3fb5818078f4984667', + }, + { + l2_block_number: 8382841, + timestamp: '2022-05-27T01:13:48.000000Z', + l1_transaction_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e', + user: { + hash: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5', + implementation_name: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + l2_transaction_hash: '0xb9212c76069b926917816767e4c5a0ef80e519b1ac1c3d3fb5818078f4984667', + }, + ], + next_page_params: { + items_count: 50, + block_number: 8382363, + }, +}; diff --git a/mocks/stats/daily_txs.ts b/mocks/stats/daily_txs.ts index 03cb3f20ac..afb2dcca58 100644 --- a/mocks/stats/daily_txs.ts +++ b/mocks/stats/daily_txs.ts @@ -126,3 +126,24 @@ export const base = { }, ], }; + +export const partialData = { + chart_data: [ + { date: '2022-11-28', tx_count: 26815 }, + { date: '2022-11-27', tx_count: 34784 }, + { date: '2022-11-26', tx_count: 77527 }, + { date: '2022-11-25', tx_count: null }, + { date: '2022-11-24', tx_count: null }, + { date: '2022-11-23', tx_count: null }, + { date: '2022-11-22', tx_count: 63433 }, + { date: '2022-11-21', tx_count: null }, + ], +}; + +export const noData = { + chart_data: [ + { date: '2022-11-25', tx_count: null }, + { date: '2022-11-24', tx_count: null }, + { date: '2022-11-23', tx_count: null }, + ], +}; diff --git a/mocks/stats/index.ts b/mocks/stats/index.ts index 4e7a63f59c..d9d00e36e1 100644 --- a/mocks/stats/index.ts +++ b/mocks/stats/index.ts @@ -1,24 +1,30 @@ -import type { HomeStats } from 'types/api/stats'; +import _mapValues from 'lodash/mapValues'; -export const base: HomeStats = { +export const base = { average_block_time: 6212.0, coin_price: '0.00199678', coin_price_change_percentage: -7.42, gas_prices: { average: { - fiat_price: '1.01', - price: 20.41, - time: 12283, + fiat_price: '1.39', + price: 23.75, + time: 12030.25, + base_fee: 2.22222, + priority_fee: 12.424242, }, fast: { - fiat_price: '1.26', - price: 25.47, - time: 9321, + fiat_price: '1.74', + price: 29.72, + time: 8763.25, + base_fee: 4.44444, + priority_fee: 22.242424, }, slow: { - fiat_price: '0.97', - price: 19.55, - time: 24543, + fiat_price: '1.35', + price: 23.04, + time: 20100.25, + base_fee: 1.11111, + priority_fee: 7.8909, }, }, gas_price_updated_at: '2022-11-11T11:09:49.051171Z', @@ -35,7 +41,30 @@ export const base: HomeStats = { tvl: '1767425.102766552', }; -export const withBtcLocked: HomeStats = { +export const withBtcLocked = { ...base, rootstock_locked_btc: '3337493406696977561374', }; + +export const withoutFiatPrices = { + ...base, + gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, fiat_price: null }) : null), +}; + +export const withoutGweiPrices = { + ...base, + gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null }) : null), +}; + +export const withoutBothPrices = { + ...base, + gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null, fiat_price: null }) : null), +}; + +export const noChartData = { + ...base, + transactions_today: null, + coin_price: null, + market_cap: null, + tvl: null, +}; diff --git a/mocks/stats/line.ts b/mocks/stats/line.ts new file mode 100644 index 0000000000..47de79b184 --- /dev/null +++ b/mocks/stats/line.ts @@ -0,0 +1,128 @@ +export const averageGasPrice = { + chart: [ + { + date: '2023-12-22', + value: '37.7804422597599', + }, + { + date: '2023-12-23', + value: '25.84889883009387', + }, + { + date: '2023-12-24', + value: '25.818463227198574', + }, + { + date: '2023-12-25', + value: '26.045513050051298', + }, + { + date: '2023-12-26', + value: '21.42600692652399', + }, + { + date: '2023-12-27', + value: '31.066730409846656', + }, + { + date: '2023-12-28', + value: '33.63955781902089', + }, + { + date: '2023-12-29', + value: '28.064736756058384', + }, + { + date: '2023-12-30', + value: '23.074500869678175', + }, + { + date: '2023-12-31', + value: '17.651005734615133', + }, + { + date: '2024-01-01', + value: '14.906085174476441', + }, + { + date: '2024-01-02', + value: '22.28459059038656', + }, + { + date: '2024-01-03', + value: '39.8311646806592', + }, + { + date: '2024-01-04', + value: '26.09989322256083', + }, + { + date: '2024-01-05', + value: '22.821996688111998', + }, + { + date: '2024-01-06', + value: '20.32680041262083', + }, + { + date: '2024-01-07', + value: '32.535045831809704', + }, + { + date: '2024-01-08', + value: '27.443477102139482', + }, + { + date: '2024-01-09', + value: '20.7911332558055', + }, + { + date: '2024-01-10', + value: '42.10740192523919', + }, + { + date: '2024-01-11', + value: '35.75215680343582', + }, + { + date: '2024-01-12', + value: '27.430414798093253', + }, + { + date: '2024-01-13', + value: '20.170934096589875', + }, + { + date: '2024-01-14', + value: '38.79660984371034', + }, + { + date: '2024-01-15', + value: '26.140740484554204', + }, + { + date: '2024-01-16', + value: '36.708543184194156', + }, + { + date: '2024-01-17', + value: '40.325438794298876', + }, + { + date: '2024-01-18', + value: '37.55145309930694', + }, + { + date: '2024-01-19', + value: '33.271450114434664', + }, + { + date: '2024-01-20', + value: '19.303304377685638', + }, + { + date: '2024-01-21', + value: '14.375908594704976', + }, + ], +}; diff --git a/mocks/stats/lines.ts b/mocks/stats/lines.ts new file mode 100644 index 0000000000..51aca3c97d --- /dev/null +++ b/mocks/stats/lines.ts @@ -0,0 +1,142 @@ +export const base = { + sections: [ + { + id: 'accounts', + title: 'Accounts', + charts: [ + { + id: 'accountsGrowth', + title: 'Accounts growth', + description: 'Cumulative accounts number per period', + units: null, + }, + { + id: 'activeAccounts', + title: 'Active accounts', + description: 'Active accounts number per period', + units: null, + }, + { + id: 'newAccounts', + title: 'New accounts', + description: 'New accounts number per day', + units: null, + }, + ], + }, + { + id: 'transactions', + title: 'Transactions', + charts: [ + { + id: 'averageTxnFee', + title: 'Average transaction fee', + description: 'The average amount in ETH spent per transaction', + units: 'ETH', + }, + { + id: 'newTxns', + title: 'New transactions', + description: 'New transactions number', + units: null, + }, + { + id: 'txnsFee', + title: 'Transactions fees', + description: 'Amount of tokens paid as fees', + units: 'ETH', + }, + { + id: 'txnsGrowth', + title: 'Transactions growth', + description: 'Cumulative transactions number', + units: null, + }, + { + id: 'txnsSuccessRate', + title: 'Transactions success rate', + description: 'Successful transactions rate per day', + units: null, + }, + ], + }, + { + id: 'blocks', + title: 'Blocks', + charts: [ + { + id: 'averageBlockRewards', + title: 'Average block rewards', + description: 'Average amount of distributed reward in tokens per day', + units: 'ETH', + }, + { + id: 'averageBlockSize', + title: 'Average block size', + description: 'Average size of blocks in bytes', + units: 'Bytes', + }, + { + id: 'newBlocks', + title: 'New blocks', + description: 'New blocks number', + units: null, + }, + ], + }, + { + id: 'tokens', + title: 'Tokens', + charts: [ + { + id: 'newNativeCoinTransfers', + title: 'New ETH transfers', + description: 'New token transfers number for the period', + units: null, + }, + ], + }, + { + id: 'gas', + title: 'Gas', + charts: [ + { + id: 'averageGasLimit', + title: 'Average gas limit', + description: 'Average gas limit per block for the period', + units: null, + }, + { + id: 'averageGasPrice', + title: 'Average gas price', + description: 'Average gas price for the period (Gwei)', + units: 'Gwei', + }, + { + id: 'gasUsedGrowth', + title: 'Gas used growth', + description: 'Cumulative gas used for the period', + units: null, + }, + ], + }, + { + id: 'contracts', + title: 'Contracts', + charts: [ + { + id: 'newVerifiedContracts', + title: 'New verified contracts', + description: 'New verified contracts number for the period', + units: null, + }, + { + id: 'verifiedContractsGrowth', + title: 'Verified contracts growth', + description: 'Cumulative number verified contracts for the period', + units: null, + }, + ], + }, + ], +}; diff --git a/mocks/tokens/tokenHolders.ts b/mocks/tokens/tokenHolders.ts index 582476e903..89f2595482 100644 --- a/mocks/tokens/tokenHolders.ts +++ b/mocks/tokens/tokenHolders.ts @@ -2,18 +2,14 @@ import type { TokenHolders } from 'types/api/token'; import { withName, withoutName } from 'mocks/address/address'; -import { tokenInfoERC1155a, tokenInfoERC20a } from './tokenInfo'; - export const tokenHoldersERC20: TokenHolders = { items: [ { address: withName, - token: tokenInfoERC20a, value: '107014805905725000000', }, { address: withoutName, - token: tokenInfoERC20a, value: '207014805905725000000', }, ], @@ -27,13 +23,11 @@ export const tokenHoldersERC1155: TokenHolders = { items: [ { address: withName, - token: tokenInfoERC1155a, value: '107014805905725000000', token_id: '12345', }, { address: withoutName, - token: tokenInfoERC1155a, value: '207014805905725000000', token_id: '12345', }, diff --git a/mocks/txs/tx.ts b/mocks/txs/tx.ts index 3ca7876b99..3940426433 100644 --- a/mocks/txs/tx.ts +++ b/mocks/txs/tx.ts @@ -341,3 +341,17 @@ export const base4 = { ...base, hash: '0x22d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193', }; + +export const withBlob = { + ...base, + blob_gas_price: '21518435987', + blob_gas_used: '131072', + blob_versioned_hashes: [ + '0x01a8c328b0370068aaaef49c107f70901cd79adcda81e3599a88855532122e09', + '0x0197fdb17195c176b23160f335daabd4b6a231aaaadd73ec567877c66a3affd1', + ], + burnt_blob_fee: '2820464441688064', + max_fee_per_blob_gas: '60000000000', + tx_types: [ 'blob_transaction' as const ], + type: 3, +}; diff --git a/mocks/user/profile.ts b/mocks/user/profile.ts index e5caed3b63..955f872e01 100644 --- a/mocks/user/profile.ts +++ b/mocks/user/profile.ts @@ -4,3 +4,10 @@ export const base = { name: 'tom goriunov', nickname: 'tom2drum', }; + +export const withoutEmail = { + avatar: 'https://avatars.githubusercontent.com/u/22130104', + email: null, + name: 'tom goriunov', + nickname: 'tom2drum', +}; diff --git a/mocks/validators/index.ts b/mocks/validators/index.ts new file mode 100644 index 0000000000..22081cae8c --- /dev/null +++ b/mocks/validators/index.ts @@ -0,0 +1,33 @@ +import type { Validator, ValidatorsCountersResponse, ValidatorsResponse } from 'types/api/validators'; + +import * as addressMock from '../address/address'; + +export const validator1: Validator = { + address: addressMock.withName, + blocks_validated_count: 7334224, + state: 'active', +}; + +export const validator2: Validator = { + address: addressMock.withEns, + blocks_validated_count: 8937453, + state: 'probation', +}; + +export const validator3: Validator = { + address: addressMock.withoutName, + blocks_validated_count: 1234, + state: 'inactive', +}; + +export const validatorsResponse: ValidatorsResponse = { + items: [ validator1, validator2, validator3 ], + next_page_params: null, +}; + +export const validatorsCountersResponse: ValidatorsCountersResponse = { + active_validators_counter: '42', + active_validators_percentage: 7.14, + new_validators_counter_24h: '11', + validators_counter: '140', +}; diff --git a/nextjs/csp/policies/ad.ts b/nextjs/csp/policies/ad.ts index 1e0e769e22..55f23ff913 100644 --- a/nextjs/csp/policies/ad.ts +++ b/nextjs/csp/policies/ad.ts @@ -18,6 +18,11 @@ export function ad(): CspDev.DirectiveDescriptor { // hype 'api.hypelab.com', + '*.ixncdn.com', + + //getit + 'v1.getittech.io', + 'ipapi.co', ], 'frame-src': [ // coinzilla diff --git a/nextjs/csp/policies/app.ts b/nextjs/csp/policies/app.ts index c1b5355516..d1596cb1c5 100644 --- a/nextjs/csp/policies/app.ts +++ b/nextjs/csp/policies/app.ts @@ -31,6 +31,8 @@ const getCspReportUrl = () => { }; export function app(): CspDev.DirectiveDescriptor { + const marketplaceFeaturePayload = getFeaturePayload(config.features.marketplace); + return { 'default-src': [ // KEY_WORDS.NONE, @@ -54,6 +56,7 @@ export function app(): CspDev.DirectiveDescriptor { getFeaturePayload(config.features.verifiedTokens)?.api.endpoint, getFeaturePayload(config.features.addressVerification)?.api.endpoint, getFeaturePayload(config.features.nameService)?.api.endpoint, + marketplaceFeaturePayload && 'api' in marketplaceFeaturePayload ? marketplaceFeaturePayload.api.endpoint : '', // chain RPC server config.chain.rpcUrl, diff --git a/nextjs/getServerSideProps.ts b/nextjs/getServerSideProps.ts index 68a1f7780e..413b19bf29 100644 --- a/nextjs/getServerSideProps.ts +++ b/nextjs/getServerSideProps.ts @@ -49,10 +49,20 @@ export const verifiedAddresses: GetServerSideProps = async(context) => { return account(context); }; +export const deposits: GetServerSideProps = async(context) => { + if (!(rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'shibarium'))) { + return { + notFound: true, + }; + } + + return base(context); +}; + export const withdrawals: GetServerSideProps = async(context) => { if ( !config.features.beaconChain.isEnabled && - !(rollupFeature.isEnabled && rollupFeature.type === 'optimistic') + !(rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'shibarium')) ) { return { notFound: true, @@ -171,3 +181,23 @@ export const userOps: GetServerSideProps = async(context) => { return base(context); }; + +export const validators: GetServerSideProps = async(context) => { + if (!config.features.validators.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const gasTracker: GetServerSideProps = async(context) => { + if (!config.features.gasTracker.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; diff --git a/nextjs/nextjs-routes.d.ts b/nextjs/nextjs-routes.d.ts index 0cc79a3a13..ee87eb5dc1 100644 --- a/nextjs/nextjs-routes.d.ts +++ b/nextjs/nextjs-routes.d.ts @@ -28,11 +28,13 @@ declare module "nextjs-routes" { | StaticRoute<"/auth/unverified-email"> | DynamicRoute<"/batches/[number]", { "number": string }> | StaticRoute<"/batches"> + | DynamicRoute<"/blobs/[hash]", { "hash": string }> | DynamicRoute<"/block/[height_or_hash]", { "height_or_hash": string }> | StaticRoute<"/blocks"> | StaticRoute<"/contract-verification"> | StaticRoute<"/csv-export"> | StaticRoute<"/deposits"> + | StaticRoute<"/gas-tracker"> | StaticRoute<"/graphiql"> | StaticRoute<"/"> | StaticRoute<"/login"> @@ -49,6 +51,7 @@ declare module "nextjs-routes" { | DynamicRoute<"/tx/[hash]", { "hash": string }> | StaticRoute<"/txs"> | DynamicRoute<"/txs/kettle/[hash]", { "hash": string }> + | StaticRoute<"/validators"> | StaticRoute<"/verified-contracts"> | StaticRoute<"/visualize/sol2uml"> | StaticRoute<"/withdrawals">; diff --git a/package.json b/package.json index 4ea9eda640..8eadbb073d 100644 --- a/package.json +++ b/package.json @@ -63,17 +63,19 @@ "chakra-react-select": "^4.4.3", "crypto-js": "^4.2.0", "d3": "^7.6.1", - "dappscout-iframe": "^0.1.0", + "dappscout-iframe": "0.2.0", "dayjs": "^1.11.5", "dom-to-image": "^2.6.0", "focus-visible": "^5.2.0", "framer-motion": "^6.5.1", + "getit-sdk": "^1.0.4", "gradient-avatar": "^1.0.2", "graphiql": "^2.2.0", "graphql": "^16.8.1", "graphql-ws": "^5.11.3", "js-cookie": "^3.0.1", "lodash": "^4.0.0", + "magic-bytes.js": "1.8.0", "mixpanel-browser": "^2.47.0", "monaco-editor": "^0.34.1", "next": "13.5.4", diff --git a/pages/_app.tsx b/pages/_app.tsx index 5086499177..15fc5edd91 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -15,6 +15,7 @@ import { ChakraProvider } from 'lib/contexts/chakra'; import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection'; import { growthBook } from 'lib/growthbook/init'; import useLoadFeatures from 'lib/growthbook/useLoadFeatures'; +import useNotifyOnNavigation from 'lib/hooks/useNotifyOnNavigation'; import { SocketProvider } from 'lib/socket/context'; import theme from 'theme'; import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary'; @@ -44,6 +45,7 @@ const ERROR_SCREEN_STYLES: ChakraProps = { function MyApp({ Component, pageProps }: AppPropsWithLayout) { useLoadFeatures(); + useNotifyOnNavigation(); const queryClient = useQueryClientConfig(); diff --git a/pages/api/media-type.ts b/pages/api/media-type.ts index f64301f094..7489fecdf1 100644 --- a/pages/api/media-type.ts +++ b/pages/api/media-type.ts @@ -22,11 +22,13 @@ export default async function mediaTypeHandler(req: NextApiRequest, res: NextApi return 'video'; } + if (contentType?.startsWith('image')) { + return 'image'; + } + if (contentType?.startsWith('text/html')) { return 'html'; } - - return 'image'; })(); res.status(200).json({ type: mediaType }); } catch (error) { diff --git a/pages/batches/index.tsx b/pages/batches/index.tsx index 79e32ab04d..e14ff7ca49 100644 --- a/pages/batches/index.tsx +++ b/pages/batches/index.tsx @@ -18,6 +18,7 @@ const Batches = dynamic(() => { case 'optimistic': return import('ui/pages/OptimisticL2TxnBatches'); } + throw new Error('Txn batches feature is not enabled.'); }, { ssr: false }); const Page: NextPage = () => { diff --git a/pages/blobs/[hash].tsx b/pages/blobs/[hash].tsx new file mode 100644 index 0000000000..f746e4a82c --- /dev/null +++ b/pages/blobs/[hash].tsx @@ -0,0 +1,20 @@ +import type { NextPage } from 'next'; +import dynamic from 'next/dynamic'; +import React from 'react'; + +import type { Props } from 'nextjs/getServerSideProps'; +import PageNextJs from 'nextjs/PageNextJs'; + +const Blob = dynamic(() => import('ui/pages/Blob'), { ssr: false }); + +const Page: NextPage = (props: Props) => { + return ( + + + + ); +}; + +export default Page; + +export { base as getServerSideProps } from 'nextjs/getServerSideProps'; diff --git a/pages/deposits/index.tsx b/pages/deposits/index.tsx index 9db20dd26d..789dd39657 100644 --- a/pages/deposits/index.tsx +++ b/pages/deposits/index.tsx @@ -4,7 +4,20 @@ import React from 'react'; import PageNextJs from 'nextjs/PageNextJs'; -const Deposits = dynamic(() => import('ui/pages/OptimisticL2Deposits'), { ssr: false }); +import config from 'configs/app'; +const rollupFeature = config.features.rollup; + +const Deposits = dynamic(() => { + if (rollupFeature.isEnabled && rollupFeature.type === 'optimistic') { + return import('ui/pages/OptimisticL2Deposits'); + } + + if (rollupFeature.isEnabled && rollupFeature.type === 'shibarium') { + return import('ui/pages/ShibariumDeposits'); + } + + throw new Error('Withdrawals feature is not enabled.'); +}, { ssr: false }); const Page: NextPage = () => { return ( @@ -16,4 +29,4 @@ const Page: NextPage = () => { export default Page; -export { optimisticRollup as getServerSideProps } from 'nextjs/getServerSideProps'; +export { deposits as getServerSideProps } from 'nextjs/getServerSideProps'; diff --git a/pages/gas-tracker.tsx b/pages/gas-tracker.tsx new file mode 100644 index 0000000000..e407fa5cb5 --- /dev/null +++ b/pages/gas-tracker.tsx @@ -0,0 +1,19 @@ +import type { NextPage } from 'next'; +import dynamic from 'next/dynamic'; +import React from 'react'; + +import PageNextJs from 'nextjs/PageNextJs'; + +const GasTracker = dynamic(() => import('ui/pages/GasTracker'), { ssr: false }); + +const Page: NextPage = () => { + return ( + + + + ); +}; + +export default Page; + +export { gasTracker as getServerSideProps } from 'nextjs/getServerSideProps'; diff --git a/pages/validators.tsx b/pages/validators.tsx new file mode 100644 index 0000000000..42409f3fd3 --- /dev/null +++ b/pages/validators.tsx @@ -0,0 +1,19 @@ +import type { NextPage } from 'next'; +import dynamic from 'next/dynamic'; +import React from 'react'; + +import PageNextJs from 'nextjs/PageNextJs'; + +const Validators = dynamic(() => import('ui/pages/Validators'), { ssr: false }); + +const Page: NextPage = () => { + return ( + + + + ); +}; + +export default Page; + +export { validators as getServerSideProps } from 'nextjs/getServerSideProps'; diff --git a/pages/withdrawals/index.tsx b/pages/withdrawals/index.tsx index 06e29f83d0..661ada21eb 100644 --- a/pages/withdrawals/index.tsx +++ b/pages/withdrawals/index.tsx @@ -12,6 +12,11 @@ const Withdrawals = dynamic(() => { if (rollupFeature.isEnabled && rollupFeature.type === 'optimistic') { return import('ui/pages/OptimisticL2Withdrawals'); } + + if (rollupFeature.isEnabled && rollupFeature.type === 'shibarium') { + return import('ui/pages/ShibariumWithdrawals'); + } + if (beaconChainFeature.isEnabled) { return import('ui/pages/BeaconChainWithdrawals'); } diff --git a/playwright-ct.config.ts b/playwright-ct.config.ts index 3ca605249d..eeb35835bf 100644 --- a/playwright-ct.config.ts +++ b/playwright-ct.config.ts @@ -26,8 +26,10 @@ const config: PlaywrightTestConfig = defineConfig({ /* Retry on CI only */ retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + /* Opt out of parallel tests. */ + // on non-performant local machines some tests may fail due to lack of resources + // so we opt out of parallel tests in any environment + workers: 1, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'html', diff --git a/playwright/TestApp.tsx b/playwright/TestApp.tsx index b4b246a621..4b3b01e331 100644 --- a/playwright/TestApp.tsx +++ b/playwright/TestApp.tsx @@ -41,6 +41,7 @@ const WALLET_CONNECT_PROJECT_ID = 'PROJECT_ID'; const wagmiConfig = defaultWagmiConfig({ chains, projectId: WALLET_CONNECT_PROJECT_ID, + enableEmail: true, }); createWeb3Modal({ diff --git a/playwright/index.ts b/playwright/index.ts index 879cfa0127..e0e5b4be99 100644 --- a/playwright/index.ts +++ b/playwright/index.ts @@ -9,6 +9,7 @@ const NEXT_ROUTER_MOCK = { query: {}, pathname: '', push: () => Promise.resolve(), + replace: () => Promise.resolve(), }; beforeMount(async({ hooksConfig }) => { diff --git a/playwright/utils/configs.ts b/playwright/utils/configs.ts index 3efc8e7a8a..c167a76fed 100644 --- a/playwright/utils/configs.ts +++ b/playwright/utils/configs.ts @@ -3,6 +3,7 @@ import { devices } from '@playwright/test'; export const viewport = { mobile: devices['iPhone 13 Pro'].viewport, + md: { width: 1001, height: 800 }, xl: { width: 1600, height: 1000 }, }; @@ -19,6 +20,10 @@ export const featureEnvs = { { name: 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL', value: 'https://localhost:3101' }, { name: 'NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL', value: 'https://localhost:3102' }, ], + shibariumRollup: [ + { name: 'NEXT_PUBLIC_ROLLUP_TYPE', value: 'shibarium' }, + { name: 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL', value: 'https://localhost:3101' }, + ], bridgedTokens: [ { name: 'NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS', @@ -39,6 +44,9 @@ export const featureEnvs = { userOps: [ { name: 'NEXT_PUBLIC_HAS_USER_OPS', value: 'true' }, ], + validators: [ + { name: 'NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE', value: 'stability' }, + ], }; export const viewsEnvs = { @@ -49,6 +57,12 @@ export const viewsEnvs = { }, }; +export const UIEnvs = { + hasContractAuditReports: [ + { name: 'NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS', value: 'true' }, + ], +}; + export const stabilityEnvs = [ { name: 'NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS', value: '["top_accounts"]' }, { name: 'NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS', value: '["value","fee_currency","gas_price","gas_fees","burnt_fees"]' }, diff --git a/public/icons/name.d.ts b/public/icons/name.d.ts index 1376d0422d..efda3f38ad 100644 --- a/public/icons/name.d.ts +++ b/public/icons/name.d.ts @@ -11,9 +11,14 @@ | "arrows/north-east" | "arrows/south-east" | "arrows/up-down" + | "blob" + | "blobs/image" + | "blobs/raw" + | "blobs/text" | "block_slim" | "block" | "brands/safe" + | "brands/solidity_scan" | "burger" | "check" | "clock-light" @@ -49,6 +54,7 @@ | "filter" | "finalized" | "flame" + | "gas_xl" | "gas" | "gear" | "globe-b" @@ -82,6 +88,7 @@ | "qr_code" | "repeat_arrow" | "restAPI" + | "rocket_xl" | "rocket" | "RPC" | "scope" @@ -128,8 +135,10 @@ | "txn_batches" | "unfinalized" | "uniswap" + | "up" | "user_op_slim" | "user_op" + | "validator" | "verified_token" | "verified" | "verify-contract" diff --git a/public/static/contract_star.png b/public/static/contract_star.png new file mode 100644 index 0000000000..32d635ef60 Binary files /dev/null and b/public/static/contract_star.png differ diff --git a/stubs/blobs.ts b/stubs/blobs.ts new file mode 100644 index 0000000000..89ca5c4cb4 --- /dev/null +++ b/stubs/blobs.ts @@ -0,0 +1,20 @@ +import type { Blob, TxBlob } from 'types/api/blobs'; + +import { TX_HASH } from './tx'; + +const BLOB_HASH = '0x0137cd898a9aaa92bbe94999d2a98241f5eabc829d9354160061789963f85995'; +const BLOB_PROOF = '0x82683d5d6e58a76f2a607b8712cad113621d46cb86a6bcfcffb1e274a70c7308b3243c6075ee22d904fecf8d4c147c6f'; + +export const TX_BLOB: TxBlob = { + blob_data: '0x010203040506070809101112', + hash: BLOB_HASH, + kzg_commitment: BLOB_PROOF, + kzg_proof: BLOB_PROOF, +}; + +export const BLOB: Blob = { + ...TX_BLOB, + transaction_hashes: [ + { block_consensus: true, transaction_hash: TX_HASH }, + ], +}; diff --git a/stubs/contract.ts b/stubs/contract.ts index 36d7b13ed2..814786936f 100644 --- a/stubs/contract.ts +++ b/stubs/contract.ts @@ -1,5 +1,5 @@ import type { SmartContract, SolidityscanReport } from 'types/api/contract'; -import type { VerifiedContract } from 'types/api/contracts'; +import type { VerifiedContract, VerifiedContractsCounters } from 'types/api/contracts'; import { ADDRESS_PARAMS } from './addressParams'; @@ -40,6 +40,7 @@ export const CONTRACT_CODE_VERIFIED = { optimization_runs: 200, source_code: 'source_code', verified_at: '2023-02-21T14:39:16.906760Z', + license_type: 'mit', } as unknown as SmartContract; export const VERIFIED_CONTRACT_INFO: VerifiedContract = { @@ -52,6 +53,14 @@ export const VERIFIED_CONTRACT_INFO: VerifiedContract = { optimization_enabled: false, tx_count: 565058, verified_at: '2023-04-10T13:16:33.884921Z', + license_type: 'mit', +}; + +export const VERIFIED_CONTRACTS_COUNTERS: VerifiedContractsCounters = { + smart_contracts: '123456789', + new_smart_contracts_24h: '12345', + verified_smart_contracts: '654321', + new_verified_smart_contracts_24h: '1234', }; export const SOLIDITYSCAN_REPORT: SolidityscanReport = { diff --git a/stubs/shibarium.ts b/stubs/shibarium.ts new file mode 100644 index 0000000000..3003be7110 --- /dev/null +++ b/stubs/shibarium.ts @@ -0,0 +1,20 @@ +import type { ShibariumDepositsItem, ShibariumWithdrawalsItem } from 'types/api/shibarium'; + +import { ADDRESS_PARAMS } from './addressParams'; +import { TX_HASH } from './tx'; + +export const SHIBARIUM_DEPOSIT_ITEM: ShibariumDepositsItem = { + l1_block_number: 9045233, + l1_transaction_hash: TX_HASH, + l2_transaction_hash: TX_HASH, + timestamp: '2023-05-22T18:00:36.000000Z', + user: ADDRESS_PARAMS, +}; + +export const SHIBARIUM_WITHDRAWAL_ITEM: ShibariumWithdrawalsItem = { + l2_block_number: 9045233, + l1_transaction_hash: TX_HASH, + l2_transaction_hash: TX_HASH, + timestamp: '2023-05-22T18:00:36.000000Z', + user: ADDRESS_PARAMS, +}; diff --git a/stubs/stats.ts b/stubs/stats.ts index 3a2bba67cb..d21ba588fd 100644 --- a/stubs/stats.ts +++ b/stubs/stats.ts @@ -9,16 +9,22 @@ export const HOMEPAGE_STATS: HomeStats = { fiat_price: '1.01', price: 20.41, time: 12283, + base_fee: 2.22222, + priority_fee: 12.424242, }, fast: { fiat_price: '1.26', price: 25.47, time: 9321, + base_fee: 4.44444, + priority_fee: 22.242424, }, slow: { fiat_price: '0.97', price: 19.55, time: 24543, + base_fee: 1.11111, + priority_fee: 7.8909, }, }, gas_price_updated_at: '2022-11-11T11:09:49.051171Z', diff --git a/stubs/token.ts b/stubs/token.ts index e60e004bad..23ab4519a6 100644 --- a/stubs/token.ts +++ b/stubs/token.ts @@ -38,13 +38,11 @@ export const TOKEN_COUNTERS: TokenCounters = { export const TOKEN_HOLDER_ERC_20: TokenHolder = { address: ADDRESS_PARAMS, - token: TOKEN_INFO_ERC_20, value: '1021378038331138520', }; export const TOKEN_HOLDER_ERC_1155: TokenHolder = { address: ADDRESS_PARAMS, - token: TOKEN_INFO_ERC_1155, token_id: '12345', value: '1021378038331138520', }; diff --git a/stubs/validators.ts b/stubs/validators.ts new file mode 100644 index 0000000000..1717a59ec0 --- /dev/null +++ b/stubs/validators.ts @@ -0,0 +1,16 @@ +import type { Validator, ValidatorsCountersResponse } from 'types/api/validators'; + +import { ADDRESS_PARAMS } from './addressParams'; + +export const VALIDATOR: Validator = { + address: ADDRESS_PARAMS, + blocks_validated_count: 25987, + state: 'active', +}; + +export const VALIDATORS_COUNTERS: ValidatorsCountersResponse = { + active_validators_counter: '42', + active_validators_percentage: 7.14, + new_validators_counter_24h: '11', + validators_counter: '140', +}; diff --git a/theme/components/FormLabel.ts b/theme/components/FormLabel.ts index 03f86537d6..c95f0e8e7a 100644 --- a/theme/components/FormLabel.ts +++ b/theme/components/FormLabel.ts @@ -1,5 +1,5 @@ import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system'; -import { getColor, mode } from '@chakra-ui/theme-tools'; +import { getColor } from '@chakra-ui/theme-tools'; import getDefaultFormColors from '../utils/getDefaultFormColors'; @@ -20,7 +20,7 @@ const baseStyle = defineStyle({ const variantFloating = defineStyle((props) => { const { theme, backgroundColor } = props; const { focusPlaceholderColor } = getDefaultFormColors(props); - const bc = backgroundColor || mode('white', 'black')(props); + const bc = backgroundColor || 'transparent'; return { left: '2px', diff --git a/theme/global.ts b/theme/global.ts index 65380d01fb..95242c622f 100644 --- a/theme/global.ts +++ b/theme/global.ts @@ -2,6 +2,7 @@ import type { StyleFunctionProps } from '@chakra-ui/theme-tools'; import { mode } from '@chakra-ui/theme-tools'; import scrollbar from './foundations/scrollbar'; +import addressEntity from './globals/address-entity'; import getDefaultTransitionProps from './utils/getDefaultTransitionProps'; const global = (props: StyleFunctionProps) => ({ @@ -23,6 +24,7 @@ const global = (props: StyleFunctionProps) => ({ w: '100%', }, ...scrollbar(props), + ...addressEntity(props), }); export default global; diff --git a/theme/globals/address-entity.ts b/theme/globals/address-entity.ts new file mode 100644 index 0000000000..25641c3020 --- /dev/null +++ b/theme/globals/address-entity.ts @@ -0,0 +1,37 @@ +import { mode } from '@chakra-ui/theme-tools'; +import type { StyleFunctionProps } from '@chakra-ui/theme-tools'; + +const styles = (props: StyleFunctionProps) => { + return { + '.address-entity': { + '&.address-entity_highlighted': { + _before: { + content: `" "`, + position: 'absolute', + py: 1, + pl: 1, + pr: 0, + top: '-5px', + left: '-5px', + width: `100%`, + height: '100%', + borderRadius: 'base', + borderColor: mode('blue.200', 'blue.600')(props), + borderWidth: '1px', + borderStyle: 'dashed', + bgColor: mode('blue.50', 'blue.900')(props), + zIndex: -1, + }, + }, + }, + '.address-entity_no-copy': { + '&.address-entity_highlighted': { + _before: { + pr: 2, + }, + }, + }, + }; +}; + +export default styles; diff --git a/theme/utils/getOutlinedFieldStyles.ts b/theme/utils/getOutlinedFieldStyles.ts index e5c6f554d3..a643b55597 100644 --- a/theme/utils/getOutlinedFieldStyles.ts +++ b/theme/utils/getOutlinedFieldStyles.ts @@ -49,6 +49,13 @@ export default function getOutlinedFieldStyles(props: StyleFunctionProps) { }, // not filled input ':placeholder-shown:not(:focus-visible):not(:hover):not([aria-invalid=true])': { borderColor: borderColor || mode('gray.100', 'gray.700')(props) }, + + // not filled input with type="date" + ':not(:placeholder-shown)[value=""]:not(:focus-visible):not(:hover):not([aria-invalid=true])': { + borderColor: borderColor || mode('gray.100', 'gray.700')(props), + color: 'gray.500', + }, + ':-webkit-autofill': { transition: 'background-color 5000s ease-in-out 0s' }, ':-webkit-autofill:hover': { transition: 'background-color 5000s ease-in-out 0s' }, ':-webkit-autofill:focus': { transition: 'background-color 5000s ease-in-out 0s' }, diff --git a/types/api/blobs.ts b/types/api/blobs.ts new file mode 100644 index 0000000000..7b8abb55fd --- /dev/null +++ b/types/api/blobs.ts @@ -0,0 +1,18 @@ +export interface TxBlob { + hash: string; + blob_data: string | null; + kzg_commitment: string | null; + kzg_proof: string | null; +} + +export type TxBlobs = { + items: Array; + next_page_params: null; +}; + +export interface Blob extends TxBlob { + transaction_hashes: Array<{ + block_consensus: boolean; + transaction_hash: string; + }>; +} diff --git a/types/api/block.ts b/types/api/block.ts index 3d72ccbbe9..f495b4a0d8 100644 --- a/types/api/block.ts +++ b/types/api/block.ts @@ -36,6 +36,12 @@ export interface Block { bitcoin_merged_mining_merkle_proof?: string | null; hash_for_merged_mining?: string | null; minimum_gas_price?: string | null; + // BLOB FIELDS + blob_gas_price?: string; + blob_gas_used?: string; + burnt_blob_fees?: string; + excess_blob_gas?: string; + blob_tx_count?: number; // HEMI FIELDS btc_finality?: number; } diff --git a/types/api/charts.ts b/types/api/charts.ts index 5414f68e60..5d74b38410 100644 --- a/types/api/charts.ts +++ b/types/api/charts.ts @@ -1,12 +1,12 @@ export interface ChartTransactionItem { date: string; - tx_count: number; + tx_count: number | null; } export interface ChartMarketItem { date: string; - closing_price: string; - market_cap?: string; + closing_price: string | null; + market_cap?: string | null; tvl?: string | null; } diff --git a/types/api/contract.ts b/types/api/contract.ts index dd66d0380f..b48229f9eb 100644 --- a/types/api/contract.ts +++ b/types/api/contract.ts @@ -3,6 +3,22 @@ import type { Abi, AbiType } from 'abitype'; export type SmartContractMethodArgType = AbiType; export type SmartContractMethodStateMutability = 'view' | 'nonpayable' | 'payable'; +export type SmartContractLicenseType = +'none' | +'unlicense' | +'mit' | +'gnu_gpl_v2' | +'gnu_gpl_v3' | +'gnu_lgpl_v2_1' | +'gnu_lgpl_v3' | +'bsd_2_clause' | +'bsd_3_clause' | +'mpl_2_0' | +'osl_3_0' | +'apache_2_0' | +'gnu_agpl_v3' | +'bsl_1_1'; + export interface SmartContract { deployed_bytecode: string | null; creation_bytecode: string | null; @@ -37,6 +53,7 @@ export interface SmartContract { verified_twin_address_hash: string | null; minimal_proxy_address_hash: string | null; language: string | null; + license_type: SmartContractLicenseType | null; } export type SmartContractDecodedConstructorArg = [ @@ -55,19 +72,18 @@ export interface SmartContractExternalLibrary { export interface SmartContractMethodBase { inputs: Array; - outputs: Array; + outputs?: Array; constant: boolean; name: string; stateMutability: SmartContractMethodStateMutability; type: 'function'; payable: boolean; error?: string; -} - -export interface SmartContractReadMethod extends SmartContractMethodBase { method_id: string; } +export type SmartContractReadMethod = SmartContractMethodBase; + export interface SmartContractWriteFallback { payable?: true; stateMutability: 'payable'; @@ -85,7 +101,7 @@ export type SmartContractWriteMethod = SmartContractMethodBase | SmartContractWr export type SmartContractMethod = SmartContractReadMethod | SmartContractWriteMethod; export interface SmartContractMethodInput { - internalType?: SmartContractMethodArgType; + internalType?: string; // there could be any string, e.g "enum MyEnum" name: string; type: SmartContractMethodArgType; components?: Array; @@ -137,6 +153,7 @@ export interface SmartContractVerificationConfigRaw { vyper_compiler_versions: Array; vyper_evm_versions: Array; is_rust_verifier_microservice_enabled: boolean; + license_types: Record; } export interface SmartContractVerificationConfig extends SmartContractVerificationConfigRaw { @@ -180,3 +197,26 @@ export type SolidityscanReport = { scanner_reference_url: string; }; } + +type SmartContractSecurityAudit = { + audit_company_name: string; + audit_publish_date: string; + audit_report_url: string; +} + +export type SmartContractSecurityAudits = { + items: Array; +} + +export type SmartContractSecurityAuditSubmission = { + 'address_hash': string; + 'submitter_name': string; + 'submitter_email': string; + 'is_project_owner': boolean; + 'project_name': string; + 'project_url': string; + 'audit_company_name': string; + 'audit_report_url': string; + 'audit_publish_date': string; + 'comment'?: string; +} diff --git a/types/api/contracts.ts b/types/api/contracts.ts index e075f038f0..65fe537568 100644 --- a/types/api/contracts.ts +++ b/types/api/contracts.ts @@ -1,4 +1,5 @@ import type { AddressParam } from './addressParams'; +import type { SmartContractLicenseType } from './contract'; export interface VerifiedContract { address: AddressParam; @@ -10,6 +11,7 @@ export interface VerifiedContract { tx_count: number | null; verified_at: string; market_cap: string | null; + license_type: SmartContractLicenseType | null; } export interface VerifiedContractsResponse { diff --git a/types/api/search.ts b/types/api/search.ts index 49b1e87ec3..da8194eef5 100644 --- a/types/api/search.ts +++ b/types/api/search.ts @@ -55,6 +55,12 @@ export interface SearchResultTx { url?: string; // not used by the frontend, we build the url ourselves } +export interface SearchResultBlob { + type: 'blob'; + blob_hash: string; + timestamp: null; +} + export interface SearchResultUserOp { type: 'user_operation'; user_operation_hash: string; @@ -62,7 +68,8 @@ export interface SearchResultUserOp { url?: string; // not used by the frontend, we build the url ourselves } -export type SearchResultItem = SearchResultToken | SearchResultAddressOrContract | SearchResultBlock | SearchResultTx | SearchResultLabel | SearchResultUserOp; +export type SearchResultItem = SearchResultToken | SearchResultAddressOrContract | SearchResultBlock | SearchResultTx | SearchResultLabel | SearchResultUserOp | +SearchResultBlob; export interface SearchResult { items: Array; @@ -86,5 +93,5 @@ export interface SearchResultFilters { export interface SearchRedirectResult { parameter: string | null; redirect: boolean; - type: 'address' | 'block' | 'transaction' | 'user_operation' | null; + type: 'address' | 'block' | 'transaction' | 'user_operation' | 'blob' | null; } diff --git a/types/api/shibarium.ts b/types/api/shibarium.ts new file mode 100644 index 0000000000..e23d9cf811 --- /dev/null +++ b/types/api/shibarium.ts @@ -0,0 +1,33 @@ +import type { AddressParam } from './addressParams'; + +export type ShibariumDepositsItem = { + l1_block_number: number; + l1_transaction_hash: string; + l2_transaction_hash: string; + timestamp: string; + user: AddressParam | string; +} + +export type ShibariumDepositsResponse = { + items: Array; + next_page_params: { + items_count: number; + block_number: number; + }; +} + +export type ShibariumWithdrawalsItem = { + l1_transaction_hash: string; + l2_block_number: number; + l2_transaction_hash: string; + timestamp: string; + user: AddressParam | string; +} + +export type ShibariumWithdrawalsResponse = { + items: Array; + next_page_params: { + items_count: number; + block_number: number; + }; +} diff --git a/types/api/stats.ts b/types/api/stats.ts index e4a32598ba..40fe7d34e2 100644 --- a/types/api/stats.ts +++ b/types/api/stats.ts @@ -28,6 +28,8 @@ export interface GasPriceInfo { fiat_price: string | null; price: number | null; time: number | null; + base_fee: number | null; + priority_fee: number | null; } export type Counters = { diff --git a/types/api/token.ts b/types/api/token.ts index 417c74aab6..6edb4c60ec 100644 --- a/types/api/token.ts +++ b/types/api/token.ts @@ -39,12 +39,9 @@ export type TokenHolderBase = { value: string; } -export type TokenHolderERC20ERC721 = TokenHolderBase & { - token: TokenInfo<'ERC-20'> | TokenInfo<'ERC-721'>; -} +export type TokenHolderERC20ERC721 = TokenHolderBase export type TokenHolderERC1155 = TokenHolderBase & { - token: TokenInfo<'ERC-1155'>; token_id: string; } diff --git a/types/api/transaction.ts b/types/api/transaction.ts index c980a15929..6ed72d7930 100644 --- a/types/api/transaction.ts +++ b/types/api/transaction.ts @@ -79,6 +79,12 @@ export type Transaction = { zkevm_batch_number?: number; zkevm_status?: typeof ZKEVM_L2_TX_STATUSES[number]; zkevm_sequence_hash?: string; + // blob tx fields + blob_versioned_hashes?: Array; + blob_gas_used?: string; + blob_gas_price?: string; + burnt_blob_fee?: string; + max_fee_per_blob_gas?: string; // Hemi fields btc_finality?: number; } @@ -106,6 +112,15 @@ export interface TransactionsResponsePending { } | null; } +export interface TransactionsResponseWithBlobs { + items: Array; + next_page_params: { + block_number: number; + index: number; + items_count: number; + } | null; +} + export interface TransactionsResponseWatchlist { items: Array; next_page_params: { @@ -121,7 +136,8 @@ export type TransactionType = 'rootstock_remasc' | 'contract_creation' | 'contract_call' | 'token_creation' | -'coin_transfer' +'coin_transfer' | +'blob_transaction' export type TxsResponse = TransactionsResponseValidated | TransactionsResponsePending | BlockTransactionsResponse; diff --git a/types/api/txInterpretation.ts b/types/api/txInterpretation.ts index a34e7947a4..87ef1d0dae 100644 --- a/types/api/txInterpretation.ts +++ b/types/api/txInterpretation.ts @@ -18,9 +18,10 @@ export type TxInterpretationVariable = TxInterpretationVariableTimestamp | TxInterpretationVariableToken | TxInterpretationVariableAddress | - TxInterpretationVariableDomain; + TxInterpretationVariableDomain | + TxInterpretationVariableMethod; -export type TxInterpretationVariableType = 'string' | 'currency' | 'timestamp' | 'token' | 'address' | 'domain'; +export type TxInterpretationVariableType = 'string' | 'currency' | 'timestamp' | 'token' | 'address' | 'domain' | 'method'; export type TxInterpretationVariableString = { type: 'string'; @@ -51,3 +52,8 @@ export type TxInterpretationVariableDomain = { type: 'domain'; value: string; } + +export type TxInterpretationVariableMethod = { + type: 'method'; + value: string; +} diff --git a/types/api/txsFilters.ts b/types/api/txsFilters.ts index e34cef83f8..17347c9cdc 100644 --- a/types/api/txsFilters.ts +++ b/types/api/txsFilters.ts @@ -4,6 +4,10 @@ export type TTxsFilters = { method?: Array; } -export type TypeFilter = 'token_transfer' | 'contract_creation' | 'contract_call' | 'coin_transfer' | 'token_creation'; +export type TTxsWithBlobsFilters = { + type: 'blob_transaction'; +} + +export type TypeFilter = 'token_transfer' | 'contract_creation' | 'contract_call' | 'coin_transfer' | 'token_creation' | 'blob_transaction'; export type MethodFilter = 'approve' | 'transfer' | 'multicall' | 'mint' | 'commit'; diff --git a/types/api/userOps.ts b/types/api/userOps.ts index 86ea183a42..7223f7f68e 100644 --- a/types/api/userOps.ts +++ b/types/api/userOps.ts @@ -49,8 +49,10 @@ export type UserOp = { user_logs_start_index: number; user_logs_count: number; raw: { + account_gas_limits?: string; call_data: string; call_gas_limit: string; + gas_fees?: string; init_code: string; max_fee_per_gas: string; max_priority_fee_per_gas: string; diff --git a/types/api/validators.ts b/types/api/validators.ts new file mode 100644 index 0000000000..ad6e33de96 --- /dev/null +++ b/types/api/validators.ts @@ -0,0 +1,38 @@ +import type { AddressParam } from './addressParams'; + +export interface Validator { + address: AddressParam; + blocks_validated_count: number; + state: 'active' | 'probation' | 'inactive'; +} + +export interface ValidatorsResponse { + items: Array; + next_page_params: { + 'address_hash': string; + 'blocks_validated': string; + 'items_count': string; + 'state': Validator['state']; + } | null; +} + +export interface ValidatorsCountersResponse { + active_validators_counter: string; + active_validators_percentage: number; + new_validators_counter_24h: string; + validators_counter: string; +} + +export interface ValidatorsFilters { + // address_hash: string | undefined; // right now API doesn't support filtering by address_hash + state_filter: Validator['state'] | undefined; +} + +export interface ValidatorsSorting { + sort: 'state' | 'blocks_validated'; + order: 'asc' | 'desc'; +} + +export type ValidatorsSortingField = ValidatorsSorting['sort']; + +export type ValidatorsSortingValue = `${ ValidatorsSortingField }-${ ValidatorsSorting['order'] }`; diff --git a/types/api/zkEvmL2.ts b/types/api/zkEvmL2.ts index 3a82b375ae..df80185cf6 100644 --- a/types/api/zkEvmL2.ts +++ b/types/api/zkEvmL2.ts @@ -5,7 +5,7 @@ export type ZkEvmL2TxnBatchesItem = { verify_tx_hash: string | null; sequence_tx_hash: string | null; status: string; - timestamp: string; + timestamp: string | null; tx_count: number; } @@ -26,7 +26,7 @@ export type ZkEvmL2TxnBatch = { sequence_tx_hash: string; state_root: string; status: typeof ZKEVM_L2_TX_BATCH_STATUSES[number]; - timestamp: string; + timestamp: string | null; transactions: Array; verify_tx_hash: string; } diff --git a/types/client/adProviders.ts b/types/client/adProviders.ts index d972932190..0f81cee1f0 100644 --- a/types/client/adProviders.ts +++ b/types/client/adProviders.ts @@ -1,6 +1,6 @@ import type { ArrayElement } from 'types/utils'; -export const SUPPORTED_AD_BANNER_PROVIDERS = [ 'slise', 'adbutler', 'coinzilla', 'hype', 'none' ] as const; +export const SUPPORTED_AD_BANNER_PROVIDERS = [ 'slise', 'adbutler', 'coinzilla', 'hype', 'getit', 'none' ] as const; export type AdBannerProviders = ArrayElement; export const SUPPORTED_AD_TEXT_PROVIDERS = [ 'coinzilla', 'none' ] as const; diff --git a/types/client/contract.ts b/types/client/contract.ts index df049315a1..63116a4072 100644 --- a/types/client/contract.ts +++ b/types/client/contract.ts @@ -1,5 +1,14 @@ +import type { SmartContractLicenseType } from 'types/api/contract'; + export interface ContractCodeIde { title: string; url: string; icon_url: string; } + +export interface ContractLicense { + type: SmartContractLicenseType; + url: string; + label: string; + title: string; +} diff --git a/types/client/gasTracker.ts b/types/client/gasTracker.ts new file mode 100644 index 0000000000..f998202e95 --- /dev/null +++ b/types/client/gasTracker.ts @@ -0,0 +1,6 @@ +export const GAS_UNITS = [ + 'usd', + 'gwei', +] as const; + +export type GasUnit = typeof GAS_UNITS[number]; diff --git a/types/client/marketplace.ts b/types/client/marketplace.ts index eb132aac20..58ede5b258 100644 --- a/types/client/marketplace.ts +++ b/types/client/marketplace.ts @@ -11,13 +11,17 @@ export type MarketplaceAppPreview = { priority?: number; } -export type MarketplaceAppOverview = MarketplaceAppPreview & { +export type MarketplaceAppSocialInfo = { + twitter?: string; + telegram?: string; + github?: string | Array; + discord?: string; +} + +export type MarketplaceAppOverview = MarketplaceAppPreview & MarketplaceAppSocialInfo & { author: string; description: string; site?: string; - twitter?: string; - telegram?: string; - github?: string; } export enum MarketplaceCategory { diff --git a/types/client/rollup.ts b/types/client/rollup.ts index f1ad67c234..2c62078efc 100644 --- a/types/client/rollup.ts +++ b/types/client/rollup.ts @@ -2,6 +2,7 @@ import type { ArrayElement } from 'types/utils'; export const ROLLUP_TYPES = [ 'optimistic', + 'shibarium', 'zkEvm', ] as const; diff --git a/types/client/validators.ts b/types/client/validators.ts new file mode 100644 index 0000000000..9fdc949d5d --- /dev/null +++ b/types/client/validators.ts @@ -0,0 +1,7 @@ +import type { ArrayElement } from 'types/utils'; + +export const VALIDATORS_CHAIN_TYPE = [ + 'stability', +] as const; + +export type ValidatorsChainType = ArrayElement; diff --git a/types/views/tx.ts b/types/views/tx.ts index 21800d80e0..dc6a750d27 100644 --- a/types/views/tx.ts +++ b/types/views/tx.ts @@ -16,3 +16,9 @@ export const TX_ADDITIONAL_FIELDS_IDS = [ ] as const; export type TxAdditionalFieldsId = ArrayElement; + +export const TX_VIEWS_IDS = [ + 'blob_txs', +] as const; + +export type TxViewId = ArrayElement; diff --git a/ui/address/AddressDetails.tsx b/ui/address/AddressDetails.tsx index 1554ee726f..662fd86a70 100644 --- a/ui/address/AddressDetails.tsx +++ b/ui/address/AddressDetails.tsx @@ -6,6 +6,7 @@ import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; import getQueryParamString from 'lib/router/getQueryParamString'; import AddressCounterItem from 'ui/address/details/AddressCounterItem'; import ServiceDegradationWarning from 'ui/shared/alerts/ServiceDegradationWarning'; +import isCustomAppError from 'ui/shared/AppError/isCustomAppError'; import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem'; @@ -59,15 +60,16 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { has_validated_blocks: false, }), [ addressHash ]); - const is404Error = addressQuery.isError && 'status' in addressQuery.error && addressQuery.error.status === 404; - const is422Error = addressQuery.isError && 'status' in addressQuery.error && addressQuery.error.status === 422; - - if (addressQuery.isError && is422Error) { - throwOnResourceLoadError(addressQuery); - } - - if (addressQuery.isError && !is404Error) { - return ; + // error handling (except 404 codes) + if (addressQuery.isError) { + if (isCustomAppError(addressQuery.error)) { + const is404Error = addressQuery.isError && 'status' in addressQuery.error && addressQuery.error.status === 404; + if (!is404Error) { + throwOnResourceLoadError(addressQuery); + } + } else { + return ; + } } const data = addressQuery.isError ? error404Data : addressQuery.data; diff --git a/ui/address/AddressTxs.tsx b/ui/address/AddressTxs.tsx index cc21c355df..d272982c71 100644 --- a/ui/address/AddressTxs.tsx +++ b/ui/address/AddressTxs.tsx @@ -20,6 +20,7 @@ import Pagination from 'ui/shared/pagination/Pagination'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue'; import getSortValueFromQuery from 'ui/shared/sort/getSortValueFromQuery'; +import { sortTxsFromSocket } from 'ui/txs/sortTxs'; import TxsWithAPISorting from 'ui/txs/TxsWithAPISorting'; import { SORT_OPTIONS } from 'ui/txs/useTxsSort'; @@ -85,7 +86,7 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => { addressTxsQuery.onFilterChange({ filter: newVal }); }, [ addressTxsQuery ]); - const handleNewSocketMessage: SocketMessage.AddressTxs['handler'] = (payload) => { + const handleNewSocketMessage: SocketMessage.AddressTxs['handler'] = React.useCallback((payload) => { setSocketAlert(''); queryClient.setQueryData( @@ -123,10 +124,10 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => { items: [ ...newItems, ...prevData.items, - ], + ].sort(sortTxsFromSocket(sort)), }; }); - }; + }, [ currentAddress, filterValue, overloadCount, queryClient, sort ]); const handleSocketClose = React.useCallback(() => { setSocketAlert('Connection is lost. Please refresh the page to load new transactions.'); diff --git a/ui/address/SolidityscanReport.tsx b/ui/address/SolidityscanReport.tsx index d7bbdc9ca2..75bf7ac7f7 100644 --- a/ui/address/SolidityscanReport.tsx +++ b/ui/address/SolidityscanReport.tsx @@ -13,18 +13,23 @@ import { Skeleton, Center, useColorModeValue, + Icon, } from '@chakra-ui/react'; import React from 'react'; -import { SolidityscanReport } from 'types/api/contract'; +import type { SolidityscanReport as TSolidityscanReport } from 'types/api/contract'; +// 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 solidityScanIcon from 'icons/brands/solidity_scan.svg'; import useApiQuery from 'lib/api/useApiQuery'; import { SOLIDITYSCAN_REPORT } from 'stubs/contract'; import IconSvg from 'ui/shared/IconSvg'; import LinkExternal from 'ui/shared/LinkExternal'; type DistributionItem = { - id: keyof SolidityscanReport['scan_report']['scan_summary']['issue_severity_distribution']; + id: keyof TSolidityscanReport['scan_report']['scan_summary']['issue_severity_distribution']; name: string; color: string; } @@ -45,7 +50,7 @@ interface Props { type ItemProps = { item: DistributionItem; - vulnerabilities: SolidityscanReport['scan_report']['scan_summary']['issue_severity_distribution']; + vulnerabilities: TSolidityscanReport['scan_report']['scan_summary']['issue_severity_distribution']; vulnerabilitiesCount: number; } @@ -133,7 +138,11 @@ const SolidityscanReport = ({ className, hash }: Props) => { - Contract analyzed for 140+ vulnerability patterns by SolidityScan + + Contract analyzed for 140+ vulnerability patterns by + + SolidityScan + { Gas used { BigNumber(props.gas_used || 0).toFormat() } - + { props.gas_used && props.gas_used !== '0' && ( + + ) } { !config.UI.views.block.hiddenFields?.total_reward && ( diff --git a/ui/address/blocksValidated/AddressBlocksValidatedTableItem.tsx b/ui/address/blocksValidated/AddressBlocksValidatedTableItem.tsx index b2ca7b4e79..d6e68dfded 100644 --- a/ui/address/blocksValidated/AddressBlocksValidatedTableItem.tsx +++ b/ui/address/blocksValidated/AddressBlocksValidatedTableItem.tsx @@ -46,11 +46,13 @@ const AddressBlocksValidatedTableItem = (props: Props) => { { BigNumber(props.gas_used || 0).toFormat() } - + { props.gas_used && props.gas_used !== '0' && ( + + ) } { !config.UI.views.block.hiddenFields?.total_reward && ( diff --git a/ui/address/contract/ContractCode.pw.tsx b/ui/address/contract/ContractCode.pw.tsx index 5358af0c40..cbd660a941 100644 --- a/ui/address/contract/ContractCode.pw.tsx +++ b/ui/address/contract/ContractCode.pw.tsx @@ -2,16 +2,20 @@ import { test as base, expect } from '@playwright/experimental-ct-react'; import React from 'react'; import * as addressMock from 'mocks/address/address'; +import { contractAudits } from 'mocks/contract/audits'; import * as contractMock from 'mocks/contract/info'; +import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; import * as socketServer from 'playwright/fixtures/socketServer'; import TestApp from 'playwright/TestApp'; import buildApiUrl from 'playwright/utils/buildApiUrl'; +import * as configs from 'playwright/utils/configs'; import MockAddressPage from 'ui/address/testUtils/MockAddressPage'; import ContractCode from './ContractCode'; const addressHash = 'hash'; const CONTRACT_API_URL = buildApiUrl('contract', { hash: addressHash }); +const CONTRACT_AUDITS_API_URL = buildApiUrl('contract_security_audits', { hash: addressHash }); const hooksConfig = { router: { query: { hash: addressHash }, @@ -229,3 +233,54 @@ test('non verified', async({ mount, page }) => { await expect(component).toHaveScreenshot(); }); + +test.describe('with audits feature', () => { + + const withAuditsTest = test.extend({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + context: contextWithEnvs(configs.UIEnvs.hasContractAuditReports) as any, + }); + + withAuditsTest('no audits', async({ mount, page }) => { + await page.route(CONTRACT_API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(contractMock.verified), + })); + await page.route(CONTRACT_AUDITS_API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify({ items: [] }), + })); + await page.route('https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/**', (route) => route.abort()); + + const component = await mount( + + + , + { hooksConfig }, + ); + + await expect(component).toHaveScreenshot(); + }); + + withAuditsTest('has audits', async({ mount, page }) => { + await page.route(CONTRACT_API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(contractMock.verified), + })); + await page.route(CONTRACT_AUDITS_API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(contractAudits), + })); + + await page.route('https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/**', (route) => route.abort()); + + const component = await mount( + + + , + { hooksConfig }, + ); + + await expect(component).toHaveScreenshot(); + }); +}); diff --git a/ui/address/contract/ContractCode.tsx b/ui/address/contract/ContractCode.tsx index cfa2e5f003..7b77ebe3b5 100644 --- a/ui/address/contract/ContractCode.tsx +++ b/ui/address/contract/ContractCode.tsx @@ -7,7 +7,9 @@ import type { Address as AddressInfo } from 'types/api/address'; import { route } from 'nextjs-routes'; +import config from 'configs/app'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; +import { CONTRACT_LICENSES } from 'lib/contracts/licenses'; import dayjs from 'lib/date/dayjs'; import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketMessage from 'lib/socket/useSocketMessage'; @@ -18,6 +20,7 @@ import LinkExternal from 'ui/shared/LinkExternal'; import LinkInternal from 'ui/shared/LinkInternal'; import RawDataSnippet from 'ui/shared/RawDataSnippet'; +import ContractSecurityAudits from './ContractSecurityAudits'; import ContractSourceCode from './ContractSourceCode'; type Props = { @@ -26,10 +29,17 @@ type Props = { noSocket?: boolean; } -const InfoItem = chakra(({ label, value, className, isLoading }: { label: string; value: string; className?: string; isLoading: boolean }) => ( +type InfoItemProps = { + label: string; + content: string | React.ReactNode; + className?: string; + isLoading: boolean; +} + +const InfoItem = chakra(({ label, content, className, isLoading }: InfoItemProps) => ( { label } - { value } + { content } )); @@ -109,6 +119,23 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ); + const licenseLink = (() => { + if (!data?.license_type) { + return null; + } + + const license = CONTRACT_LICENSES.find((license) => license.type === data.license_type); + if (!license || license.type === 'none') { + return null; + } + + return ( + + { license.label } + + ); + })(); + const constructorArgs = (() => { if (!data?.decoded_constructor_args) { return data?.constructor_args; @@ -221,15 +248,23 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { { data?.is_verified && ( - { data.name && } - { data.compiler_version && } - { data.evm_version && } + { data.name && } + { data.compiler_version && } + { data.evm_version && } + { licenseLink && } { typeof data.optimization_enabled === 'boolean' && - } - { data.optimization_runs && } + } + { data.optimization_runs && } { data.verified_at && - } - { data.file_path && } + } + { data.file_path && } + { config.UI.hasContractAuditReports && ( + } + isLoading={ isPlaceholderData } + /> + ) } ) } diff --git a/ui/address/contract/ContractMethodCallable.tsx b/ui/address/contract/ContractMethodCallable.tsx deleted file mode 100644 index a9ce55bf0e..0000000000 --- a/ui/address/contract/ContractMethodCallable.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import { Box, Button, chakra, Flex } from '@chakra-ui/react'; -import React from 'react'; -import type { SubmitHandler } from 'react-hook-form'; -import { useForm, FormProvider } from 'react-hook-form'; - -import type { MethodFormFields, ContractMethodCallResult } from './types'; -import type { SmartContractMethodInput, SmartContractMethod } from 'types/api/contract'; - -import config from 'configs/app'; -import * as mixpanel from 'lib/mixpanel/index'; -import IconSvg from 'ui/shared/IconSvg'; - -import ContractMethodCallableRow from './ContractMethodCallableRow'; -import { formatFieldValues, transformFieldsToArgs } from './utils'; - -interface ResultComponentProps { - item: T; - result: ContractMethodCallResult; - onSettle: () => void; -} - -interface Props { - data: T; - onSubmit: (data: T, args: Array>) => Promise>; - resultComponent: (props: ResultComponentProps) => JSX.Element | null; - isWrite?: boolean; -} - -// groupName%groupIndex:inputName%inputIndex -const getFormFieldName = (input: { index: number; name: string }, group?: { index: number; name: string }) => - `${ group ? `${ group.name }%${ group.index }:` : '' }${ input.name || 'input' }%${ input.index }`; - -const ContractMethodCallable = ({ data, onSubmit, resultComponent: ResultComponent, isWrite }: Props) => { - - const [ result, setResult ] = React.useState>(); - const [ isLoading, setLoading ] = React.useState(false); - - const inputs: Array = React.useMemo(() => { - return [ - ...('inputs' in data ? data.inputs : []), - ...('stateMutability' in data && data.stateMutability === 'payable' ? [ { - name: `Send native ${ config.chain.currency.symbol || 'coin' }`, - type: 'uint256' as const, - internalType: 'uint256' as const, - fieldType: 'native_coin' as const, - } ] : []), - ]; - }, [ data ]); - - const formApi = useForm({ - mode: 'onBlur', - }); - - const handleTxSettle = React.useCallback(() => { - setLoading(false); - }, []); - - const handleFormChange = React.useCallback(() => { - result && setResult(undefined); - }, [ result ]); - - const onFormSubmit: SubmitHandler = React.useCallback(async(formData) => { - const formattedData = formatFieldValues(formData, inputs); - const args = transformFieldsToArgs(formattedData); - - setResult(undefined); - setLoading(true); - - onSubmit(data, args) - .then((result) => { - setResult(result); - }) - .catch((error) => { - setResult(error?.error || error?.data || (error?.reason && { message: error.reason }) || error); - setLoading(false); - }) - .finally(() => { - mixpanel.logEvent(mixpanel.EventTypes.CONTRACT_INTERACTION, { - 'Method type': isWrite ? 'Write' : 'Read', - 'Method name': 'name' in data ? data.name : 'Fallback', - }); - }); - }, [ inputs, onSubmit, data, isWrite ]); - - return ( - - - - - { inputs.map((input, index) => { - const fieldName = getFormFieldName({ name: input.name, index }); - - if (input.type === 'tuple' && input.components) { - return ( - - { index !== 0 && <>
} - - { input.name } ({ input.type }) - - { input.components.map((component, componentIndex) => { - const fieldName = getFormFieldName( - { name: component.name, index: componentIndex }, - { name: input.name, index }, - ); - - return ( - - ); - }) } - { index !== inputs.length - 1 && <>
} - - ); - } - - return ( - 1 } - onChange={ handleFormChange } - /> - ); - }) } - - - - - { 'outputs' in data && !isWrite && data.outputs.length > 0 && ( - - -

- { data.outputs.map(({ type, name }, index) => { - return ( - <> - { name } - { name ? `(${ type })` : type } - { index < data.outputs.length - 1 && , } - - ); - }) } -

-
- ) } - { result && } - - ); -}; - -export default React.memo(ContractMethodCallable) as typeof ContractMethodCallable; diff --git a/ui/address/contract/ContractMethodCallableRow.tsx b/ui/address/contract/ContractMethodCallableRow.tsx deleted file mode 100644 index 2cc01d7861..0000000000 --- a/ui/address/contract/ContractMethodCallableRow.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { Box, Flex, useColorModeValue } from '@chakra-ui/react'; -import React from 'react'; -import { useFormContext } from 'react-hook-form'; - -import type { MethodFormFields } from './types'; -import type { SmartContractMethodArgType, SmartContractMethodInput } from 'types/api/contract'; - -import ContractMethodField from './ContractMethodField'; -import ContractMethodFieldArray from './ContractMethodFieldArray'; -import { ARRAY_REGEXP } from './utils'; - -interface Props { - fieldName: string; - fieldType?: SmartContractMethodInput['fieldType']; - argName: string; - argType: SmartContractMethodArgType; - onChange: () => void; - isDisabled: boolean; - isGrouped?: boolean; - isOptional?: boolean; -} - -const ContractMethodCallableRow = ({ argName, fieldName, fieldType, argType, onChange, isDisabled, isGrouped, isOptional }: Props) => { - const { control, getValues, setValue } = useFormContext(); - const arrayTypeMatch = argType.match(ARRAY_REGEXP); - const nativeCoinFieldBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.100'); - - const content = arrayTypeMatch ? ( - - ) : ( - - ); - - const isNativeCoinField = fieldType === 'native_coin'; - - return ( - - - { argName }{ isOptional ? '' : '*' } ({ argType }) - - { content } - - ); -}; - -export default React.memo(ContractMethodCallableRow); diff --git a/ui/address/contract/ContractMethodField.tsx b/ui/address/contract/ContractMethodField.tsx deleted file mode 100644 index 47837114f6..0000000000 --- a/ui/address/contract/ContractMethodField.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import { - Box, - FormControl, - Input, - InputGroup, - InputRightElement, - useColorModeValue, -} from '@chakra-ui/react'; -import React from 'react'; -import type { Control, ControllerRenderProps, FieldError, UseFormGetValues, UseFormSetValue, UseFormStateReturn } from 'react-hook-form'; -import { Controller } from 'react-hook-form'; -import { NumericFormat } from 'react-number-format'; -import { isAddress, isHex, getAddress } from 'viem'; - -import type { MethodFormFields } from './types'; -import type { SmartContractMethodArgType } from 'types/api/contract'; - -import ClearButton from 'ui/shared/ClearButton'; - -import ContractMethodFieldZeroes from './ContractMethodFieldZeroes'; -import { INT_REGEXP, BYTES_REGEXP, getIntBoundaries, formatBooleanValue } from './utils'; - -interface Props { - name: string; - index?: number; - groupName?: string; - placeholder: string; - argType: SmartContractMethodArgType; - control: Control; - setValue: UseFormSetValue; - getValues: UseFormGetValues; - isDisabled: boolean; - isOptional?: boolean; - onChange: () => void; -} - -const ContractMethodField = ({ control, name, groupName, index, argType, placeholder, setValue, getValues, isDisabled, isOptional, onChange }: Props) => { - const ref = React.useRef(null); - const bgColor = useColorModeValue('white', 'black'); - - const handleClear = React.useCallback(() => { - setValue(name, ''); - onChange(); - ref.current?.focus(); - }, [ name, onChange, setValue ]); - - const handleAddZeroesClick = React.useCallback((power: number) => { - const value = groupName && index !== undefined ? getValues()[groupName][index] : getValues()[name]; - const zeroes = Array(power).fill('0').join(''); - const newValue = value ? value + zeroes : '1' + zeroes; - setValue(name, newValue); - onChange(); - }, [ getValues, groupName, index, name, onChange, setValue ]); - - const intMatch = React.useMemo(() => { - const match = argType.match(INT_REGEXP); - if (!match) { - return null; - } - - const [ , isUnsigned, power = '256' ] = match; - const [ min, max ] = getIntBoundaries(Number(power), Boolean(isUnsigned)); - - return { isUnsigned, power, min, max }; - }, [ argType ]); - - const bytesMatch = React.useMemo(() => { - return argType.match(BYTES_REGEXP); - }, [ argType ]); - - const renderInput = React.useCallback(( - { field, formState }: { field: ControllerRenderProps; formState: UseFormStateReturn }, - ) => { - const error: FieldError | undefined = index !== undefined && groupName !== undefined ? - (formState.errors[groupName] as unknown as Array)?.[index] : - formState.errors[name]; - - // show control for all inputs which allows to insert 10^18 or greater numbers - const hasZerosControl = intMatch && Number(intMatch.power) >= 64; - - return ( - - - - - - { typeof field.value === 'string' && field.value.replace('\n', '') && } - { hasZerosControl && } - - - - { error && { error.message } } - - ); - }, [ index, groupName, name, intMatch, isDisabled, isOptional, placeholder, bgColor, handleClear, handleAddZeroesClick ]); - - const validate = React.useCallback((_value: string | Array | undefined) => { - if (typeof _value === 'object' || !_value) { - return; - } - - const value = _value.replace('\n', ''); - - if (!value && !isOptional) { - return 'Field is required'; - } - - if (argType === 'address') { - if (!isAddress(value)) { - return 'Invalid address format'; - } - - // all lowercase addresses are valid - const isInLowerCase = value === value.toLowerCase(); - if (isInLowerCase) { - return true; - } - - // check if address checksum is valid - return getAddress(value) === value ? true : 'Invalid address checksum'; - } - - if (intMatch) { - const formattedValue = Number(value.replace(/\s/g, '')); - - if (Object.is(formattedValue, NaN)) { - return 'Invalid integer format'; - } - - if (formattedValue > intMatch.max || formattedValue < intMatch.min) { - const lowerBoundary = intMatch.isUnsigned ? '0' : `-1 * 2 ^ ${ Number(intMatch.power) / 2 }`; - const upperBoundary = intMatch.isUnsigned ? `2 ^ ${ intMatch.power } - 1` : `2 ^ ${ Number(intMatch.power) / 2 } - 1`; - return `Value should be in range from "${ lowerBoundary }" to "${ upperBoundary }" inclusively`; - } - - return true; - } - - if (argType === 'bool') { - const formattedValue = formatBooleanValue(value); - if (formattedValue === undefined) { - return 'Invalid boolean format. Allowed values: 0, 1, true, false'; - } - } - - if (bytesMatch) { - const [ , length ] = bytesMatch; - - if (!isHex(value)) { - return 'Invalid bytes format'; - } - - if (length) { - const valueLengthInBytes = value.replace('0x', '').length / 2; - return valueLengthInBytes !== Number(length) ? `Value should be ${ length } bytes in length` : true; - } - - return true; - } - - return true; - }, [ isOptional, argType, intMatch, bytesMatch ]); - - return ( - - ); -}; - -export default React.memo(ContractMethodField); diff --git a/ui/address/contract/ContractMethodFieldArray.tsx b/ui/address/contract/ContractMethodFieldArray.tsx deleted file mode 100644 index 35018b2f7f..0000000000 --- a/ui/address/contract/ContractMethodFieldArray.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { Flex, IconButton } from '@chakra-ui/react'; -import React from 'react'; -import type { Control, UseFormGetValues, UseFormSetValue } from 'react-hook-form'; -import { useFieldArray } from 'react-hook-form'; - -import type { MethodFormFields } from './types'; -import type { SmartContractMethodArgType } from 'types/api/contract'; - -import IconSvg from 'ui/shared/IconSvg'; - -import ContractMethodField from './ContractMethodField'; - -interface Props { - name: string; - size: number; - argType: SmartContractMethodArgType; - control: Control; - setValue: UseFormSetValue; - getValues: UseFormGetValues; - isDisabled: boolean; - onChange: () => void; -} - -const ContractMethodFieldArray = ({ control, name, setValue, getValues, isDisabled, argType, onChange, size }: Props) => { - const { fields, append, remove } = useFieldArray({ - name: name as never, - control, - }); - - React.useEffect(() => { - if (fields.length === 0) { - if (size === Infinity) { - append(''); - } else { - for (let i = 0; i < size - 1; i++) { - // a little hack to append multiple empty fields in the array - // had to adjust code in ContractMethodField as well - append('\n'); - } - } - } - - }, [ fields.length, append, size ]); - - const handleAddButtonClick = React.useCallback(() => { - append(''); - }, [ append ]); - - const handleRemoveButtonClick = React.useCallback((event: React.MouseEvent) => { - const itemIndex = event.currentTarget.getAttribute('data-index'); - if (itemIndex) { - remove(Number(itemIndex)); - } - }, [ remove ]); - - return ( - - { fields.map((field, index, array) => { - return ( - - - { array.length > 1 && size === Infinity && ( - } - isDisabled={ isDisabled } - /> - ) } - { index === array.length - 1 && size === Infinity && ( - } - isDisabled={ isDisabled } - /> - ) } - - ); - }) } - - ); -}; - -export default React.memo(ContractMethodFieldArray); diff --git a/ui/address/contract/ContractMethodsAccordionItem.tsx b/ui/address/contract/ContractMethodsAccordionItem.tsx index d69222cdad..b30c0d998b 100644 --- a/ui/address/contract/ContractMethodsAccordionItem.tsx +++ b/ui/address/contract/ContractMethodsAccordionItem.tsx @@ -45,50 +45,54 @@ const ContractMethodsAccordionItem = ({ data, ind return ( - - - { 'method_id' in data && ( - - - + { ({ isExpanded }) => ( + <> + + + { 'method_id' in data && ( + + + + + + ) } + + { index + 1 }. { data.type === 'fallback' || data.type === 'receive' ? data.type : data.name } - - ) } - - { index + 1 }. { data.type === 'fallback' || data.type === 'receive' ? data.type : data.name } - - { data.type === 'fallback' && ( - - ) } - { data.type === 'receive' && ( - + ) } + { data.type === 'receive' && ( + - ) } - - - - - { renderContent(data, index, id) } - + }/> + ) } + + + + + { renderContent(data, index, id) } + + + ) } ); }; diff --git a/ui/address/contract/ContractRead.pw.tsx b/ui/address/contract/ContractRead.pw.tsx index d540c1c07e..1dc215d070 100644 --- a/ui/address/contract/ContractRead.pw.tsx +++ b/ui/address/contract/ContractRead.pw.tsx @@ -37,7 +37,7 @@ test('base view +@mobile +@dark-mode', async({ mount, page }) => { await expect(component).toHaveScreenshot(); - await component.getByPlaceholder(/address/i).type('0xa113Ce24919C08a26C952E81681dAc861d6a2466'); + await component.getByPlaceholder(/address/i).fill('0xa113Ce24919C08a26C952E81681dAc861d6a2466'); await component.getByText(/read/i).click(); await component.getByText(/wei/i).click(); diff --git a/ui/address/contract/ContractRead.tsx b/ui/address/contract/ContractRead.tsx index 88330196ea..7a2fd94ded 100644 --- a/ui/address/contract/ContractRead.tsx +++ b/ui/address/contract/ContractRead.tsx @@ -14,9 +14,9 @@ import DataFetchAlert from 'ui/shared/DataFetchAlert'; import ContractConnectWallet from './ContractConnectWallet'; import ContractCustomAbiAlert from './ContractCustomAbiAlert'; import ContractImplementationAddress from './ContractImplementationAddress'; -import ContractMethodCallable from './ContractMethodCallable'; import ContractMethodConstant from './ContractMethodConstant'; import ContractReadResult from './ContractReadResult'; +import ContractMethodForm from './methodForm/ContractMethodForm'; import useWatchAccount from './useWatchAccount'; const ContractRead = () => { @@ -40,7 +40,7 @@ const ContractRead = () => { }, }); - const handleMethodFormSubmit = React.useCallback(async(item: SmartContractReadMethod, args: Array>) => { + const handleMethodFormSubmit = React.useCallback(async(item: SmartContractReadMethod, args: Array) => { return apiFetch<'contract_method_query', SmartContractQueryMethodRead>('contract_method_query', { pathParams: { hash: addressHash }, queryParams: { @@ -63,7 +63,7 @@ const ContractRead = () => { return { item.error }; } - if (item.outputs.some(({ value }) => value !== undefined && value !== null)) { + if (item.outputs?.some(({ value }) => value !== undefined && value !== null)) { return ( { item.outputs.map((output, index) => ) } @@ -72,11 +72,12 @@ const ContractRead = () => { } return ( - ); }, [ handleMethodFormSubmit ]); diff --git a/ui/address/contract/ContractSecurityAudits.tsx b/ui/address/contract/ContractSecurityAudits.tsx new file mode 100644 index 0000000000..cac7c356b0 --- /dev/null +++ b/ui/address/contract/ContractSecurityAudits.tsx @@ -0,0 +1,81 @@ +import { Box, Button, useDisclosure } from '@chakra-ui/react'; +import React from 'react'; + +import type { SmartContractSecurityAuditSubmission } from 'types/api/contract'; + +import useApiQuery from 'lib/api/useApiQuery'; +import dayjs from 'lib/date/dayjs'; +import ContainerWithScrollY from 'ui/shared/ContainerWithScrollY'; +import FormModal from 'ui/shared/FormModal'; +import LinkExternal from 'ui/shared/LinkExternal'; + +import ContractSubmitAuditForm from './contractSubmitAuditForm/ContractSubmitAuditForm'; + +const SCROLL_GRADIENT_HEIGHT = 24; + +type Props = { + addressHash?: string; +} + +const ContractSecurityAudits = ({ addressHash }: Props) => { + const { data, isPlaceholderData } = useApiQuery('contract_security_audits', { + pathParams: { hash: addressHash }, + queryOptions: { + refetchOnMount: false, + placeholderData: { items: [] }, + enabled: Boolean(addressHash), + }, + }); + + const containerRef = React.useRef(null); + const [ hasScroll, setHasScroll ] = React.useState(false); + + React.useEffect(() => { + if (!containerRef.current) { + return; + } + + setHasScroll(containerRef.current.scrollHeight >= containerRef.current.clientHeight + SCROLL_GRADIENT_HEIGHT / 2); + }, []); + + const formTitle = 'Submit audit'; + + const modalProps = useDisclosure(); + + const renderForm = React.useCallback(() => { + return ; + }, [ addressHash, modalProps.onClose ]); + + return ( + <> + + { data?.items && data.items.length > 0 && ( + + + { data.items.map(item => ( + + { `${ item.audit_company_name }, ${ dayjs(item.audit_publish_date).format('MMM DD, YYYY') }` } + + )) } + + + ) } + + isOpen={ modalProps.isOpen } + onClose={ modalProps.onClose } + title={ formTitle } + renderForm={ renderForm } + /> + + ); +}; + +export default React.memo(ContractSecurityAudits); diff --git a/ui/address/contract/ContractSourceCode.tsx b/ui/address/contract/ContractSourceCode.tsx index 4d5356eb65..1766479979 100644 --- a/ui/address/contract/ContractSourceCode.tsx +++ b/ui/address/contract/ContractSourceCode.tsx @@ -163,6 +163,7 @@ const ContractSourceCode = ({ address, implementationAddress }: Props) => { libraries={ primaryContractQuery.data?.external_libraries ?? undefined } language={ primaryContractQuery.data?.language ?? undefined } mainFile={ primaryEditorData[0]?.file_path } + contractName={ primaryContractQuery.data?.name || undefined } /> { secondaryEditorData && ( @@ -173,6 +174,7 @@ const ContractSourceCode = ({ address, implementationAddress }: Props) => { libraries={ secondaryContractQuery.data?.external_libraries ?? undefined } language={ secondaryContractQuery.data?.language ?? undefined } mainFile={ secondaryEditorData?.[0]?.file_path } + contractName={ secondaryContractQuery.data?.name || undefined } /> ) } diff --git a/ui/address/contract/ContractWrite.tsx b/ui/address/contract/ContractWrite.tsx index b199b99291..3e68421ed2 100644 --- a/ui/address/contract/ContractWrite.tsx +++ b/ui/address/contract/ContractWrite.tsx @@ -14,8 +14,8 @@ import DataFetchAlert from 'ui/shared/DataFetchAlert'; import ContractConnectWallet from './ContractConnectWallet'; import ContractCustomAbiAlert from './ContractCustomAbiAlert'; import ContractImplementationAddress from './ContractImplementationAddress'; -import ContractMethodCallable from './ContractMethodCallable'; import ContractWriteResult from './ContractWriteResult'; +import ContractMethodForm from './methodForm/ContractMethodForm'; import useContractAbi from './useContractAbi'; import { getNativeCoinValue, prepareAbi } from './utils'; @@ -39,12 +39,14 @@ const ContractWrite = () => { }, queryOptions: { enabled: Boolean(addressHash), + refetchOnMount: false, }, }); const contractAbi = useContractAbi({ addressHash, isProxy, isCustomAbi }); - const handleMethodFormSubmit = React.useCallback(async(item: SmartContractWriteMethod, args: Array>) => { + // TODO @tom2drum maybe move this inside the form + const handleMethodFormSubmit = React.useCallback(async(item: SmartContractWriteMethod, args: Array) => { if (!isConnected) { throw new Error('Wallet is not connected'); } @@ -66,21 +68,22 @@ const ContractWrite = () => { return { hash }; } - const _args = 'stateMutability' in item && item.stateMutability === 'payable' ? args.slice(0, -1) : args; - const value = 'stateMutability' in item && item.stateMutability === 'payable' ? getNativeCoinValue(args[args.length - 1]) : undefined; const methodName = item.name; if (!methodName) { throw new Error('Method name is not defined'); } + const _args = args.slice(0, item.inputs.length); + const value = getNativeCoinValue(args[item.inputs.length]); const abi = prepareAbi(contractAbi, item); + const hash = await walletClient?.writeContract({ args: _args, abi, functionName: methodName, address: addressHash as `0x${ string }`, - value: value as undefined, + value, }); return { hash }; @@ -88,12 +91,12 @@ const ContractWrite = () => { const renderItemContent = React.useCallback((item: SmartContractWriteMethod, index: number, id: number) => { return ( - ); }, [ handleMethodFormSubmit ]); diff --git a/ui/address/contract/ContractWriteResult.tsx b/ui/address/contract/ContractWriteResult.tsx index 9fed0e271e..266f64dd03 100644 --- a/ui/address/contract/ContractWriteResult.tsx +++ b/ui/address/contract/ContractWriteResult.tsx @@ -1,22 +1,19 @@ import React from 'react'; import { useWaitForTransaction } from 'wagmi'; +import type { ResultComponentProps } from './methodForm/types'; import type { ContractMethodWriteResult } from './types'; +import type { SmartContractWriteMethod } from 'types/api/contract'; import ContractWriteResultDumb from './ContractWriteResultDumb'; -interface Props { - result: ContractMethodWriteResult; - onSettle: () => void; -} - -const ContractWriteResult = ({ result, onSettle }: Props) => { +const ContractWriteResult = ({ result, onSettle }: ResultComponentProps) => { const txHash = result && 'hash' in result ? result.hash as `0x${ string }` : undefined; const txInfo = useWaitForTransaction({ hash: txHash, }); - return ; + return ; }; -export default React.memo(ContractWriteResult); +export default React.memo(ContractWriteResult) as typeof ContractWriteResult; diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_dark-color-mode_full-view-mobile-dark-mode-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_dark-color-mode_full-view-mobile-dark-mode-1.png index 991c4e2000..d4e7526c59 100644 Binary files a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_dark-color-mode_full-view-mobile-dark-mode-1.png and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_dark-color-mode_full-view-mobile-dark-mode-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_full-view-mobile-dark-mode-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_full-view-mobile-dark-mode-1.png index 371527fdcd..f11a52a153 100644 Binary files a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_full-view-mobile-dark-mode-1.png and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_full-view-mobile-dark-mode-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-with-changed-byte-code-socket-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-with-changed-byte-code-socket-1.png index 613f117789..6c88ba36e0 100644 Binary files a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-with-changed-byte-code-socket-1.png and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-with-changed-byte-code-socket-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-audits-feature-has-audits-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-audits-feature-has-audits-1.png new file mode 100644 index 0000000000..191d800a72 Binary files /dev/null and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-audits-feature-has-audits-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-audits-feature-no-audits-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-audits-feature-no-audits-1.png new file mode 100644 index 0000000000..ab936fc8f6 Binary files /dev/null and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-audits-feature-no-audits-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_mobile_full-view-mobile-dark-mode-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_mobile_full-view-mobile-dark-mode-1.png index 18dbc49d73..539f531700 100644 Binary files a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_mobile_full-view-mobile-dark-mode-1.png and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_mobile_full-view-mobile-dark-mode-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractRead.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png index 11221c4dfe..f19ba84553 100644 Binary files a/ui/address/contract/__screenshots__/ContractRead.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png and b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractRead.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-2.png b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-2.png index bf6bb309fa..9402cd11fc 100644 Binary files a/ui/address/contract/__screenshots__/ContractRead.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-2.png and b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-2.png differ diff --git a/ui/address/contract/__screenshots__/ContractRead.pw.tsx_default_base-view-mobile-dark-mode-1.png b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_default_base-view-mobile-dark-mode-1.png index ab26adfc56..82900c2f57 100644 Binary files a/ui/address/contract/__screenshots__/ContractRead.pw.tsx_default_base-view-mobile-dark-mode-1.png and b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_default_base-view-mobile-dark-mode-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractRead.pw.tsx_default_base-view-mobile-dark-mode-2.png b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_default_base-view-mobile-dark-mode-2.png index dcf7af98eb..635406e6f5 100644 Binary files a/ui/address/contract/__screenshots__/ContractRead.pw.tsx_default_base-view-mobile-dark-mode-2.png and b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_default_base-view-mobile-dark-mode-2.png differ diff --git a/ui/address/contract/__screenshots__/ContractRead.pw.tsx_mobile_base-view-mobile-dark-mode-1.png b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_mobile_base-view-mobile-dark-mode-1.png index 1408d976c6..930ca02a9e 100644 Binary files a/ui/address/contract/__screenshots__/ContractRead.pw.tsx_mobile_base-view-mobile-dark-mode-1.png and b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_mobile_base-view-mobile-dark-mode-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractRead.pw.tsx_mobile_base-view-mobile-dark-mode-2.png b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_mobile_base-view-mobile-dark-mode-2.png index 2aba458f92..b5d534a0d0 100644 Binary files a/ui/address/contract/__screenshots__/ContractRead.pw.tsx_mobile_base-view-mobile-dark-mode-2.png and b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_mobile_base-view-mobile-dark-mode-2.png differ diff --git a/ui/address/contract/__screenshots__/ContractWrite.pw.tsx_default_base-view-mobile-1.png b/ui/address/contract/__screenshots__/ContractWrite.pw.tsx_default_base-view-mobile-1.png index eee1a2a500..e58e9c4755 100644 Binary files a/ui/address/contract/__screenshots__/ContractWrite.pw.tsx_default_base-view-mobile-1.png and b/ui/address/contract/__screenshots__/ContractWrite.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractWrite.pw.tsx_mobile_base-view-mobile-1.png b/ui/address/contract/__screenshots__/ContractWrite.pw.tsx_mobile_base-view-mobile-1.png index 5724e9b234..c9a99a0bfb 100644 Binary files a/ui/address/contract/__screenshots__/ContractWrite.pw.tsx_mobile_base-view-mobile-1.png and b/ui/address/contract/__screenshots__/ContractWrite.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/address/contract/contractSubmitAuditForm/ContractSubmitAuditForm.pw.tsx b/ui/address/contract/contractSubmitAuditForm/ContractSubmitAuditForm.pw.tsx new file mode 100644 index 0000000000..d82adea521 --- /dev/null +++ b/ui/address/contract/contractSubmitAuditForm/ContractSubmitAuditForm.pw.tsx @@ -0,0 +1,18 @@ +import { test, expect } from '@playwright/experimental-ct-react'; +import React from 'react'; + +import TestApp from 'playwright/TestApp'; + +import ContractSubmitAuditForm from './ContractSubmitAuditForm'; + +test('base view', async({ mount }) => { + + const component = await mount( + + { /* eslint-disable-next-line react/jsx-no-bind */ } + {} }/> + , + ); + + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/address/contract/contractSubmitAuditForm/ContractSubmitAuditForm.tsx b/ui/address/contract/contractSubmitAuditForm/ContractSubmitAuditForm.tsx new file mode 100644 index 0000000000..b03d4884cc --- /dev/null +++ b/ui/address/contract/contractSubmitAuditForm/ContractSubmitAuditForm.tsx @@ -0,0 +1,124 @@ +import { Button, VStack } from '@chakra-ui/react'; +import React from 'react'; +import type { SubmitHandler } from 'react-hook-form'; +import { useForm } from 'react-hook-form'; + +import type { SmartContractSecurityAuditSubmission } from 'types/api/contract'; + +import type { ResourceError } from 'lib/api/resources'; +import useApiFetch from 'lib/api/useApiFetch'; +import useToast from 'lib/hooks/useToast'; + +import AuditComment from './fields/AuditComment'; +import AuditCompanyName from './fields/AuditCompanyName'; +import AuditProjectName from './fields/AuditProjectName'; +import AuditProjectUrl from './fields/AuditProjectUrl'; +import AuditReportDate from './fields/AuditReportDate'; +import AuditReportUrl from './fields/AuditReportUrl'; +import AuditSubmitterEmail from './fields/AuditSubmitterEmail'; +import AuditSubmitterIsOwner from './fields/AuditSubmitterIsOwner'; +import AuditSubmitterName from './fields/AuditSubmitterName'; + +interface Props { + address?: string; + onSuccess: () => void; +} + +export type Inputs = { + submitter_name: string; + submitter_email: string; + is_project_owner: boolean; + project_name: string; + project_url: string; + audit_company_name: string; + audit_report_url: string; + audit_publish_date: string; + comment?: string; +} + +type AuditSubmissionErrors = { + errors: Record>; +} + +const ContractSubmitAuditForm = ({ address, onSuccess }: Props) => { + const containerRef = React.useRef(null); + + const apiFetch = useApiFetch(); + const toast = useToast(); + + const { handleSubmit, formState, control, setError } = useForm({ + mode: 'onTouched', + defaultValues: { is_project_owner: false }, + }); + + const onFormSubmit: SubmitHandler = React.useCallback(async(data) => { + try { + await apiFetch<'contract_security_audits', SmartContractSecurityAuditSubmission, AuditSubmissionErrors>('contract_security_audits', { + pathParams: { hash: address }, + fetchParams: { + method: 'POST', + body: data, + }, + }); + + toast({ + position: 'top-right', + title: 'Success', + description: 'Your audit report has been successfully submitted for review', + status: 'success', + variant: 'subtle', + isClosable: true, + }); + + onSuccess(); + + } catch (_error) { + const error = _error as ResourceError; + // add scroll to the error field + const errorMap = error?.payload?.errors; + if (errorMap && Object.keys(errorMap).length) { + (Object.keys(errorMap) as Array).forEach((errorField) => { + setError(errorField, { type: 'custom', message: errorMap[errorField].join(', ') }); + }); + } else { + toast({ + position: 'top-right', + title: 'Error', + description: (_error as ResourceError<{ message: string }>)?.payload?.message || 'Something went wrong. Try again later.', + status: 'error', + variant: 'subtle', + isClosable: true, + }); + } + } + }, [ apiFetch, address, toast, setError, onSuccess ]); + + return ( +
+ + + + + + + + + + + + + +
+ ); +}; + +export default React.memo(ContractSubmitAuditForm); diff --git a/ui/address/contract/contractSubmitAuditForm/__screenshots__/ContractSubmitAuditForm.pw.tsx_default_base-view-1.png b/ui/address/contract/contractSubmitAuditForm/__screenshots__/ContractSubmitAuditForm.pw.tsx_default_base-view-1.png new file mode 100644 index 0000000000..c5a18bad26 Binary files /dev/null and b/ui/address/contract/contractSubmitAuditForm/__screenshots__/ContractSubmitAuditForm.pw.tsx_default_base-view-1.png differ diff --git a/ui/address/contract/contractSubmitAuditForm/fields/AuditComment.tsx b/ui/address/contract/contractSubmitAuditForm/fields/AuditComment.tsx new file mode 100644 index 0000000000..918a496dea --- /dev/null +++ b/ui/address/contract/contractSubmitAuditForm/fields/AuditComment.tsx @@ -0,0 +1,40 @@ +import { FormControl, Textarea } from '@chakra-ui/react'; +import React from 'react'; +import type { Control, ControllerProps } from 'react-hook-form'; +import { Controller } from 'react-hook-form'; + +import InputPlaceholder from 'ui/shared/InputPlaceholder'; + +import type { Inputs } from '../ContractSubmitAuditForm'; + +interface Props { + control: Control; +} + +const AuditComment = ({ control }: Props) => { + const renderControl: ControllerProps['render'] = React.useCallback(({ field, fieldState }) => { + return ( + +