diff --git a/.gitignore b/.gitignore index c877d7bbea..721584d133 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ .DS_Store *.pem .tools +grafana # debug npm-debug.log* diff --git a/.vscode/tasks.json b/.vscode/tasks.json index d8682527e6..7b37441165 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -338,15 +338,19 @@ "options": [ "main", "main.L2", - "poa_core", - "eth_goerli", - "sepolia", + "localhost", + "base", + "gnosis", "eth", - "rootstock", + "eth_goerli", + "eth_sepolia", + "optimism", + "optimism_sepolia", "polygon", + "rootstock", + "stability", "zkevm", - "gnosis", - "localhost", + "zksync", ], "default": "main" }, diff --git a/Dockerfile b/Dockerfile index f38d68ebaa..a4c38c66f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,8 @@ # ***************************** FROM node:20.11.0-alpine AS deps # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. -RUN apk add --no-cache libc6-compat +RUN apk add --no-cache libc6-compat python3 make g++ +RUN ln -sf /usr/bin/python3 /usr/bin/python ### APP # Install dependencies @@ -69,7 +70,7 @@ RUN cd ./deploy/tools/feature-reporter && yarn build ### ENV VARIABLES CHECKER -# Copy dependencies and source code, then build +# Copy dependencies and source code, then build COPY --from=deps /envs-validator/node_modules ./deploy/tools/envs-validator/node_modules RUN cd ./deploy/tools/envs-validator && yarn build @@ -118,6 +119,11 @@ RUN ["chmod", "-R", "777", "./public"] COPY --from=builder /app/.env.registry . COPY --from=builder /app/.env . +# Copy ENVs presets +ARG ENVS_PRESET +ENV ENVS_PRESET=$ENVS_PRESET +COPY ./configs/envs ./configs/envs + # Automatically leverage output traces to reduce image size # https://nextjs.org/docs/advanced-features/output-file-tracing COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md new file mode 100644 index 0000000000..30ec4f8b6f --- /dev/null +++ b/RELEASE_NOTES.md @@ -0,0 +1,33 @@ +## 🚀 New Features +- Description of the new feature 1. +- Description of the new feature 2. + +## 🐛 Bug Fixes +- Description of the bug fix 1. +- Description of the bug fix 2. + +## ⚡ Performance Improvements +- Description of the performance improvement 1. +- Description of the performance improvement 2. + +## 📦 Dependencies updates +- Updated dependency: PackageName 1 to version x.x.x. +- Updated dependency: PackageName 2 to version x.x.x. + +## ✨ Other Changes +- Another minor change 1. +- Another minor change 2. + +## 🚨 Changes in ENV variables +- Added new environment variable: ENV_VARIABLE_NAME with value. +- Updated existing environment variable: ENV_VARIABLE_NAME to new value. + +**Full list of the ENV variables**: [v1.2.3](https://github.com/blockscout/frontend/blob/v1.2.3/docs/ENVS.md) + +## 🦄 New Contributors +- @contributor1 made their first contribution in https://github.com/blockscout/frontend/pull/1 +- @contributor2 made their first contribution in https://github.com/blockscout/frontend/pull/2 + +--- + +**Full Changelog**: https://github.com/blockscout/frontend/compare/v1.2.2...v1.2.3 diff --git a/configs/app/chain.ts b/configs/app/chain.ts index 1ec156e4e0..32f5376aac 100644 --- a/configs/app/chain.ts +++ b/configs/app/chain.ts @@ -12,8 +12,8 @@ const chain = Object.freeze({ symbol: getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL'), decimals: Number(getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS')) || DEFAULT_CURRENCY_DECIMALS, }, - governanceToken: { - symbol: getEnvValue('NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL'), + secondaryCoin: { + symbol: getEnvValue('NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL'), }, rpcUrl: getEnvValue('NEXT_PUBLIC_NETWORK_RPC_URL'), isTestnet: getEnvValue('NEXT_PUBLIC_IS_TESTNET') === 'true', diff --git a/configs/app/features/addressMetadata.ts b/configs/app/features/addressMetadata.ts new file mode 100644 index 0000000000..5ca5b78ada --- /dev/null +++ b/configs/app/features/addressMetadata.ts @@ -0,0 +1,27 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; + +const apiHost = getEnvValue('NEXT_PUBLIC_METADATA_SERVICE_API_HOST'); + +const title = 'Address metadata'; + +const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => { + if (apiHost) { + return Object.freeze({ + title, + isEnabled: true, + api: { + endpoint: apiHost, + basePath: '', + }, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/adsBanner.ts b/configs/app/features/adsBanner.ts index 0785ab160f..f9860d1afc 100644 --- a/configs/app/features/adsBanner.ts +++ b/configs/app/features/adsBanner.ts @@ -1,7 +1,7 @@ import type { Feature } from './types'; import type { AdButlerConfig } from 'types/client/adButlerConfig'; import { SUPPORTED_AD_BANNER_PROVIDERS } from 'types/client/adProviders'; -import type { AdBannerProviders } from 'types/client/adProviders'; +import type { AdBannerProviders, AdBannerAdditionalProviders } from 'types/client/adProviders'; import { getEnvValue, parseEnvJson } from '../utils'; @@ -11,6 +11,8 @@ const provider: AdBannerProviders = (() => { return envValue && SUPPORTED_AD_BANNER_PROVIDERS.includes(envValue) ? envValue : 'slise'; })(); +const additionalProvider = getEnvValue('NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER') as AdBannerAdditionalProviders; + const title = 'Banner ads'; type AdsBannerFeaturePayload = { @@ -23,6 +25,15 @@ type AdsBannerFeaturePayload = { mobile: AdButlerConfig; }; }; +} | { + provider: Exclude; + additionalProvider: 'adbutler'; + adButler: { + config: { + desktop: AdButlerConfig; + mobile: AdButlerConfig; + }; + }; } const config: Feature = (() => { @@ -44,6 +55,24 @@ const config: Feature = (() => { }); } } else if (provider !== 'none') { + + if (additionalProvider === 'adbutler') { + const desktopConfig = parseEnvJson(getEnvValue('NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP')); + const mobileConfig = parseEnvJson(getEnvValue('NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE')); + + return Object.freeze({ + title, + isEnabled: true, + provider, + additionalProvider, + adButler: { + config: { + desktop: desktopConfig, + mobile: mobileConfig, + }, + }, + }); + } return Object.freeze({ title, isEnabled: true, diff --git a/configs/app/features/dataAvailability.ts b/configs/app/features/dataAvailability.ts new file mode 100644 index 0000000000..add9e5fec5 --- /dev/null +++ b/configs/app/features/dataAvailability.ts @@ -0,0 +1,21 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; + +const title = 'Data availability'; + +const config: Feature<{ isEnabled: true }> = (() => { + if (getEnvValue('NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED') === 'true') { + return Object.freeze({ + title, + isEnabled: true, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/index.ts b/configs/app/features/index.ts index 042b11ad97..3272c26741 100644 --- a/configs/app/features/index.ts +++ b/configs/app/features/index.ts @@ -1,16 +1,19 @@ export { default as account } from './account'; export { default as addressVerification } from './addressVerification'; +export { default as addressMetadata } from './addressMetadata'; export { default as adsBanner } from './adsBanner'; export { default as adsText } from './adsText'; 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 dataAvailability } from './dataAvailability'; 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'; diff --git a/configs/app/features/marketplace.ts b/configs/app/features/marketplace.ts index 288c14a125..937397be6a 100644 --- a/configs/app/features/marketplace.ts +++ b/configs/app/features/marketplace.ts @@ -10,35 +10,53 @@ const submitFormUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM'); const suggestIdeasFormUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM'); const categoriesUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL'); const adminServiceApiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST'); +const securityReportsUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL'); +const featuredApp = getEnvValue('NEXT_PUBLIC_MARKETPLACE_FEATURED_APP'); +const bannerContentUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL'); +const bannerLinkUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL'); const title = 'Marketplace'; const config: Feature<( { configUrl: string } | { api: { endpoint: string; basePath: string } } -) & { submitFormUrl: string; categoriesUrl: string | undefined; suggestIdeasFormUrl: string | undefined } -> = (() => { +) & { + submitFormUrl: string; + categoriesUrl: string | undefined; + suggestIdeasFormUrl: string | undefined; + securityReportsUrl: string | undefined; + featuredApp: string | undefined; + banner: { contentUrl: string; linkUrl: string } | undefined; +}> = (() => { if (enabled === 'true' && chain.rpcUrl && submitFormUrl) { + const props = { + submitFormUrl, + categoriesUrl, + suggestIdeasFormUrl, + securityReportsUrl, + featuredApp, + banner: bannerContentUrl && bannerLinkUrl ? { + contentUrl: bannerContentUrl, + linkUrl: bannerLinkUrl, + } : undefined, + }; + if (configUrl) { return Object.freeze({ title, isEnabled: true, configUrl, - submitFormUrl, - categoriesUrl, - suggestIdeasFormUrl, + ...props, }); } else if (adminServiceApiHost) { return Object.freeze({ title, isEnabled: true, - submitFormUrl, - categoriesUrl, - suggestIdeasFormUrl, api: { endpoint: adminServiceApiHost, basePath: '', }, + ...props, }); } } 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/sol2uml.ts b/configs/app/features/sol2uml.ts index 06853e62d8..5a0ac2d4be 100644 --- a/configs/app/features/sol2uml.ts +++ b/configs/app/features/sol2uml.ts @@ -1,5 +1,7 @@ import type { Feature } from './types'; +import stripTrailingSlash from 'lib/stripTrailingSlash'; + import { getEnvValue } from '../utils'; const apiEndpoint = getEnvValue('NEXT_PUBLIC_VISUALIZE_API_HOST'); @@ -13,7 +15,7 @@ const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => isEnabled: true, api: { endpoint: apiEndpoint, - basePath: '', + basePath: stripTrailingSlash(getEnvValue('NEXT_PUBLIC_VISUALIZE_API_BASE_PATH') || ''), }, }); } diff --git a/configs/app/features/stats.ts b/configs/app/features/stats.ts index b05f2f9659..d3a90ce061 100644 --- a/configs/app/features/stats.ts +++ b/configs/app/features/stats.ts @@ -1,5 +1,7 @@ import type { Feature } from './types'; +import stripTrailingSlash from 'lib/stripTrailingSlash'; + import { getEnvValue } from '../utils'; const apiEndpoint = getEnvValue('NEXT_PUBLIC_STATS_API_HOST'); @@ -13,7 +15,7 @@ const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => isEnabled: true, api: { endpoint: apiEndpoint, - basePath: '', + basePath: stripTrailingSlash(getEnvValue('NEXT_PUBLIC_STATS_API_BASE_PATH') || ''), }, }); } diff --git a/configs/app/meta.ts b/configs/app/meta.ts index cf0f309534..0fd72681ea 100644 --- a/configs/app/meta.ts +++ b/configs/app/meta.ts @@ -1,13 +1,17 @@ import app from './app'; import { getEnvValue, getExternalAssetFilePath } from './utils'; -const defaultImageUrl = app.baseUrl + '/static/og_placeholder.png'; +const defaultImageUrl = '/static/og_placeholder.png'; const meta = Object.freeze({ promoteBlockscoutInTitle: getEnvValue('NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE') || 'true', og: { description: getEnvValue('NEXT_PUBLIC_OG_DESCRIPTION') || '', - imageUrl: getExternalAssetFilePath('NEXT_PUBLIC_OG_IMAGE_URL') || defaultImageUrl, + imageUrl: app.baseUrl + (getExternalAssetFilePath('NEXT_PUBLIC_OG_IMAGE_URL') || defaultImageUrl), + enhancedDataEnabled: getEnvValue('NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED') === 'true', + }, + seo: { + enhancedDataEnabled: getEnvValue('NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED') === 'true', }, }); diff --git a/configs/envs/.env.base b/configs/envs/.env.base new file mode 100644 index 0000000000..4753aef51f --- /dev/null +++ b/configs/envs/.env.base @@ -0,0 +1,74 @@ +# Set of ENVs for Base network explorer +# https://base.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=Base Mainnet +NEXT_PUBLIC_NETWORK_SHORT_NAME=Base +NEXT_PUBLIC_NETWORK_ID=8453 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation +NEXT_PUBLIC_NETWORK_RPC_URL=https://mainnet.base.org/ + +# api configuration +NEXT_PUBLIC_API_HOST=base.blockscout.com +NEXT_PUBLIC_API_BASE_PATH=/ + +# ui config +## homepage +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=linear-gradient(136.9deg,rgb(107,94,236)1.5%,rgb(0,82,255)56.84%,rgb(82,62,231)98.54%) +## sidebar +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/base.svg +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/base.svg +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/base-mainnet.json +NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://base.drpc.org?ref=559183','text':'Public RPC'}] +## footer +NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/base-mainnet.json +##views +NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] +NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'OpenSea','collection_url':'https://opensea.io/assets/ethereum/{hash}','instance_url':'https://opensea.io/assets/ethereum/{hash}/{id}','logo_url':'https://opensea.io/static/images/logos/opensea-logo.svg'},{'name':'LooksRare','collection_url':'https://looksrare.org/collections/{hash}','instance_url':'https://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':'l2scan','baseUrl':'https://base.l2scan.co/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}},{'title':'Tenderly','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/base'}},{'title':'3xpl','baseUrl':'https://3xpl.com/','paths':{'tx':'/base/transaction','address':'/base/address'}}] +NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE=gradient_avatar +NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=

BNS & DAppscout collaboration: earn BNS points for purchasing .base domains! Check here for more details

+ +# app features +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xfd5c5dae7b69fe29e61d19b9943e688aa0f1be1e983c4fba8fe985f90ff69d5f +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_AUTH_URL=http://localhost:3000/login +NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout +NEXT_PUBLIC_STATS_API_HOST=https://stats-l2-base-mainnet.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_AD_BANNER_ADDITIONAL_PROVIDER=adbutler +NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={'id':'728301','width':'728','height':'90'} +NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={'id':'728301','width':'320','height':'100'} +NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com +NEXT_PUBLIC_SWAP_BUTTON_URL=aerodrome +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/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL +NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form +NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_HAS_USER_OPS=true +NEXT_PUBLIC_METASUITES_ENABLED=true +NEXT_PUBLIC_ROLLUP_TYPE=optimistic +NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://bridge.base.org/withdraw +NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com/ +NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-base.safe.global + +#meta +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/base-mainnet.png diff --git a/configs/envs/.env.eth b/configs/envs/.env.eth index d61e45b153..2019083866 100644 --- a/configs/envs/.env.eth +++ b/configs/envs/.env.eth @@ -22,21 +22,21 @@ NEXT_PUBLIC_API_BASE_PATH=/ # ui config ## homepage -NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs', 'coin_price', 'market_cap'] +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap'] ## sidebar NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth.json ## footer ##views NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'OpenSea','collection_url':'https://opensea.io/assets/ethereum/{hash}','instance_url':'https://opensea.io/assets/ethereum/{hash}/{id}','logo_url':'https://opensea.io/static/images/logos/opensea-logo.svg'},{'name':'LooksRare','collection_url':'https://looksrare.org/collections/{hash}','instance_url':'https://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':'Etherscan','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/etherscan.png?raw=true','baseUrl':'https://etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}, {'title':'Blockchair','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/blockchair.png?raw=true','baseUrl':'https://blockchair.com/','paths':{'tx':'/ethereum/transaction','address':'/ethereum/address','token':'/ethereum/erc-20/token','block':'/ethereum/block'}},{'title':'Sentio','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/sentio.png?raw=true','baseUrl':'https://app.sentio.xyz/','paths':{'tx':'/tx/1','address':'/contract/1'}}, {'title':'Tenderly','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/tenderly.png?raw=true','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/mainnet'}}, {'title':'0xPPL','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/0xPPl.png?raw=true','baseUrl':'https://0xppl.com','paths':{'tx':'/Ethereum/tx','address':'/','token':'/c/Ethereum'}}, {'title':'3xpl','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/3xpl.png?raw=true','baseUrl':'https://3xpl.com/','paths':{'tx':'/ethereum/transaction','address':'/ethereum/address'}} ]" +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Etherscan','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/etherscan.png?raw=true','baseUrl':'https://etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}},{'title':'Blockchair','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/blockchair.png?raw=true','baseUrl':'https://blockchair.com/','paths':{'tx':'/ethereum/transaction','address':'/ethereum/address','token':'/ethereum/erc-20/token','block':'/ethereum/block'}},{'title':'Sentio','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/sentio.png?raw=true','baseUrl':'https://app.sentio.xyz/','paths':{'tx':'/tx/1','address':'/contract/1'}},{'title':'Tenderly','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/tenderly.png?raw=true','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/mainnet'}},{'title':'0xPPL','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/0xPPl.png?raw=true','baseUrl':'https://0xppl.com','paths':{'tx':'/Ethereum/tx','address':'/','token':'/c/Ethereum'}},{'title':'3xpl','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/3xpl.png?raw=true','baseUrl':'https://3xpl.com/','paths':{'tx':'/ethereum/transaction','address':'/ethereum/address'}}] # app features NEXT_PUBLIC_APP_ENV=development NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d NEXT_PUBLIC_HAS_BEACON_CHAIN=true NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true -NEXT_PUBLIC_AUTH_URL=http://localhost:3000 +NEXT_PUBLIC_AUTH_URL=http://localhost:3000/login NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s-prod-1.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com @@ -44,6 +44,16 @@ NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.co 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 +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED=true diff --git a/configs/envs/.env.eth_goerli b/configs/envs/.env.eth_goerli index 8e601473d5..997d31c125 100644 --- a/configs/envs/.env.eth_goerli +++ b/configs/envs/.env.eth_goerli @@ -53,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 \ No newline at end of file diff --git a/configs/envs/.env.sepolia b/configs/envs/.env.eth_sepolia similarity index 89% rename from configs/envs/.env.sepolia rename to configs/envs/.env.eth_sepolia index b54adc0775..8f3cb0e164 100644 --- a/configs/envs/.env.sepolia +++ b/configs/envs/.env.eth_sepolia @@ -44,7 +44,7 @@ NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Etherscan','baseUrl':'https://sepolia.e NEXT_PUBLIC_APP_ENV=development NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xbf69c7abc4fee283b59a9633dadfdaedde5c5ee0fba3e80a08b5b8a3acbd4363 NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true -NEXT_PUBLIC_AUTH_URL=http://localhost:3000 +NEXT_PUBLIC_AUTH_URL=http://localhost:3000/login NEXT_PUBLIC_LOGOUT_URL=https://blockscout-goerli.us.auth0.com/v2/logout NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C @@ -55,6 +55,12 @@ 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 +NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true +NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-sepolia.safe.global +NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens-rs-test.k8s-dev.blockscout.com +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata-test.k8s-dev.blockscout.com #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.gnosis b/configs/envs/.env.gnosis index 1e20f6c303..4c5b09260f 100644 --- a/configs/envs/.env.gnosis +++ b/configs/envs/.env.gnosis @@ -13,6 +13,7 @@ NEXT_PUBLIC_NETWORK_ID=100 NEXT_PUBLIC_NETWORK_CURRENCY_NAME=xDAI NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=xDAI NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL=GNO NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.gnosischain.com @@ -22,9 +23,9 @@ NEXT_PUBLIC_API_BASE_PATH=/ # ui config ## homepage -NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] -NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND="rgb(46, 74, 60)" -NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR="rgb(255, 255, 255)" +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','secondary_coin_price','market_cap'] +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgb(46,74,60) +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgb(255,255,255) ## sidebar NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/gnosis-chain-mainnet.json NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/gnosis.svg @@ -42,6 +43,7 @@ NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace/gnosis-chain.json NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrmiO9mDGJoPNmJe +NEXT_PUBLIC_STATS_API_HOST=https://stats-gnosis-mainnet.k8s-prod-1.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask'] NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com diff --git a/configs/envs/.env.main b/configs/envs/.env.main index 2d003f5930..9322f0dbca 100644 --- a/configs/envs/.env.main +++ b/configs/envs/.env.main @@ -13,7 +13,6 @@ NEXT_PUBLIC_NETWORK_ID=5 NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 -NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL=ETH NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.ankr.com/eth_goerli NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation NEXT_PUBLIC_IS_TESTNET=true @@ -48,8 +47,12 @@ 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_SECURITY_REPORTS_URL=https://gist.githubusercontent.com/maxaleks/ce5c7e3de53e8f5b240b88265daf5839/raw/328383c958a8f7ecccf6d50c953bcdf8ab3faa0a/security_reports_goerli_test.json NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form +# NEXT_PUBLIC_MARKETPLACE_FEATURED_APP=aave +NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/maxaleks/36f779fd7d74877b57ec7a25a9a3a6c9/raw/746a8a59454c0537235ee44616c4690ce3bbf3c8/banner.html +NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://www.basename.app NEXT_PUBLIC_STATS_API_HOST=https://stats-goerli.k8s-dev.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.k8s-dev.blockscout.com NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info-test.k8s-dev.blockscout.com diff --git a/configs/envs/.env.optimism b/configs/envs/.env.optimism new file mode 100644 index 0000000000..f5438e5fc8 --- /dev/null +++ b/configs/envs/.env.optimism @@ -0,0 +1,66 @@ +# Set of ENVs for Optimism (dev only) +# https://optimism.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=OP Mainnet +NEXT_PUBLIC_NETWORK_SHORT_NAME=OP +NEXT_PUBLIC_NETWORK_ID=10 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL=OP +NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation +NEXT_PUBLIC_NETWORK_RPC_URL=https://mainnet.optimism.io + +# api configuration +NEXT_PUBLIC_API_HOST=optimism.blockscout.com +NEXT_PUBLIC_API_BASE_PATH=/ + +# ui config +## homepage +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap','secondary_coin_price'] +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgb(255,255,255) +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=linear-gradient(90deg,rgb(232,52,53)0%,rgb(139,28,232)100%) +## sidebar +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/optimism-mainnet.json +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism.svg +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism.svg +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg +NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://optimism.drpc.org?ref=559183','text':'Public RPC'}] +## footer +NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/optimism.json +## views +NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] +## misc +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/optimism-mainnet.png +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Tenderly','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/optimistic'}},{'title':'3xpl','baseUrl':'https://3xpl.com/','paths':{'tx':'/optimism/transaction','address':'/optimism/address'}}] +# app features +NEXT_PUBLIC_APP_INSTANCE=local +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_AUTH_URL=http://localhost:3000/login +NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws +NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout +NEXT_PUBLIC_STATS_API_HOST=https://stats-optimism-mainnet.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_MARKETPLACE_ENABLED=true +NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json +NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json +NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form +NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL +NEXT_PUBLIC_SWAP_BUTTON_URL=uniswap +NEXT_PUBLIC_HAS_USER_OPS=true +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +# rollup +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.blockscout.com/ \ No newline at end of file diff --git a/configs/envs/.env.optimism_goerli b/configs/envs/.env.optimism_goerli deleted file mode 100644 index d34f132149..0000000000 --- a/configs/envs/.env.optimism_goerli +++ /dev/null @@ -1,48 +0,0 @@ -# Set of ENVs for zkevm (dev only) -# 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='OP Goerli' -NEXT_PUBLIC_NETWORK_SHORT_NAME='OP Goerli' -NEXT_PUBLIC_NETWORK_ID=420 -NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether -NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH -NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 -NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation -NEXT_PUBLIC_NETWORK_RPC_URL=https://goerli.optimism.io - -# api configuration -NEXT_PUBLIC_API_HOST=optimism-goerli.blockscout.com -NEXT_PUBLIC_API_PORT=80 -NEXT_PUBLIC_API_PROTOCOL=http -NEXT_PUBLIC_API_BASE_PATH=/ - -# ui config -## homepage -NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] -## sidebar -NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/polygon-mainnet.json -## footer -## misc -NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Etherscan','baseUrl':'https://etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}] -# app features -NEXT_PUBLIC_APP_INSTANCE=local -NEXT_PUBLIC_APP_ENV=development -NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d -NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true -# NEXT_PUBLIC_AUTH_URL=http://localhost:3000 -NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws -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_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/ diff --git a/configs/envs/.env.optimism_sepolia b/configs/envs/.env.optimism_sepolia new file mode 100644 index 0000000000..7bf13dc4a1 --- /dev/null +++ b/configs/envs/.env.optimism_sepolia @@ -0,0 +1,63 @@ +# Set of ENVs for Optimism (dev only) +# https://optimism.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=OP Sepolia +NEXT_PUBLIC_NETWORK_SHORT_NAME=OP Sepolia +NEXT_PUBLIC_NETWORK_ID=11155420 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL=OP +NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation +NEXT_PUBLIC_NETWORK_RPC_URL=https://sepolia.optimism.io + +# api configuration +NEXT_PUBLIC_API_HOST=optimism-sepolia.blockscout.com + +# ui config +## homepage +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgb(255,255,255) +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=linear-gradient(90deg,rgb(232,52,53)0%,rgb(139,28,232)100%) +## sidebar +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/optimism-sepolia.json +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism.svg +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism.svg +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg +NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://optimism.drpc.org?ref=559183','text':'Public RPC'}] +## footer +NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/optimism.json +## views +NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] +## misc +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/optimism-mainnet.png +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Tenderly','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/optimistic-sepolia'}}] +NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=

Build faster with the Superchain Dev Console: Get testnet ETH and tools to help you build,launch,and grow your app on the Superchain

+# app features +NEXT_PUBLIC_APP_INSTANCE=local +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x59d26836041ab35169bdce431d68d070b7b8acb589fa52e126e6c828b6ece5e9 +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_AUTH_URL=http://localhost:3000/login +NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws +NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout +NEXT_PUBLIC_STATS_API_HOST=https://stats-optimism-sepolia.k8s.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_MARKETPLACE_ENABLED=false +NEXT_PUBLIC_SWAP_BUTTON_URL=uniswap +NEXT_PUBLIC_HAS_USER_OPS=true +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +# rollup +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-sepolia.blockscout.com/ diff --git a/configs/envs/.env.poa_core b/configs/envs/.env.poa_core deleted file mode 100644 index f874d89f5d..0000000000 --- a/configs/envs/.env.poa_core +++ /dev/null @@ -1,40 +0,0 @@ -# Set of ENVs for POA network explorer -# https://blockscout.com/poa/core/ - -# app configuration -NEXT_PUBLIC_APP_PROTOCOL=http -NEXT_PUBLIC_APP_HOST=localhost -NEXT_PUBLIC_APP_PORT=3000 - -# blockchain parameters -NEXT_PUBLIC_NETWORK_NAME=POA -NEXT_PUBLIC_NETWORK_SHORT_NAME=POA -NEXT_PUBLIC_NETWORK_ID=99 -NEXT_PUBLIC_NETWORK_CURRENCY_NAME=POA -NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=POA -NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 - -# api configuration -NEXT_PUBLIC_API_HOST=blockscout.com -NEXT_PUBLIC_API_BASE_PATH=/poa/core - -# ui config -## homepage -NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap'] -NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND='no-repeat bottom 20% right 0px/100% url(https://neon-labs.org/images/index/banner.jpg)' -NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=\#DCFE76 -## sidebar -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/dev/configs/network-logos/poa.svg -NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-icons/poa.svg -## footer -## misc -NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address','block':'/ethereum/poa/core/block'}}] - -# app features -NEXT_PUBLIC_APP_ENV=development -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_NETWORK_VERIFICATION_TYPE=validation -NEXT_PUBLIC_NETWORK_RPC_URL=https://core.poa.network diff --git a/configs/envs/.env.polygon b/configs/envs/.env.polygon index 2cc023145d..e39066d6a6 100644 --- a/configs/envs/.env.polygon +++ b/configs/envs/.env.polygon @@ -23,8 +23,8 @@ NEXT_PUBLIC_API_BASE_PATH=/ # ui config ## homepage NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] -NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND="linear-gradient(122deg, rgba(162, 41, 197, 1) 0%, rgba(123, 63, 228, 1) 100%)" -NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR="rgba(255, 255, 255, 1)" +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=linear-gradient(122deg,rgba(162,41,197,1)0%,rgba(123,63,228,1)100%) +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(255,255,255,1) ## sidebar NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/polygon-mainnet.json NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/polygon.svg @@ -35,9 +35,7 @@ NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-c # app features NEXT_PUBLIC_APP_ENV=development -# NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x97fa753626b8d44011d0b9f9a947c735f20b6e895efdee49d7cda76a50001017 NEXT_PUBLIC_HAS_BEACON_CHAIN=false -# NEXT_PUBLIC_STATS_API_HOST=https://stats-rsk-testnet.k8s.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask'] diff --git a/configs/envs/.env.pw b/configs/envs/.env.pw index 8798b1b73d..223abf5f32 100644 --- a/configs/envs/.env.pw +++ b/configs/envs/.env.pw @@ -17,6 +17,7 @@ NEXT_PUBLIC_IS_TESTNET=true NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation # api configuration +NEXT_PUBLIC_API_PROTOCOL=http NEXT_PUBLIC_API_HOST=localhost NEXT_PUBLIC_API_PORT=3003 NEXT_PUBLIC_API_BASE_PATH=/ @@ -41,12 +42,14 @@ 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 NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx -NEXT_PUBLIC_STATS_API_HOST=https://localhost:3004 -NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://localhost:3005 -NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://localhost:3006 +NEXT_PUBLIC_STATS_API_HOST=http://localhost:3004 +NEXT_PUBLIC_CONTRACT_INFO_API_HOST=http://localhost:3005 +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=http://localhost:3006 NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx +NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx \ No newline at end of file diff --git a/configs/envs/.env.rootstock b/configs/envs/.env.rootstock index ad4bcfb880..97b6ae0a48 100644 --- a/configs/envs/.env.rootstock +++ b/configs/envs/.env.rootstock @@ -23,7 +23,7 @@ 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=rgb(255,145,0) ## 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/rootstock.svg diff --git a/configs/envs/.env.stability b/configs/envs/.env.stability new file mode 100644 index 0000000000..6642687f41 --- /dev/null +++ b/configs/envs/.env.stability @@ -0,0 +1,61 @@ +# 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=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_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout +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/configs/envs/.env.zkevm b/configs/envs/.env.zkevm index 9d1ef2487a..17b03a0116 100644 --- a/configs/envs/.env.zkevm +++ b/configs/envs/.env.zkevm @@ -18,8 +18,6 @@ NEXT_PUBLIC_NETWORK_RPC_URL=https://zkevm-rpc.com # api configuration NEXT_PUBLIC_API_HOST=zkevm.blockscout.com -NEXT_PUBLIC_API_PORT=80 -NEXT_PUBLIC_API_PROTOCOL=http NEXT_PUBLIC_API_BASE_PATH=/ # ui config @@ -29,8 +27,8 @@ NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/polygon-mainnet.json NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/polygon.svg NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/polygon-short.svg -NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND='linear-gradient(122deg, rgba(162, 41, 197, 1) 0%, rgba(123, 63, 228, 1) 100%)' -NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR='rgba(255, 255, 255, 1)' +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=linear-gradient(122deg,rgba(162,41,197,1)0%,rgba(123,63,228,1)100%) +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(255,255,255,1) ## footer ## misc NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Etherscan','baseUrl':'https://etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}] @@ -39,7 +37,6 @@ NEXT_PUBLIC_APP_INSTANCE=local NEXT_PUBLIC_APP_ENV=development NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true -# NEXT_PUBLIC_AUTH_URL=http://localhost:3000 NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s.blockscout.com @@ -48,4 +45,4 @@ NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.co NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com # rollup NEXT_PUBLIC_ROLLUP_TYPE=zkEvm -NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://polygon.blockscout.com +NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com diff --git a/configs/envs/.env.zksync b/configs/envs/.env.zksync new file mode 100644 index 0000000000..b7ddf44afb --- /dev/null +++ b/configs/envs/.env.zksync @@ -0,0 +1,56 @@ +# Set of ENVs for zkSync (dev only) +# https://zksync.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=ZkSync Era +NEXT_PUBLIC_NETWORK_SHORT_NAME=ZkSync Era +NEXT_PUBLIC_NETWORK_ID=324 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation +NEXT_PUBLIC_NETWORK_RPC_URL=https://mainnet.era.zksync.io + +# api configuration +NEXT_PUBLIC_API_HOST=zksync.blockscout.com +NEXT_PUBLIC_API_BASE_PATH=/ + +# ui config +## homepage +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +## sidebar +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/polygon-mainnet.json +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/zksync.svg +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/zksync-dark.svg +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/zksync-short.svg +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/zksync-short-dark.svg +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgba(53,103,246,1) +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(255,255,255,1) +NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://zksync.drpc.org?ref=559183','text':'Public RPC'}] +## footer +NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/zksync.json +## views +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=false +## misc +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'l2scan','baseUrl':'https://zksync-era.l2scan.co/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}] +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/zksync.png +# app features +NEXT_PUBLIC_APP_INSTANCE=local +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x79c7802ccdf3be5a49c47cc751aad351b0027e8275f6f54878eda50ee559a648 +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_LOGOUT_URL=https://zksync.us.auth0.com/v2/logout +NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws +NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s.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_SAFE_TX_SERVICE_URL=https://safe-transaction-zksync.safe.global +# rollup +NEXT_PUBLIC_ROLLUP_TYPE=zkSync +NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com diff --git a/deploy/scripts/download_assets.sh b/deploy/scripts/download_assets.sh index f80d043ad0..747b7269c9 100755 --- a/deploy/scripts/download_assets.sh +++ b/deploy/scripts/download_assets.sh @@ -16,6 +16,8 @@ ASSETS_DIR="$1" ASSETS_ENVS=( "NEXT_PUBLIC_MARKETPLACE_CONFIG_URL" "NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL" + "NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL" + "NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL" "NEXT_PUBLIC_FEATURED_NETWORKS" "NEXT_PUBLIC_FOOTER_LINKS" "NEXT_PUBLIC_NETWORK_LOGO" diff --git a/deploy/scripts/entrypoint.sh b/deploy/scripts/entrypoint.sh index 2924f09189..6f0f8c17b1 100755 --- a/deploy/scripts/entrypoint.sh +++ b/deploy/scripts/entrypoint.sh @@ -1,5 +1,40 @@ #!/bin/bash + +export_envs_from_preset() { + if [ -z "$ENVS_PRESET" ]; then + return + fi + + if [ "$ENVS_PRESET" = "none" ]; then + return + fi + + local preset_file="./configs/envs/.env.$ENVS_PRESET" + + if [ ! -f "$preset_file" ]; then + return + fi + + local blacklist=( + "NEXT_PUBLIC_APP_PROTOCOL" + "NEXT_PUBLIC_APP_HOST" + "NEXT_PUBLIC_APP_PORT" + "NEXT_PUBLIC_APP_ENV" + "NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL" + ) + + while IFS='=' read -r name value; do + name="${name#"${name%%[![:space:]]*}"}" # Trim leading whitespace + if [[ -n $name && $name == "NEXT_PUBLIC_"* && ! "${blacklist[*]}" =~ "$name" ]]; then + export "$name"="$value" + fi + done < <(grep "^[^#;]" "$preset_file") +} + +# If there is a preset, load the environment variables from the its file +export_envs_from_preset + # Download external assets ./download_assets.sh ./public/assets diff --git a/deploy/scripts/make_envs_template.sh b/deploy/scripts/make_envs_template.sh new file mode 100644 index 0000000000..063e833a7d --- /dev/null +++ b/deploy/scripts/make_envs_template.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# Check if the number of arguments provided is correct +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +input_file="$1" +prefix="NEXT_PUBLIC_" + +# Function to make the environment variables template file +# It will read the input file, extract all prefixed string and use them as variables names +# This variables will have placeholders for their values at buildtime which will be replaced with actual values at runtime +make_envs_template_file() { + output_file=".env.production" + + # Check if file already exists and empty its content if it does + if [ -f "$output_file" ]; then + > "$output_file" + fi + + grep -oE "${prefix}[[:alnum:]_]+" "$input_file" | sort -u | while IFS= read -r var_name; do + echo "$var_name=__PLACEHOLDER_FOR_${var_name}__" >> "$output_file" + done +} + +# Function to save build-time environment variables to .env file +save_build-time_envs() { + output_file=".env" + + # Check if file already exists and empty its content if it does or create a new one + if [ -f "$output_file" ]; then + > "$output_file" + else + touch "$output_file" + fi + + env | grep "^${prefix}" | while IFS= read -r line; do + echo "$line" >> "$output_file" + done +} + +make_envs_template_file +save_build-time_envs \ No newline at end of file diff --git a/deploy/scripts/replace_envs.sh b/deploy/scripts/replace_envs.sh new file mode 100644 index 0000000000..4f469972dd --- /dev/null +++ b/deploy/scripts/replace_envs.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# no verbose +set +x + +# config +envFilename='.env.production' +nextFolder='./.next/' + +# replacing build-stage ENVs with run-stage ENVs +# https://raphaelpralat.medium.com/system-environment-variables-in-next-js-with-docker-1f0754e04cde +function replace_envs { + # read all config file + while read line; do + # no comment or not empty + if [ "${line:0:1}" == "#" ] || [ "${line}" == "" ]; then + continue + fi + + # split + configName="$(cut -d'=' -f1 <<<"$line")" + configValue="$(cut -d'=' -f2- <<<"$line")" + # get system env + envValue=$(env | grep "^$configName=" | sed "s/^$configName=//g"); + + # if config found + if [ -n "$configValue" ]; then + # replace all + echo "Replace: ${configValue} with: ${envValue}" + find $nextFolder \( -type d -name .git -prune \) -o -type f -print0 | xargs -0 sed -i "s#$configValue#${envValue-''}#g" + fi + done < $envFilename +} + +replace_envs \ No newline at end of file diff --git a/deploy/tools/envs-validator/dev.sh b/deploy/tools/envs-validator/dev.sh new file mode 100644 index 0000000000..71bfe0cff2 --- /dev/null +++ b/deploy/tools/envs-validator/dev.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +cp ../../../types/envs.ts . +export NEXT_PUBLIC_GIT_COMMIT_SHA=$(git rev-parse --short HEAD) +export NEXT_PUBLIC_GIT_TAG=$(git describe --tags --abbrev=0) +../../scripts/make_envs_template.sh ../../../docs/ENVS.md +yarn build +dotenv -e ../../../configs/envs/.env.main -e ../../../configs/envs/.env.secrets yarn validate \ No newline at end of file diff --git a/deploy/tools/envs-validator/index.ts b/deploy/tools/envs-validator/index.ts index 105efca7e1..9770ea6399 100644 --- a/deploy/tools/envs-validator/index.ts +++ b/deploy/tools/envs-validator/index.ts @@ -37,6 +37,7 @@ async function validateEnvs(appEnvs: Record) { 'NEXT_PUBLIC_FEATURED_NETWORKS', 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', 'NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL', + 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', 'NEXT_PUBLIC_FOOTER_LINKS', ]; diff --git a/deploy/tools/envs-validator/schema.ts b/deploy/tools/envs-validator/schema.ts index fd2a2c656c..0db98f496c 100644 --- a/deploy/tools/envs-validator/schema.ts +++ b/deploy/tools/envs-validator/schema.ts @@ -9,12 +9,12 @@ declare module 'yup' { import * as yup from 'yup'; 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 { SUPPORTED_AD_TEXT_PROVIDERS, SUPPORTED_AD_BANNER_PROVIDERS, SUPPORTED_AD_BANNER_ADDITIONAL_PROVIDERS } from '../../../types/client/adProviders'; +import type { AdTextProviders, AdBannerProviders, AdBannerAdditionalProviders } 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 type { MarketplaceAppOverview, MarketplaceAppSecurityReportRaw, MarketplaceAppSecurityReport } 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'; @@ -25,6 +25,7 @@ import type { ValidatorsChainType } from '../../../types/client/validators'; import type { WalletType } from '../../../types/client/wallets'; import { SUPPORTED_WALLETS } from '../../../types/client/wallets'; import type { CustomLink, CustomLinksGroup } from '../../../types/footerLinks'; +import { CHAIN_INDICATOR_IDS } from '../../../types/homepage'; import type { ChainIndicatorId } from '../../../types/homepage'; import { type NetworkVerificationType, type NetworkExplorer, type FeaturedNetwork, NETWORK_GROUPS } from '../../../types/networks'; import type { AddressViewId } from '../../../types/views/address'; @@ -75,11 +76,75 @@ 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(), }); +const issueSeverityDistributionSchema: yup.ObjectSchema = yup + .object({ + critical: yup.number().required(), + gas: yup.number().required(), + high: yup.number().required(), + informational: yup.number().required(), + low: yup.number().required(), + medium: yup.number().required(), + }); + +const solidityscanReportSchema: yup.ObjectSchema = yup + .object({ + contractname: yup.string().required(), + scan_status: yup.string().required(), + scan_summary: yup + .object({ + issue_severity_distribution: issueSeverityDistributionSchema.required(), + lines_analyzed_count: yup.number().required(), + scan_time_taken: yup.number().required(), + score: yup.string().required(), + score_v2: yup.string().required(), + threat_score: yup.string().required(), + }) + .required(), + scanner_reference_url: yup.string().test(urlTest).required(), + }); + +const contractDataSchema: yup.ObjectSchema = yup + .object({ + address: yup.string().required(), + isVerified: yup.boolean().required(), + solidityScanReport: solidityscanReportSchema.nullable().notRequired(), + }); + +const chainsDataSchema = yup.lazy((objValue) => { + let schema = yup.object(); + Object.keys(objValue).forEach((key) => { + schema = schema.shape({ + [key]: yup.object({ + overallInfo: yup.object({ + verifiedNumber: yup.number().required(), + totalContractsNumber: yup.number().required(), + solidityScanContractsNumber: yup.number().required(), + securityScore: yup.number().required(), + issueSeverityDistribution: issueSeverityDistributionSchema.required(), + }).required(), + contractsData: yup.array().of(contractDataSchema).required(), + }), + }); + }); + return schema; +}); + +const securityReportSchema: yup.ObjectSchema = yup + .object({ + appName: yup.string().required(), + chainsData: chainsDataSchema, + }); + const marketplaceSchema = yup .object() .shape({ @@ -120,6 +185,40 @@ const marketplaceSchema = yup // 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_ENABLED'), }), + NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL: yup + .array() + .json() + .of(securityReportSchema) + .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', { + is: true, + then: (schema) => schema, + // eslint-disable-next-line max-len + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), + }), + NEXT_PUBLIC_MARKETPLACE_FEATURED_APP: 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_FEATURED_APP cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), + }), + NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL: yup + .string() + .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_BANNER_CONTENT_URL cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), + }), + NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL: yup + .string() + .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_BANNER_LINK_URL cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), + }), }); const beaconChainSchema = yup @@ -171,12 +270,23 @@ const adButlerConfigSchema = yup height: yup.number().positive().required(), }) .required(), + }) + .when('NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER', { + is: (value: AdBannerProviders) => value === 'adbutler', + then: (schema) => schema + .shape({ + id: yup.string().required(), + width: yup.number().positive().required(), + height: yup.number().positive().required(), + }) + .required(), }); const adsBannerSchema = yup .object() .shape({ NEXT_PUBLIC_AD_BANNER_PROVIDER: yup.string().oneOf(SUPPORTED_AD_BANNER_PROVIDERS), + NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER: yup.string().oneOf(SUPPORTED_AD_BANNER_ADDITIONAL_PROVIDERS), NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP: adButlerConfigSchema, NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE: adButlerConfigSchema, }); @@ -239,12 +349,20 @@ const accountSchema = yup then: (schema) => schema.test(urlTest).required(), otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_LOGOUT_URL cannot not be used if NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED is not set to "true"'), }), + }); + +const adminServiceSchema = yup + .object() + .shape({ NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: yup .string() - .when('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED', { - is: (value: boolean) => value, + .when([ 'NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED', 'NEXT_PUBLIC_MARKETPLACE_ENABLED' ], { + is: (value1: boolean, value2: boolean) => value1 || value2, then: (schema) => schema.test(urlTest), - otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_ADMIN_SERVICE_API_HOST cannot not be used if NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED is not set to "true"'), + otherwise: (schema) => schema.max( + -1, + 'NEXT_PUBLIC_ADMIN_SERVICE_API_HOST cannot not be used if NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED or NEXT_PUBLIC_MARKETPLACE_ENABLED is not set to "true"', + ), }), }); @@ -372,7 +490,7 @@ const schema = yup NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME: yup.string(), NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL: yup.string(), NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS: yup.number().integer().positive(), - NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL: yup.string(), + NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL: yup.string(), NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: yup.string().oneOf([ 'validation', 'mining' ]), NEXT_PUBLIC_IS_TESTNET: yup.boolean(), @@ -389,7 +507,7 @@ const schema = yup .array() .transform(replaceQuotes) .json() - .of(yup.string().oneOf([ 'daily_txs', 'coin_price', 'market_cap', 'tvl' ])), + .of(yup.string().oneOf(CHAIN_INDICATOR_IDS)), NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR: yup.string(), NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND: yup.string(), NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME: yup.boolean(), @@ -468,9 +586,12 @@ const schema = yup // 5. Features configuration NEXT_PUBLIC_API_SPEC_URL: yup.string().test(urlTest), NEXT_PUBLIC_STATS_API_HOST: yup.string().test(urlTest), + NEXT_PUBLIC_STATS_API_BASE_PATH: yup.string(), NEXT_PUBLIC_VISUALIZE_API_HOST: yup.string().test(urlTest), + NEXT_PUBLIC_VISUALIZE_API_BASE_PATH: yup.string(), NEXT_PUBLIC_CONTRACT_INFO_API_HOST: yup.string().test(urlTest), NEXT_PUBLIC_NAME_SERVICE_API_HOST: yup.string().test(urlTest), + NEXT_PUBLIC_METADATA_SERVICE_API_HOST: yup.string().test(urlTest), NEXT_PUBLIC_GRAPHIQL_TRANSACTION: yup.string().matches(regexp.HEX_REGEXP), NEXT_PUBLIC_WEB3_WALLETS: yup .mixed() @@ -490,12 +611,17 @@ 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_OG_ENHANCED_DATA_ENABLED: yup.boolean(), + NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED: yup.boolean(), + 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)), + NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED: yup.boolean(), // 6. External services envs NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(), @@ -513,6 +639,7 @@ const schema = yup .concat(rollupSchema) .concat(beaconChainSchema) .concat(bridgedTokensSchema) - .concat(sentrySchema); + .concat(sentrySchema) + .concat(adminServiceSchema); export default schema; diff --git a/deploy/tools/envs-validator/test.sh b/deploy/tools/envs-validator/test.sh index 179ef25bc9..7e29716658 100755 --- a/deploy/tools/envs-validator/test.sh +++ b/deploy/tools/envs-validator/test.sh @@ -1,6 +1,5 @@ #!/bin/bash -secrets_file=".env.secrets" test_folder="./test" common_file="${test_folder}/.env.common" @@ -8,7 +7,6 @@ common_file="${test_folder}/.env.common" export NEXT_PUBLIC_GIT_COMMIT_SHA=$(git rev-parse --short HEAD) export NEXT_PUBLIC_GIT_TAG=$(git describe --tags --abbrev=0) ../../scripts/collect_envs.sh ../../../docs/ENVS.md -cp ../../../.env.example ${secrets_file} # Copy test assets mkdir -p "./public/assets" @@ -26,7 +24,6 @@ validate_file() { dotenv \ -e $test_file \ -e $common_file \ - -e $secrets_file \ yarn run validate -- --silent if [ $? -eq 0 ]; then @@ -46,4 +43,4 @@ for file in "${test_files[@]}"; do if [ $? -eq 1 ]; then exit 1 fi -done \ No newline at end of file +done diff --git a/deploy/tools/envs-validator/test/.env.adbutler_add b/deploy/tools/envs-validator/test/.env.adbutler_add new file mode 100644 index 0000000000..7f1968e4bb --- /dev/null +++ b/deploy/tools/envs-validator/test/.env.adbutler_add @@ -0,0 +1,4 @@ +NEXT_PUBLIC_AD_BANNER_PROVIDER='slise' +NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER='adbutler' +NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={'id':'123456','width':'728','height':'90'} +NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={'id':'654321','width':'300','height':'100'} \ No newline at end of file diff --git a/deploy/tools/envs-validator/test/.env.base b/deploy/tools/envs-validator/test/.env.base index 1635d21d88..4ed2fa938a 100644 --- a/deploy/tools/envs-validator/test/.env.base +++ b/deploy/tools/envs-validator/test/.env.base @@ -1,3 +1,15 @@ +NEXT_PUBLIC_SENTRY_DSN=https://sentry.io +NEXT_PUBLIC_AUTH_URL=https://example.com +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_LOGOUT_URL=https://example.com +NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx +NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx +NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X +NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx +NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx +NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx +FAVICON_GENERATOR_API_KEY=xxx +NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx NEXT_PUBLIC_AD_TEXT_PROVIDER=coinzilla NEXT_PUBLIC_AD_BANNER_PROVIDER=slise NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://example.com @@ -11,6 +23,7 @@ NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS=[{'id':'1','title':'Ethereum','short_title':'E NEXT_PUBLIC_BRIDGED_TOKENS_BRIDGES=[{'type':'omni','title':'OmniBridge','short_title':'OMNI'}] NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout={domain}','icon_url':'https://example.com/icon.svg'}] NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://example.com +NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true NEXT_PUBLIC_FEATURED_NETWORKS=https://example.com NEXT_PUBLIC_FOOTER_LINKS=https://example.com NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d @@ -24,11 +37,13 @@ NEXT_PUBLIC_GAS_TRACKER_ENABLED=true NEXT_PUBLIC_GAS_TRACKER_UNITS=['gwei'] NEXT_PUBLIC_IS_TESTNET=true NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE='Hello' +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://example.com +NEXT_PUBLIC_METASUITES_ENABLED=true NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Explorer','baseUrl':'https://example.com/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}] -NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL=gETH +NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL=GNO NEXT_PUBLIC_NETWORK_ICON=https://example.com/icon.png NEXT_PUBLIC_NETWORK_ICON_DARK=https://example.com/icon.png NEXT_PUBLIC_NETWORK_LOGO=https://example.com/logo.png @@ -38,9 +53,13 @@ NEXT_PUBLIC_NETWORK_SHORT_NAME=Test NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation NEXT_PUBLIC_OG_DESCRIPTION='Hello world!' NEXT_PUBLIC_OG_IMAGE_URL=https://example.com/image.png +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED=true 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_STATS_API_BASE_PATH=/ NEXT_PUBLIC_USE_NEXT_JS_PROXY=false NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE=gradient_avatar NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS=['top_accounts'] @@ -49,7 +68,8 @@ NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'NFT Marketplace','collection_url':' 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_VISUALIZE_API_HOST=https://example.com +NEXT_PUBLIC_VISUALIZE_API_BASE_PATH=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 +NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE=stability diff --git a/deploy/tools/envs-validator/test/.env.common b/deploy/tools/envs-validator/test/.env.common index 1f900840ff..5788f392d3 100644 --- a/deploy/tools/envs-validator/test/.env.common +++ b/deploy/tools/envs-validator/test/.env.common @@ -1,7 +1,4 @@ NEXT_PUBLIC_API_HOST=blockscout.com NEXT_PUBLIC_APP_HOST=localhost -NEXT_PUBLIC_AUTH_URL=https://example.com -NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true -NEXT_PUBLIC_LOGOUT_URL=https://example.com NEXT_PUBLIC_NETWORK_ID=1 NEXT_PUBLIC_NETWORK_NAME=Testnet diff --git a/deploy/tools/envs-validator/test/.env.marketplace b/deploy/tools/envs-validator/test/.env.marketplace index 316dd70bd1..01eab57086 100644 --- a/deploy/tools/envs-validator/test/.env.marketplace +++ b/deploy/tools/envs-validator/test/.env.marketplace @@ -3,4 +3,8 @@ 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_MARKETPLACE_SECURITY_REPORTS_URL=https://example.com NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://example.com +NEXT_PUBLIC_MARKETPLACE_FEATURED_APP=aave +NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/maxaleks/36f779fd7d74877b57ec7a25a9a3a6c9/raw/746a8a59454c0537235ee44616c4690ce3bbf3c8/banner.html +NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://www.basename.app diff --git a/deploy/tools/envs-validator/test/assets/marketplace_security_reports.json b/deploy/tools/envs-validator/test/assets/marketplace_security_reports.json new file mode 100644 index 0000000000..cf0f481ae3 --- /dev/null +++ b/deploy/tools/envs-validator/test/assets/marketplace_security_reports.json @@ -0,0 +1,1073 @@ +[ + { + "appName": "paraswap", + "doc": "https://developers.paraswap.network/smart-contracts", + "chainsData": { + "1": { + "overallInfo": { + "verifiedNumber": 4, + "totalContractsNumber": 4, + "solidityScanContractsNumber": 4, + "securityScore": 77.41749999999999, + "issueSeverityDistribution": { + "critical": 5, + "gas": 58, + "high": 9, + "informational": 27, + "low": 41, + "medium": 5 + } + }, + "contractsData": [ + { + "address": "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57", + "contract_chain": "optimism", + "contract_platform": "blockscout", + "contract_url": "https://optimism.blockscout.com/address/0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57", + "contractname": "AugustusSwapper", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57/blockscout/eth?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 8, + "high": 4, + "informational": 7, + "low": 8, + "medium": 1 + }, + "lines_analyzed_count": 180, + "scan_time_taken": 1, + "score": "3.61", + "score_v2": "72.22", + "threat_score": "73.68" + } + } + }, + { + "address": "0x216b4b4ba9f3e719726886d34a177484278bfcae", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x216b4b4ba9f3e719726886d34a177484278bfcae", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0x216b4b4ba9f3e719726886d34a177484278bfcae", + "contractname": "TokenTransferProxy", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x216b4b4ba9f3e719726886d34a177484278bfcae/blockscout/eth?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 1, + "gas": 29, + "high": 5, + "informational": 14, + "low": 21, + "medium": 3 + }, + "lines_analyzed_count": 553, + "scan_time_taken": 1, + "score": "3.92", + "score_v2": "78.48", + "threat_score": "78.95" + } + } + }, + { + "address": "0xa68bEA62Dc4034A689AA0F58A76681433caCa663", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0xa68bEA62Dc4034A689AA0F58A76681433caCa663", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0xa68bEA62Dc4034A689AA0F58A76681433caCa663", + "contractname": "AugustusRegistry", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0xa68bEA62Dc4034A689AA0F58A76681433caCa663/blockscout/eth?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 3, + "high": 0, + "informational": 5, + "low": 4, + "medium": 0 + }, + "lines_analyzed_count": 103, + "scan_time_taken": 0, + "score": "4.22", + "score_v2": "84.47", + "threat_score": "88.89" + } + } + }, + { + "address": "0xeF13101C5bbD737cFb2bF00Bbd38c626AD6952F7", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0xeF13101C5bbD737cFb2bF00Bbd38c626AD6952F7", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0xeF13101C5bbD737cFb2bF00Bbd38c626AD6952F7", + "contractname": "FeeClaimer", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0xeF13101C5bbD737cFb2bF00Bbd38c626AD6952F7/blockscout/eth?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 18, + "high": 0, + "informational": 1, + "low": 8, + "medium": 1 + }, + "lines_analyzed_count": 149, + "scan_time_taken": 0, + "score": "3.72", + "score_v2": "74.50", + "threat_score": "94.74" + } + } + } + ] + }, + "10": { + "overallInfo": { + "verifiedNumber": 3, + "totalContractsNumber": 4, + "solidityScanContractsNumber": 3, + "securityScore": 75.44333333333333, + "issueSeverityDistribution": { + "critical": 4, + "gas": 29, + "high": 4, + "informational": 20, + "low": 20, + "medium": 2 + } + }, + "contractsData": [ + { + "address": "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57", + "contract_chain": "optimism", + "contract_platform": "blockscout", + "contract_url": "https://optimism.blockscout.com/address/0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57", + "contractname": "AugustusSwapper", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57/blockscout/optimism?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 8, + "high": 4, + "informational": 7, + "low": 8, + "medium": 1 + }, + "lines_analyzed_count": 180, + "scan_time_taken": 1, + "score": "3.61", + "score_v2": "72.22", + "threat_score": "73.68" + } + } + }, + { + "address": "0x216B4B4Ba9F3e719726886d34a177484278Bfcae", + "isVerified": false, + "solidityScanReport": null + }, + { + "address": "0x6e7bE86000dF697facF4396efD2aE2C322165dC3", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x6e7bE86000dF697facF4396efD2aE2C322165dC3", + "contract_chain": "optimism", + "contract_platform": "blockscout", + "contract_url": "https://optimism.blockscout.com/address/0x6e7bE86000dF697facF4396efD2aE2C322165dC3", + "contractname": "AugustusRegistry", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x6e7bE86000dF697facF4396efD2aE2C322165dC3/blockscout/optimism?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 3, + "high": 0, + "informational": 5, + "low": 4, + "medium": 0 + }, + "lines_analyzed_count": 102, + "scan_time_taken": 0, + "score": "4.22", + "score_v2": "84.31", + "threat_score": "88.89" + } + } + }, + { + "address": "0xA7465CCD97899edcf11C56D2d26B49125674e45F", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0xA7465CCD97899edcf11C56D2d26B49125674e45F", + "contract_chain": "optimism", + "contract_platform": "blockscout", + "contract_url": "https://optimism.blockscout.com/address/0xA7465CCD97899edcf11C56D2d26B49125674e45F", + "contractname": "FeeClaimer", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0xA7465CCD97899edcf11C56D2d26B49125674e45F/blockscout/optimism?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 18, + "high": 0, + "informational": 8, + "low": 8, + "medium": 1 + }, + "lines_analyzed_count": 149, + "scan_time_taken": 1, + "score": "3.49", + "score_v2": "69.80", + "threat_score": "94.74" + } + } + } + ] + }, + "8453": { + "overallInfo": { + "verifiedNumber": 1, + "totalContractsNumber": 4, + "solidityScanContractsNumber": 1, + "securityScore": 73.33, + "issueSeverityDistribution": { + "critical": 4, + "gas": 8, + "high": 4, + "informational": 5, + "low": 8, + "medium": 1 + } + }, + "contractsData": [ + { + "address": "0x59C7C832e96D2568bea6db468C1aAdcbbDa08A52", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x59C7C832e96D2568bea6db468C1aAdcbbDa08A52", + "contract_chain": "base", + "contract_platform": "blockscout", + "contract_url": "https://base.blockscout.com/address/0x59C7C832e96D2568bea6db468C1aAdcbbDa08A52", + "contractname": "AugustusSwapper", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x59C7C832e96D2568bea6db468C1aAdcbbDa08A52/blockscout/base?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 8, + "high": 4, + "informational": 5, + "low": 8, + "medium": 1 + }, + "lines_analyzed_count": 180, + "scan_time_taken": 1, + "score": "3.67", + "score_v2": "73.33", + "threat_score": "73.68" + } + } + }, + { + "address": "0x93aAAe79a53759cD164340E4C8766E4Db5331cD7", + "isVerified": false, + "solidityScanReport": null + }, + { + "address": "0x7e31b336f9e8ba52ba3c4ac861b033ba90900bb3", + "isVerified": false, + "solidityScanReport": null + }, + { + "address": "0x9aaB4B24541af30fD72784ED98D8756ac0eFb3C7", + "isVerified": false, + "solidityScanReport": null + } + ] + } + } + }, + { + "appName": "mean-finance", + "doc": "https://docs.mean.finance/guides/smart-contract-registry", + "chainsData": { + "1": { + "overallInfo": { + "verifiedNumber": 4, + "totalContractsNumber": 6, + "solidityScanContractsNumber": 4, + "securityScore": 61.36750000000001, + "issueSeverityDistribution": { + "critical": 6, + "gas": 25, + "high": 1, + "informational": 10, + "low": 20, + "medium": 3 + } + }, + "contractsData": [ + { + "address": "0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345", + "isVerified": false, + "solidityScanReport": null + }, + { + "address": "0x20bdAE1413659f47416f769a4B27044946bc9923", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x20bdAE1413659f47416f769a4B27044946bc9923", + "contract_chain": "optimism", + "contract_platform": "blockscout", + "contract_url": "https://optimism.blockscout.com/address/0x20bdAE1413659f47416f769a4B27044946bc9923", + "contractname": "DCAPermissionsManager", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x20bdAE1413659f47416f769a4B27044946bc9923/blockscout/eth?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 2, + "gas": 22, + "high": 0, + "informational": 8, + "low": 11, + "medium": 3 + }, + "lines_analyzed_count": 314, + "scan_time_taken": 1, + "score": "3.87", + "score_v2": "77.39", + "threat_score": "88.89" + } + } + }, + { + "address": "0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE", + "isVerified": false, + "solidityScanReport": null + }, + { + "address": "0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b", + "contractname": "DCAHubPositionDescriptor", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b/blockscout/eth?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 1, + "high": 1, + "informational": 2, + "low": 3, + "medium": 0 + }, + "lines_analyzed_count": 280, + "scan_time_taken": 1, + "score": "4.77", + "score_v2": "95.36", + "threat_score": "100.00" + } + } + }, + { + "address": "0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9", + "contractname": "DCAHubCompanion", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9/blockscout/eth?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 1, + "high": 0, + "informational": 0, + "low": 3, + "medium": 0 + }, + "lines_analyzed_count": 11, + "scan_time_taken": 0, + "score": "1.82", + "score_v2": "36.36", + "threat_score": "100.00" + } + } + }, + { + "address": "0x5ad2fED59E8DF461c6164c31B4267Efb7cBaF9C0", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x5ad2fED59E8DF461c6164c31B4267Efb7cBaF9C0", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0x5ad2fED59E8DF461c6164c31B4267Efb7cBaF9C0", + "contractname": "DCAHubCompanion", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x5ad2fED59E8DF461c6164c31B4267Efb7cBaF9C0/blockscout/eth?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 1, + "high": 0, + "informational": 0, + "low": 3, + "medium": 0 + }, + "lines_analyzed_count": 11, + "scan_time_taken": 0, + "score": "1.82", + "score_v2": "36.36", + "threat_score": "100.00" + } + } + } + ] + }, + "10": { + "overallInfo": { + "verifiedNumber": 5, + "totalContractsNumber": 6, + "solidityScanContractsNumber": 5, + "securityScore": 66.986, + "issueSeverityDistribution": { + "critical": 6, + "gas": 26, + "high": 1, + "informational": 10, + "low": 23, + "medium": 3 + } + }, + "contractsData": [ + { + "address": "0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345", + "contract_chain": "optimism", + "contract_platform": "blockscout", + "contract_url": "https://optimism.blockscout.com/address/0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345", + "contractname": "DCAHub", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345/blockscout/optimism?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 1, + "high": 0, + "informational": 0, + "low": 3, + "medium": 0 + }, + "lines_analyzed_count": 23, + "scan_time_taken": 0, + "score": "3.48", + "score_v2": "69.57", + "threat_score": "94.44" + } + } + }, + { + "address": "0x20bdAE1413659f47416f769a4B27044946bc9923", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x20bdAE1413659f47416f769a4B27044946bc9923", + "contract_chain": "optimism", + "contract_platform": "blockscout", + "contract_url": "https://optimism.blockscout.com/address/0x20bdAE1413659f47416f769a4B27044946bc9923", + "contractname": "DCAPermissionsManager", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x20bdAE1413659f47416f769a4B27044946bc9923/blockscout/optimism?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 2, + "gas": 22, + "high": 0, + "informational": 8, + "low": 11, + "medium": 3 + }, + "lines_analyzed_count": 314, + "scan_time_taken": 1, + "score": "3.87", + "score_v2": "77.39", + "threat_score": "88.89" + } + } + }, + { + "address": "0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE", + "contract_chain": "optimism", + "contract_platform": "blockscout", + "contract_url": "https://optimism.blockscout.com/address/0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE", + "contractname": "DCAHubCompanion", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE/blockscout/optimism?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 1, + "high": 0, + "informational": 0, + "low": 3, + "medium": 0 + }, + "lines_analyzed_count": 16, + "scan_time_taken": 0, + "score": "2.81", + "score_v2": "56.25", + "threat_score": "100.00" + } + } + }, + { + "address": "0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b", + "contractname": "DCAHubPositionDescriptor", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b/blockscout/optimism?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 1, + "high": 1, + "informational": 2, + "low": 3, + "medium": 0 + }, + "lines_analyzed_count": 280, + "scan_time_taken": 1, + "score": "4.77", + "score_v2": "95.36", + "threat_score": "100.00" + } + } + }, + { + "address": "0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9", + "contract_chain": "optimism", + "contract_platform": "blockscout", + "contract_url": "https://optimism.blockscout.com/address/0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9", + "contractname": "DCAHubCompanion", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9/blockscout/optimism?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 1, + "high": 0, + "informational": 0, + "low": 3, + "medium": 0 + }, + "lines_analyzed_count": 11, + "scan_time_taken": 0, + "score": "1.82", + "score_v2": "36.36", + "threat_score": "100.00" + } + } + }, + { + "address": "0x5ad2fED59E8DF461c6164c31B4267Efb7cBaF9C0", + "isVerified": false, + "solidityScanReport": null + } + ] + }, + "8453": { + "overallInfo": { + "verifiedNumber": 4, + "totalContractsNumber": 6, + "solidityScanContractsNumber": 4, + "securityScore": 74.88, + "issueSeverityDistribution": { + "critical": 6, + "gas": 25, + "high": 1, + "informational": 7, + "low": 20, + "medium": 3 + } + }, + "contractsData": [ + { + "address": "0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345", + "contract_chain": "base", + "contract_platform": "blockscout", + "contract_url": "https://base.blockscout.com/address/0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345", + "contractname": "DCAHub", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345/blockscout/base?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 1, + "high": 0, + "informational": 0, + "low": 3, + "medium": 0 + }, + "lines_analyzed_count": 23, + "scan_time_taken": 0, + "score": "3.48", + "score_v2": "69.57", + "threat_score": "94.44" + } + } + }, + { + "address": "0x20bdAE1413659f47416f769a4B27044946bc9923", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x20bdAE1413659f47416f769a4B27044946bc9923", + "contract_chain": "base", + "contract_platform": "blockscout", + "contract_url": "https://base.blockscout.com/address/0x20bdAE1413659f47416f769a4B27044946bc9923", + "contractname": "DCAPermissionsManager", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x20bdAE1413659f47416f769a4B27044946bc9923/blockscout/base?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 2, + "gas": 22, + "high": 0, + "informational": 5, + "low": 11, + "medium": 3 + }, + "lines_analyzed_count": 314, + "scan_time_taken": 1, + "score": "3.92", + "score_v2": "78.34", + "threat_score": "88.89" + } + } + }, + { + "address": "0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE", + "contract_chain": "base", + "contract_platform": "blockscout", + "contract_url": "https://base.blockscout.com/address/0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE", + "contractname": "DCAHubCompanion", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE/blockscout/base?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 1, + "high": 0, + "informational": 0, + "low": 3, + "medium": 0 + }, + "lines_analyzed_count": 16, + "scan_time_taken": 0, + "score": "2.81", + "score_v2": "56.25", + "threat_score": "100.00" + } + } + }, + { + "address": "0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b", + "contractname": "DCAHubPositionDescriptor", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b/blockscout/base?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 1, + "high": 1, + "informational": 2, + "low": 3, + "medium": 0 + }, + "lines_analyzed_count": 280, + "scan_time_taken": 1, + "score": "4.77", + "score_v2": "95.36", + "threat_score": "100.00" + } + } + }, + { + "address": "0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9", + "isVerified": false, + "solidityScanReport": null + }, + { + "address": "0x5ad2fED59E8DF461c6164c31B4267Efb7cBaF9C0", + "isVerified": false, + "solidityScanReport": null + } + ] + } + } + }, + { + "appName": "cow-swap", + "doc": "https://docs.cow.fi/cow-protocol/reference/contracts/core#deployments", + "chainsData": { + "1": { + "overallInfo": { + "verifiedNumber": 3, + "totalContractsNumber": 3, + "solidityScanContractsNumber": 3, + "securityScore": 87.60000000000001, + "issueSeverityDistribution": { + "critical": 4, + "gas": 18, + "high": 0, + "informational": 13, + "low": 14, + "medium": 3 + } + }, + "contractsData": [ + { + "address": "0x9008D19f58AAbD9eD0D60971565AA8510560ab41", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x9008D19f58AAbD9eD0D60971565AA8510560ab41", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0x9008D19f58AAbD9eD0D60971565AA8510560ab41", + "contractname": "GPv2Settlement", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x9008D19f58AAbD9eD0D60971565AA8510560ab41/blockscout/eth?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 16, + "high": 0, + "informational": 7, + "low": 5, + "medium": 3 + }, + "lines_analyzed_count": 493, + "scan_time_taken": 1, + "score": "4.57", + "score_v2": "91.48", + "threat_score": "94.74" + } + } + }, + { + "address": "0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE", + "contractname": "EIP173Proxy", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE/blockscout/eth?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 0, + "high": 0, + "informational": 4, + "low": 5, + "medium": 0 + }, + "lines_analyzed_count": 94, + "scan_time_taken": 0, + "score": "4.26", + "score_v2": "85.11", + "threat_score": "88.89" + } + } + }, + { + "address": "0xC92E8bdf79f0507f65a392b0ab4667716BFE0110", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0xC92E8bdf79f0507f65a392b0ab4667716BFE0110", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0xC92E8bdf79f0507f65a392b0ab4667716BFE0110", + "contractname": "GPv2VaultRelayer", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0xC92E8bdf79f0507f65a392b0ab4667716BFE0110/blockscout/eth?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 2, + "high": 0, + "informational": 2, + "low": 4, + "medium": 0 + }, + "lines_analyzed_count": 87, + "scan_time_taken": 0, + "score": "4.31", + "score_v2": "86.21", + "threat_score": "94.74" + } + } + } + ] + }, + "100": { + "overallInfo": { + "verifiedNumber": 3, + "totalContractsNumber": 3, + "solidityScanContractsNumber": 3, + "securityScore": 87.60000000000001, + "issueSeverityDistribution": { + "critical": 4, + "gas": 18, + "high": 0, + "informational": 13, + "low": 14, + "medium": 3 + } + }, + "contractsData": [ + { + "address": "0x9008D19f58AAbD9eD0D60971565AA8510560ab41", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x9008D19f58AAbD9eD0D60971565AA8510560ab41", + "contract_chain": "gnosis", + "contract_platform": "blockscout", + "contract_url": "https://gnosis.blockscout.com/address/0x9008D19f58AAbD9eD0D60971565AA8510560ab41", + "contractname": "GPv2Settlement", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x9008D19f58AAbD9eD0D60971565AA8510560ab41/blockscout/gnosis?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 16, + "high": 0, + "informational": 7, + "low": 5, + "medium": 3 + }, + "lines_analyzed_count": 493, + "scan_time_taken": 1, + "score": "4.57", + "score_v2": "91.48", + "threat_score": "94.74" + } + } + }, + { + "address": "0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE", + "contractname": "EIP173Proxy", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE/blockscout/gnosis?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 0, + "high": 0, + "informational": 4, + "low": 5, + "medium": 0 + }, + "lines_analyzed_count": 94, + "scan_time_taken": 0, + "score": "4.26", + "score_v2": "85.11", + "threat_score": "88.89" + } + } + }, + { + "address": "0xC92E8bdf79f0507f65a392b0ab4667716BFE0110", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0xC92E8bdf79f0507f65a392b0ab4667716BFE0110", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0xC92E8bdf79f0507f65a392b0ab4667716BFE0110", + "contractname": "GPv2VaultRelayer", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0xC92E8bdf79f0507f65a392b0ab4667716BFE0110/blockscout/gnosis?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 2, + "high": 0, + "informational": 2, + "low": 4, + "medium": 0 + }, + "lines_analyzed_count": 87, + "scan_time_taken": 0, + "score": "4.31", + "score_v2": "86.21", + "threat_score": "94.74" + } + } + } + ] + } + } + } +] diff --git a/deploy/values/main/values.yaml b/deploy/values/main/values.yaml index 493941ad0a..8504a27e31 100644 --- a/deploy/values/main/values.yaml +++ b/deploy/values/main/values.yaml @@ -4,9 +4,9 @@ imagePullSecrets: - name: regcred config: network: - id: 5 - name: Göerli - shortname: Göerli + id: "11155111" + name: Sepolia + shortname: Sepolia currency: name: Ether symbol: ETH @@ -54,11 +54,10 @@ blockscout: cpu: "3" # Blockscout environment variables env: - BLOCKSCOUT_VERSION: v5.3.0-beta ETHEREUM_JSONRPC_VARIANT: geth HEART_BEAT_TIMEOUT: 30 SUBNETWORK: Ethereum - NETWORK: (Goerli) + NETWORK: (Sepolia) NETWORK_ICON: _network_icon.html LOGO: /images/goerli_logo.svg TXS_STATS_DAYS_TO_COMPILE_AT_INIT: 1 @@ -83,10 +82,10 @@ blockscout: INDEXER_TOKEN_BALANCES_CONCURRENCY: 4 DISABLE_EXCHANGE_RATES: 'true' DISABLE_INDEXER: 'false' - FIRST_BLOCK: '8739119' - LAST_BLOCK: '8739119' - TRACE_FIRST_BLOCK: '8739119' - TRACE_LAST_BLOCK: '8739119' + FIRST_BLOCK: '5780052' + LAST_BLOCK: '5780052' + TRACE_FIRST_BLOCK: '5780052' + TRACE_LAST_BLOCK: '5780052' envFromSecret: ETHEREUM_JSONRPC_TRACE_URL: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/ETHEREUM_JSONRPC_TRACE_URL ETHEREUM_JSONRPC_HTTP_URL: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/ETHEREUM_JSONRPC_HTTP_URL @@ -102,7 +101,6 @@ blockscout: ACCOUNT_CLOAK_KEY: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/ACCOUNT_CLOAK_KEY SECRET_KEY_BASE: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SECRET_KEY_BASE DATABASE_URL: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/DATABASE_URL - DATABASE_READ_ONLY_API_URL: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/DATABASE_READ_ONLY_API_URL API_SENSITIVE_ENDPOINTS_KEY: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/API_SENSITIVE_ENDPOINTS_KEY ACCOUNT_DATABASE_URL: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/ACCOUNT_DATABASE_URL ACCOUNT_REDIS_URL: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/ACCOUNT_REDIS_URL @@ -141,11 +139,11 @@ frontend: env: # ui config - NEXT_PUBLIC_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/eth-goerli.json - 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_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/eth-sepolia.json + NEXT_PUBLIC_NETWORK_EXPLORERS: "[{'title':'Etherscan','baseUrl':'https://sepolia.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}, {'title':'Tenderly','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/sepolia'}} ]" # network config - 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_NETWORK_LOGO: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/sepolia.svg + NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/sepolia.svg NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: validation NEXT_PUBLIC_MARKETPLACE_ENABLED: true @@ -159,14 +157,14 @@ frontend: 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_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout - NEXT_PUBLIC_NETWORK_RPC_URL: https://rpc.ankr.com/eth_goerli + NEXT_PUBLIC_NETWORK_RPC_URL: https://sepolia.drpc.org NEXT_PUBLIC_HOMEPAGE_CHARTS: "['daily_txs','coin_price','market_cap']" - 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_CONFIG_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-sepolia.json + #NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace-categories/default.json NEXT_PUBLIC_GRAPHIQL_TRANSACTION: 0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d NEXT_PUBLIC_WEB3_WALLETS: "['token_pocket','coinbase','metamask']" - 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'}]" - 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_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'}]" + NEXT_PUBLIC_CONTRACT_CODE_IDES: "[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout=eth-sepolia.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]" NEXT_PUBLIC_SWAP_BUTTON_URL: uniswap envFromSecret: NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN diff --git a/deploy/values/review-l2/values.yaml.gotmpl b/deploy/values/review-l2/values.yaml.gotmpl index 4aa3eb1b53..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 diff --git a/deploy/values/review/values.yaml.gotmpl b/deploy/values/review/values.yaml.gotmpl index dc8459129e..e738ae3e63 100644 --- a/deploy/values/review/values.yaml.gotmpl +++ b/deploy/values/review/values.yaml.gotmpl @@ -4,7 +4,7 @@ imagePullSecrets: - name: regcred config: network: - id: 5 + id: 11155111 name: Blockscout shortname: Blockscout currency: @@ -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 @@ -48,24 +50,30 @@ frontend: NEXT_PUBLIC_APP_ENV: development NEXT_PUBLIC_APP_INSTANCE: review NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: validation - 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_STATS_API_HOST: https://stats-goerli.k8s-dev.blockscout.com/ + NEXT_PUBLIC_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-sepolia.json + NEXT_PUBLIC_NETWORK_LOGO: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/sepolia.svg + NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/sepolia.png + NEXT_PUBLIC_API_HOST: eth-sepolia.k8s-dev.blockscout.com + NEXT_PUBLIC_STATS_API_HOST: https://stats-sepolia.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_METADATA_SERVICE_API_HOST: https://metadata-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_MARKETPLACE_SECURITY_REPORTS_URL: https://gist.githubusercontent.com/maxaleks/ce5c7e3de53e8f5b240b88265daf5839/raw/328383c958a8f7ecccf6d50c953bcdf8ab3faa0a/security_reports_goerli_test.json NEXT_PUBLIC_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout NEXT_PUBLIC_HOMEPAGE_CHARTS: "['daily_txs','coin_price','market_cap']" - NEXT_PUBLIC_NETWORK_RPC_URL: https://rpc.ankr.com/eth_goerli + NEXT_PUBLIC_NETWORK_RPC_URL: https://eth-sepolia.public.blastapi.io + NEXT_PUBLIC_NETWORK_ID: '11155111' 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','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/etherscan.png?raw=true','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]" NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace-categories/default.json + NEXT_PUBLIC_MARKETPLACE_FEATURED_APP: zkbob-wallet + NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL: https://gist.githubusercontent.com/maxaleks/36f779fd7d74877b57ec7a25a9a3a6c9/raw/746a8a59454c0537235ee44616c4690ce3bbf3c8/banner.html + NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL: https://www.basename.app NEXT_PUBLIC_GRAPHIQL_TRANSACTION: 0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d NEXT_PUBLIC_WEB3_WALLETS: "['token_pocket','coinbase','metamask']" NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE: gradient_avatar @@ -77,8 +85,15 @@ frontend: 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'}]" 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 NEXT_PUBLIC_SWAP_BUTTON_URL: uniswap NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS: true + NEXT_PUBLIC_AD_BANNER_PROVIDER: getit + NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER: adbutler + NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP: "{ \"id\": \"632019\", \"width\": \"728\", \"height\": \"90\" }" + NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE: "{ \"id\": \"632018\", \"width\": \"320\", \"height\": \"100\" }" + NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED: true + NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED: true 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 @@ -89,4 +104,3 @@ frontend: FAVICON_GENERATOR_API_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN - diff --git a/docs/DEPRECATED_ENVS.md b/docs/DEPRECATED_ENVS.md index b46ea04108..00c6ef1629 100644 --- a/docs/DEPRECATED_ENVS.md +++ b/docs/DEPRECATED_ENVS.md @@ -6,4 +6,5 @@ | 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 +| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` | Set to false if network doesn't have gas tracker | - | `true` | `false` | v1.25.0 | Replaced by NEXT_PUBLIC_GAS_TRACKER_ENABLED | +| NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL | `string` | Network governance token symbol | - | - | `GNO` | v1.29.0 | Replaced by NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL | \ No newline at end of file diff --git a/docs/ENVS.md b/docs/ENVS.md index ac910e2751..1972e42cdf 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -49,9 +49,11 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will - [Transaction interpretation](ENVS.md#transaction-interpretation) - [Verified tokens info](ENVS.md#verified-tokens-info) - [Name service integration](ENVS.md#name-service-integration) + - [Metadata service integration](ENVS.md#metadata-service-integration) - [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) @@ -72,6 +74,8 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will ## Blockchain parameters +*Note!* The `NEXT_PUBLIC_NETWORK_CURRENCY` variables represent the blockchain's native token used for paying transaction fees. `NEXT_PUBLIC_NETWORK_SECONDARY_COIN` variables refer to tokens like protocol-specific tokens (e.g., OP token on Optimism chain) or governance tokens (e.g., GNO on Gnosis chain). + | Variable | Type| Description | Compulsoriness | Default value | Example value | | --- | --- | --- | --- | --- | --- | | NEXT_PUBLIC_NETWORK_NAME | `string` | Displayed name of the network | Required | - | `Gnosis Chain` | @@ -82,7 +86,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will | NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME | `string` | Name of network currency subdenomination | - | `wei` | `duck` | | NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | Network currency symbol | - | - | `ETH` | | NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS | `string` | Network currency decimals | - | `18` | `6` | -| NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL | `string` | Network governance token symbol | - | - | `GNO` | +| NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL | `string` | Network secondary coin symbol. | - | - | `GNO` | | NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE | `validation` or `mining` | Verification type in the network | - | `mining` | `validation` | | NEXT_PUBLIC_IS_TESTNET | `boolean`| Set to true if network is testnet | - | `false` | `true` | @@ -106,7 +110,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will | Variable | Type| Description | Compulsoriness | Default value | Example value | | --- | --- | --- | --- | --- | --- | -| 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_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'secondary_coin_price' \| 'market_cap' \| 'tvl'>` | List of charts displayed on the home page | - | - | `['daily_txs','coin_price','market_cap']` | | 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_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` | @@ -168,13 +172,15 @@ By default, the app has generic favicon. You can override this behavior by provi ### Meta -Settings for meta tags and OG tags +Settings for meta tags, OG tags and SEO | Variable | Type| Description | Compulsoriness | Default value | Example value | | --- | --- | --- | --- | --- | --- | | NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE | `boolean` | Set to `true` to promote Blockscout in meta and OG titles | - | `true` | `true` | | NEXT_PUBLIC_OG_DESCRIPTION | `string` | Custom OG description | - | - | `Blockscout is the #1 open-source blockchain explorer available today. 100+ chains and counting rely on Blockscout data availability, APIs, and ecosystem tools to support their networks.` | | NEXT_PUBLIC_OG_IMAGE_URL | `string` | OG image url. Minimum image size is 200 x 20 pixels (recommended: 1200 x 600); maximum supported file size is 8 MB; 2:1 aspect ratio; supported formats: image/jpeg, image/gif, image/png | - | `static/og_placeholder.png` | `https://placekitten.com/1200/600` | +| NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED | `boolean` | Set to `true` to populate OG tags (title, description) with API data for social preview robot requests | - | `false` | `true` | +| NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED | `boolean` | Set to `true` to pre-render page titles (e.g Token page) on the server side and inject page h1-tag to the markup before it is sent to the browser. | - | `false` | `true` |   @@ -194,6 +200,8 @@ Settings for meta tags and OG tags | `total_reward` | Total block reward | | `nonce` | Block nonce | | `miner` | Address of block's miner or validator | +| `L1_status` | Short interpretation of the batch lifecycle (applicable for Rollup chains) | +| `batch` | Batch index (applicable for Rollup chains) |   @@ -201,7 +209,7 @@ Settings for meta tags and OG tags | Variable | Type | Description | Compulsoriness | Default value | Example value | | --- | --- | --- | --- | --- | --- | -| NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE | `"github" \| "jazzicon" \| "gradient_avatar" \| "blockie"` | Style of address identicon appearance. Choose between [GitHub](https://github.blog/2013-08-14-identicons/), [Metamask Jazzicon](https://metamask.github.io/jazzicon/), [Gradient Avatar](https://github.com/varld/gradient-avatar) and [Ethereum Blocky](https://mycryptohq.github.io/ethereum-blockies-base64/) | - | `jazzicon` | `gradient_avatar` | +| NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE | `"github" \| "jazzicon" \| "gradient_avatar" \| "blockie"` | Default style of address identicon appearance. Choose between [GitHub](https://github.blog/2013-08-14-identicons/), [Metamask Jazzicon](https://metamask.github.io/jazzicon/), [Gradient Avatar](https://github.com/varld/gradient-avatar) and [Ethereum Blocky](https://mycryptohq.github.io/ethereum-blockies-base64/) | - | `jazzicon` | `gradient_avatar` | | NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS | `Array` | Address views that should not be displayed. See below the list of the possible id values. | - | - | `'["top_accounts"]'` | | NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED | `boolean` | Set to `true` if SolidityScan reports are supported | - | - | `true` | @@ -228,6 +236,8 @@ Settings for meta tags and OG tags | `tx_fee` | Total transaction fee | | `gas_fees` | Gas fees breakdown | | `burnt_fees` | Amount of native coin burnt for transaction | +| `L1_status` | Short interpretation of the batch lifecycle (applicable for Rollup chains) | +| `batch` | Batch index (applicable for Rollup chains) | ##### Transaction additional fields list | Id | Description | @@ -344,7 +354,8 @@ 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_BANNER_ADDITIONAL_PROVIDER | `adbutler` | Additional ads provider to mix with the main one | - | - | `adbutler` | | 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'}` | @@ -381,9 +392,9 @@ 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' \| 'shibarium' \| 'zkEvm' ` | Rollup chain type | Required | - | `'optimistic'` | +| NEXT_PUBLIC_ROLLUP_TYPE | `'optimistic' \| 'shibarium' \| 'zkEvm' \| 'zkSync' ` | 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` | +| NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL | `string` | URL for L2 -> L1 withdrawals | Required only for `optimistic` rollups | - | `https://app.optimism.io/bridge/withdraw` |   @@ -448,6 +459,10 @@ This feature is **always enabled**, but you can configure its behavior by passin | 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 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` | +| NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL | `string` | URL of configuration file (`.json` format only) which contains app security reports for displaying security scores on the Marketplace page | - | - | `https://example.com/marketplace_security_reports.json` | +| NEXT_PUBLIC_MARKETPLACE_FEATURED_APP | `string` | ID of the featured application to be displayed on the banner on the Marketplace page | - | - | `uniswap` | +| NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL | `string` | URL of the banner HTML content | - | - | `https://example.com/banner` | +| NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL | `string` | URL of the page the banner leads to | - | - | `https://example.com` | #### Marketplace app configuration properties @@ -491,6 +506,7 @@ For each application, you need to specify the `MarketplaceCategoryId` to which i | Variable | Type| Description | Compulsoriness | Default value | Example value | | --- | --- | --- | --- | --- | --- | | NEXT_PUBLIC_VISUALIZE_API_HOST | `string` | Visualize API endpoint url | Required | - | `https://visualizer.services.blockscout.com` | +| NEXT_PUBLIC_VISUALIZE_API_BASE_PATH | `string` | Base path for Visualize API endpoint url | - | - | `/poa/core` |   @@ -498,7 +514,8 @@ For each application, you need to specify the `MarketplaceCategoryId` to which i | Variable | Type| Description | Compulsoriness | Default value | Example value | | --- | --- | --- | --- | --- | --- | -| NEXT_PUBLIC_STATS_API_HOST | `string` | API endpoint url | Required | - | `https://stats.services.blockscout.com` | +| NEXT_PUBLIC_STATS_API_HOST | `string` | Stats API endpoint url | Required | - | `https://stats.services.blockscout.com` | +| NEXT_PUBLIC_STATS_API_BASE_PATH | `string` | Base path for Stats API endpoint url | - | - | `/poa/core` |   @@ -517,7 +534,7 @@ This feature is **enabled by default** with the `['metamask']` value. To switch | Variable | Type| Description | Compulsoriness | Default value | Example value | | --- | --- | --- | --- | --- | --- | -| NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER | `blockscout` \| `none` | Transaction interpretation provider that displays human readable transaction description | - | `none` | `blockscout` | +| NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER | `blockscout` \| `noves` \| `none` | Transaction interpretation provider that displays human readable transaction description | - | `none` | `blockscout` |   @@ -539,6 +556,26 @@ This feature allows resolving blockchain addresses using human-readable domain n   +### Metadata service integration + +This feature allows name tags and other public tags for addresses. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | +| --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_METADATA_SERVICE_API_HOST | `string` | Metadata Service API endpoint url | Required | - | `https://metadata.services.blockscout.com` | + +  + +### Data Availability + +This feature enables views related to blob transactions (EIP-4844), such as the Blob Txns tab on the Transactions page and the Blob details page. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | +| --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED | `boolean` | Set to true to enable blob transactions views. | Required | - | `true` | + +  + ### Bridged tokens This feature allows users to view tokens that have been bridged from other EVM chains. Additional tab "Bridged" will be added to the tokens page and the link to original token will be displayed on the token page. @@ -571,7 +608,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` |   @@ -585,6 +626,16 @@ 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. diff --git a/global.d.ts b/global.d.ts index 2955f3f872..1632505b52 100644 --- a/global.d.ts +++ b/global.d.ts @@ -1,4 +1,4 @@ -import type { WindowProvider } from 'wagmi'; +import type { WalletProvider } from 'types/web3'; type CPreferences = { zone: string; @@ -8,7 +8,7 @@ type CPreferences = { declare global { export interface Window { - ethereum?: WindowProvider; + ethereum?: WalletProvider | undefined; coinzilla_display: Array; ga?: { getAll: () => Array<{ get: (prop: string) => string }>; @@ -27,3 +27,5 @@ declare global { } } } + +export {}; diff --git a/icons/apps_list.svg b/icons/apps_list.svg new file mode 100644 index 0000000000..62cb5020d6 --- /dev/null +++ b/icons/apps_list.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/apps_xs.svg b/icons/apps_xs.svg new file mode 100644 index 0000000000..4daa74955d --- /dev/null +++ b/icons/apps_xs.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/beta.svg b/icons/beta.svg new file mode 100644 index 0000000000..bba1309f3a --- /dev/null +++ b/icons/beta.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/beta_xs.svg b/icons/beta_xs.svg new file mode 100644 index 0000000000..a6dc48ee4e --- /dev/null +++ b/icons/beta_xs.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/blob.svg b/icons/blob.svg new file mode 100644 index 0000000000..9b40d72ecb --- /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/contracts.svg b/icons/contracts.svg new file mode 100644 index 0000000000..1f0b62afd2 --- /dev/null +++ b/icons/contracts.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/icons/contracts_verified.svg b/icons/contracts_verified.svg new file mode 100644 index 0000000000..2a004f596d --- /dev/null +++ b/icons/contracts_verified.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/icons/empty_search_result.svg b/icons/empty_search_result.svg index a60b3b1e70..f4d62eff0e 100644 --- a/icons/empty_search_result.svg +++ b/icons/empty_search_result.svg @@ -1,11 +1,16 @@ - - - + + + + + + - - - + + + + + diff --git a/icons/gas_xl.svg b/icons/gas_xl.svg index a3c436b5d4..5a3913ac16 100644 --- a/icons/gas_xl.svg +++ b/icons/gas_xl.svg @@ -1,3 +1,3 @@ - + diff --git a/icons/gear_slim.svg b/icons/gear_slim.svg new file mode 100644 index 0000000000..abc14e6a78 --- /dev/null +++ b/icons/gear_slim.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/lightning.svg b/icons/lightning.svg index 91b1ae92ca..03fea73d75 100644 --- a/icons/lightning.svg +++ b/icons/lightning.svg @@ -1,3 +1,3 @@ - - + + diff --git a/icons/social/tweet.svg b/icons/social/tweet.svg deleted file mode 100644 index 20cc63ccc6..0000000000 --- a/icons/social/tweet.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/icons/social/twitter.svg b/icons/social/twitter.svg new file mode 100644 index 0000000000..21e9812ff7 --- /dev/null +++ b/icons/social/twitter.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/social/twitter_filled.svg b/icons/social/twitter_filled.svg index 5fc356a969..0d73b850a0 100644 --- a/icons/social/twitter_filled.svg +++ b/icons/social/twitter_filled.svg @@ -1,3 +1,3 @@ - - + + diff --git a/icons/star_filled.svg b/icons/star_filled.svg index 10a3cfb0c8..2bdea23a41 100644 --- a/icons/star_filled.svg +++ b/icons/star_filled.svg @@ -1,3 +1,3 @@ - - + + diff --git a/icons/star_outline.svg b/icons/star_outline.svg index e6fd05339c..bf2eca9845 100644 --- a/icons/star_outline.svg +++ b/icons/star_outline.svg @@ -1,3 +1,3 @@ - - + + diff --git a/icons/status/pending.svg b/icons/status/pending.svg index a8c19c187b..f9e5a88d53 100644 --- a/icons/status/pending.svg +++ b/icons/status/pending.svg @@ -1,4 +1,5 @@ - - - + + + + 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/vertical_dots.svg b/icons/vertical_dots.svg new file mode 100644 index 0000000000..0d4cf417f4 --- /dev/null +++ b/icons/vertical_dots.svg @@ -0,0 +1,3 @@ + + + diff --git a/jest/lib.tsx b/jest/lib.tsx index 7097e2b19a..823e4b1f6e 100644 --- a/jest/lib.tsx +++ b/jest/lib.tsx @@ -15,12 +15,9 @@ import 'lib/setLocale'; const PAGE_PROPS = { cookies: '', referrer: '', - id: '', - height_or_hash: '', - hash: '', - number: '', - q: '', - name: '', + query: {}, + adBannerProvider: undefined, + apiData: null, }; const TestApp = ({ children }: {children: React.ReactNode}) => { diff --git a/lib/address/parseMetaPayload.ts b/lib/address/parseMetaPayload.ts new file mode 100644 index 0000000000..ad5e8d401a --- /dev/null +++ b/lib/address/parseMetaPayload.ts @@ -0,0 +1,36 @@ +import type { AddressMetadataTag } from 'types/api/addressMetadata'; +import type { AddressMetadataTagFormatted } from 'types/client/addressMetadata'; + +type MetaParsed = NonNullable; + +export default function parseMetaPayload(meta: AddressMetadataTag['meta']): AddressMetadataTagFormatted['meta'] { + try { + const parsedMeta = JSON.parse(meta || ''); + + if (typeof parsedMeta !== 'object' || parsedMeta === null || Array.isArray(parsedMeta)) { + throw new Error('Invalid JSON'); + } + + const result: AddressMetadataTagFormatted['meta'] = {}; + + const stringFields: Array = [ + 'textColor', + 'bgColor', + 'tagUrl', + 'tooltipIcon', + 'tooltipTitle', + 'tooltipDescription', + 'tooltipUrl', + ]; + + for (const stringField of stringFields) { + if (stringField in parsedMeta && typeof parsedMeta[stringField as keyof typeof parsedMeta] === 'string') { + result[stringField] = parsedMeta[stringField as keyof typeof parsedMeta]; + } + } + + return result; + } catch (error) { + return null; + } +} diff --git a/lib/address/useAddressMetadataInfoQuery.ts b/lib/address/useAddressMetadataInfoQuery.ts new file mode 100644 index 0000000000..2268e8636b --- /dev/null +++ b/lib/address/useAddressMetadataInfoQuery.ts @@ -0,0 +1,47 @@ +import { useQuery } from '@tanstack/react-query'; + +import type { AddressMetadataInfo } from 'types/api/addressMetadata'; +import type { AddressMetadataInfoFormatted, AddressMetadataTagFormatted } from 'types/client/addressMetadata'; + +import config from 'configs/app'; +import useApiFetch from 'lib/api/useApiFetch'; +import { getResourceKey } from 'lib/api/useApiQuery'; + +import parseMetaPayload from './parseMetaPayload'; + +export default function useAddressMetadataInfoQuery(addresses: Array) { + + const apiFetch = useApiFetch(); + + const queryParams = { + addresses, + chainId: config.chain.id, + tagsLimit: '20', + }; + const resource = 'address_metadata_info'; + + // TODO @tom2drum: Improve the typing here + // since we are formatting the API data in the select function here + // we cannot use the useApiQuery hook because of its current typing + // enhance useApiQuery so it can accept an API data and the formatted data types + return useQuery({ + queryKey: getResourceKey(resource, { queryParams }), + queryFn: async() => { + return apiFetch(resource, { queryParams }) as Promise; + }, + enabled: addresses.length > 0 && config.features.addressMetadata.isEnabled, + select: (data) => { + const addresses = Object.entries(data.addresses) + .map(([ address, { tags, reputation } ]) => { + const formattedTags: Array = tags.map((tag) => ({ ...tag, meta: parseMetaPayload(tag.meta) })); + return [ address.toLowerCase(), { tags: formattedTags, reputation } ] as const; + }) + .reduce((result, item) => { + result[item[0]] = item[1]; + return result; + }, {} as AddressMetadataInfoFormatted['addresses']); + + return { addresses }; + }, + }); +} diff --git a/lib/api/resources.ts b/lib/api/resources.ts index fd6986e98f..f0a359c5c2 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -29,10 +29,13 @@ import type { AddressNFTsResponse, AddressCollectionsResponse, AddressNFTTokensFilter, + AddressCoinBalanceHistoryChartOld, } from 'types/api/address'; import type { AddressesResponse } from 'types/api/addresses'; +import type { AddressMetadataInfo } from 'types/api/addressMetadata'; +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 { ChartMarketResponse, ChartSecondaryCoinPriceResponse, ChartTransactionResponse } from 'types/api/charts'; import type { BackendVersionConfig } from 'types/api/configs'; import type { SmartContract, @@ -55,6 +58,7 @@ import type { import type { IndexingStatus } from 'types/api/indexingStatus'; import type { InternalTransactionsResponse } from 'types/api/internalTransaction'; import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log'; +import type { NovesAccountHistoryResponse, NovesDescribeTxsResponse, NovesResponseData } from 'types/api/noves'; import type { OptimisticL2DepositsResponse, OptimisticL2DepositsItem, @@ -84,16 +88,26 @@ import type { Transaction, TransactionsResponseWatchlist, TransactionsSorting, + TransactionsResponseWithBlobs, + TransactionsStats, } 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'; -import type { ZkEvmL2TxnBatch, ZkEvmL2TxnBatchesItem, ZkEvmL2TxnBatchesResponse, ZkEvmL2TxnBatchTxs } from 'types/api/zkEvmL2'; +import type { + ZkEvmL2DepositsResponse, + ZkEvmL2TxnBatch, + ZkEvmL2TxnBatchesItem, + ZkEvmL2TxnBatchesResponse, + ZkEvmL2TxnBatchTxs, + ZkEvmL2WithdrawalsResponse, +} from 'types/api/zkEvmL2'; +import type { ZkSyncBatch, ZkSyncBatchesResponse, ZkSyncBatchTxs } from 'types/api/zkSyncL2'; import type { MarketplaceAppOverview } from 'types/client/marketplace'; import type { ArrayElement } from 'types/utils'; @@ -231,6 +245,18 @@ export const RESOURCES = { filterFields: [ 'name' as const, 'only_active' as const ], }, + // METADATA SERVICE + address_metadata_info: { + path: '/api/v1/metadata', + endpoint: getFeaturePayload(config.features.addressMetadata)?.api.endpoint, + basePath: getFeaturePayload(config.features.addressMetadata)?.api.basePath, + }, + address_metadata_tag_search: { + path: '/api/v1/tags:search', + endpoint: getFeaturePayload(config.features.addressMetadata)?.api.endpoint, + basePath: getFeaturePayload(config.features.addressMetadata)?.api.basePath, + }, + // VISUALIZATION visualize_sol2uml: { path: '/api/v1/solidity\\:visualize-contracts', @@ -264,13 +290,16 @@ 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', pathParams: [ 'height_or_hash' as const ], filterFields: [], }, + txs_stats: { + path: '/api/v2/transactions/stats', + }, txs_validated: { path: '/api/v2/transactions', filterFields: [ 'filter' as const, 'type' as const, 'method' as const ], @@ -279,6 +308,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: [ ], @@ -316,6 +349,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 ], @@ -527,6 +564,9 @@ export const RESOURCES = { stats_charts_market: { path: '/api/v2/stats/charts/market', }, + stats_charts_secondary_coin_price: { + path: '/api/v2/stats/charts/secondary-coin-market', + }, // HOMEPAGE homepage_blocks: { @@ -550,6 +590,9 @@ export const RESOURCES = { homepage_zkevm_latest_batch: { path: '/api/v2/main-page/zkevm/batches/latest-number', }, + homepage_zksync_latest_batch: { + path: '/api/v2/main-page/zksync/batches/latest-number', + }, // SEARCH quick_search: { @@ -564,43 +607,62 @@ export const RESOURCES = { path: '/api/v2/search/check-redirect', }, - // L2 - l2_deposits: { + // optimistic L2 + optimistic_l2_deposits: { path: '/api/v2/optimism/deposits', filterFields: [], }, - l2_deposits_count: { + optimistic_l2_deposits_count: { path: '/api/v2/optimism/deposits/count', }, - l2_withdrawals: { + optimistic_l2_withdrawals: { path: '/api/v2/optimism/withdrawals', filterFields: [], }, - l2_withdrawals_count: { + optimistic_l2_withdrawals_count: { path: '/api/v2/optimism/withdrawals/count', }, - l2_output_roots: { + optimistic_l2_output_roots: { path: '/api/v2/optimism/output-roots', filterFields: [], }, - l2_output_roots_count: { + optimistic_l2_output_roots_count: { path: '/api/v2/optimism/output-roots/count', }, - l2_txn_batches: { + optimistic_l2_txn_batches: { path: '/api/v2/optimism/txn-batches', filterFields: [], }, - l2_txn_batches_count: { + optimistic_l2_txn_batches_count: { path: '/api/v2/optimism/txn-batches/count', }, + // zkEvm L2 + zkevm_l2_deposits: { + path: '/api/v2/zkevm/deposits', + filterFields: [], + }, + + zkevm_l2_deposits_count: { + path: '/api/v2/zkevm/deposits/count', + }, + + zkevm_l2_withdrawals: { + path: '/api/v2/zkevm/withdrawals', + filterFields: [], + }, + + zkevm_l2_withdrawals_count: { + path: '/api/v2/zkevm/withdrawals/count', + }, + zkevm_l2_txn_batches: { path: '/api/v2/zkevm/batches', filterFields: [], @@ -614,12 +676,34 @@ export const RESOURCES = { path: '/api/v2/zkevm/batches/:number', pathParams: [ 'number' as const ], }, + zkevm_l2_txn_batch_txs: { path: '/api/v2/transactions/zkevm-batch/:number', pathParams: [ 'number' as const ], filterFields: [], }, + // zkSync L2 + zksync_l2_txn_batches: { + path: '/api/v2/zksync/batches', + filterFields: [], + }, + + zksync_l2_txn_batches_count: { + path: '/api/v2/zksync/batches/count', + }, + + zksync_l2_txn_batch: { + path: '/api/v2/zksync/batches/:number', + pathParams: [ 'number' as const ], + }, + + zksync_l2_txn_batch_txs: { + path: '/api/v2/transactions/zksync-batch/:number', + pathParams: [ 'number' as const ], + filterFields: [], + }, + // SHIBARIUM L2 shibarium_deposits: { path: '/api/v2/shibarium/deposits', @@ -639,6 +723,20 @@ export const RESOURCES = { path: '/api/v2/shibarium/withdrawals/count', }, + // NOVES-FI + noves_transaction: { + path: '/api/v2/proxy/noves-fi/transactions/:hash', + pathParams: [ 'hash' as const ], + }, + noves_address_history: { + path: '/api/v2/proxy/noves-fi/addresses/:address/transactions', + pathParams: [ 'address' as const ], + filterFields: [], + }, + noves_describe_txs: { + path: '/api/v2/proxy/noves-fi/transaction-descriptions', + }, + // USER OPS user_ops: { path: '/api/v2/proxy/account-abstraction/operations', @@ -652,23 +750,39 @@ 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' 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: { path: '/api/v2/config/backend-version', }, + // CSV EXPORT + csv_export_token_holders: { + path: '/api/v2/tokens/:hash/holders/csv', + pathParams: [ 'hash' as const ], + }, + // OTHER api_v2_key: { path: '/api/v2/key', @@ -723,8 +837,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' | @@ -732,12 +846,13 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' | 'tokens_bridged' | 'token_instance_transfers' | 'token_instance_holders' | 'verified_contracts' | -'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' | +'optimistic_l2_output_roots' | 'optimistic_l2_withdrawals' | 'optimistic_l2_txn_batches' | 'optimistic_l2_deposits' | 'shibarium_deposits' | 'shibarium_withdrawals' | -'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' | +'zkevm_l2_deposits' | 'zkevm_l2_withdrawals' | 'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' | +'zksync_l2_txn_batches' | 'zksync_l2_txn_batch_txs' | 'withdrawals' | 'address_withdrawals' | 'block_withdrawals' | 'watchlist' | 'private_tags_address' | 'private_tags_tx' | -'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'validators'; +'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'validators' | 'noves_address_history'; export type PaginatedResponse = ResourcePayload; @@ -759,6 +874,7 @@ Q extends 'token_info_applications' ? TokenInfoApplications : Q extends 'stats' ? HomeStats : Q extends 'stats_charts_txs' ? ChartTransactionResponse : Q extends 'stats_charts_market' ? ChartMarketResponse : +Q extends 'stats_charts_secondary_coin_price' ? ChartSecondaryCoinPriceResponse : Q extends 'homepage_blocks' ? Array : Q extends 'homepage_txs' ? Array : Q extends 'homepage_txs_watchlist' ? Array : @@ -766,6 +882,7 @@ Q extends 'homepage_deposits' ? Array : Q extends 'homepage_zkevm_l2_batches' ? { items: Array } : Q extends 'homepage_indexing_status' ? IndexingStatus : Q extends 'homepage_zkevm_latest_batch' ? number : +Q extends 'homepage_zksync_latest_batch' ? number : Q extends 'stats_counters' ? Counters : Q extends 'stats_lines' ? StatsCharts : Q extends 'stats_line' ? StatsChart : @@ -773,8 +890,10 @@ Q extends 'blocks' ? BlocksResponse : Q extends 'block' ? Block : Q extends 'block_txs' ? BlockTransactionsResponse : Q extends 'block_withdrawals' ? BlockWithdrawalsResponse : +Q extends 'txs_stats' ? TransactionsStats : 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 : @@ -783,6 +902,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 : @@ -793,7 +913,7 @@ Q extends 'address_internal_txs' ? AddressInternalTxsResponse : Q extends 'address_token_transfers' ? AddressTokenTransferResponse : Q extends 'address_blocks_validated' ? AddressBlocksValidatedResponse : Q extends 'address_coin_balance' ? AddressCoinBalanceHistoryResponse : -Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart : +Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChartOld | AddressCoinBalanceHistoryChart : Q extends 'address_logs' ? LogsResponseAddress : Q extends 'address_tokens' ? AddressTokensResponse : Q extends 'address_nfts' ? AddressNFTsResponse : @@ -826,26 +946,16 @@ Q extends 'visualize_sol2uml' ? VisualizedContract : Q extends 'contract_verification_config' ? SmartContractVerificationConfig : Q extends 'withdrawals' ? WithdrawalsResponse : Q extends 'withdrawals_counters' ? WithdrawalsCounters : -Q extends 'l2_output_roots' ? OptimisticL2OutputRootsResponse : -Q extends 'l2_withdrawals' ? OptimisticL2WithdrawalsResponse : -Q extends 'l2_deposits' ? OptimisticL2DepositsResponse : -Q extends 'l2_txn_batches' ? OptimisticL2TxnBatchesResponse : -Q extends 'l2_output_roots_count' ? number : -Q extends 'l2_withdrawals_count' ? number : -Q extends 'l2_deposits_count' ? number : -Q extends 'l2_txn_batches_count' ? number : -Q extends 'zkevm_l2_txn_batches' ? ZkEvmL2TxnBatchesResponse : -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 'optimistic_l2_output_roots' ? OptimisticL2OutputRootsResponse : +Q extends 'optimistic_l2_withdrawals' ? OptimisticL2WithdrawalsResponse : +Q extends 'optimistic_l2_deposits' ? OptimisticL2DepositsResponse : +Q extends 'optimistic_l2_txn_batches' ? OptimisticL2TxnBatchesResponse : +Q extends 'optimistic_l2_output_roots_count' ? number : +Q extends 'optimistic_l2_withdrawals_count' ? number : +Q extends 'optimistic_l2_deposits_count' ? number : +Q extends 'optimistic_l2_txn_batches_count' ? number : Q extends '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 : +Q extends 'address_metadata_info' ? AddressMetadataInfo : never; // !!! IMPORTANT !!! // See comment above @@ -853,6 +963,7 @@ 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 : @@ -861,7 +972,30 @@ Q extends 'shibarium_withdrawals' ? ShibariumWithdrawalsResponse : Q extends 'shibarium_deposits' ? ShibariumDepositsResponse : Q extends 'shibarium_withdrawals_count' ? number : Q extends 'shibarium_deposits_count' ? number : +Q extends 'zkevm_l2_deposits' ? ZkEvmL2DepositsResponse : +Q extends 'zkevm_l2_deposits_count' ? number : +Q extends 'zkevm_l2_withdrawals' ? ZkEvmL2WithdrawalsResponse : +Q extends 'zkevm_l2_withdrawals_count' ? number : +Q extends 'zkevm_l2_txn_batches' ? ZkEvmL2TxnBatchesResponse : +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 'zksync_l2_txn_batches' ? ZkSyncBatchesResponse : +Q extends 'zksync_l2_txn_batches_count' ? number : +Q extends 'zksync_l2_txn_batch' ? ZkSyncBatch : +Q extends 'zksync_l2_txn_batch_txs' ? ZkSyncBatchTxs : 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 : +Q extends 'noves_transaction' ? NovesResponseData : +Q extends 'noves_address_history' ? NovesAccountHistoryResponse : +Q extends 'noves_describe_txs' ? NovesDescribeTxsResponse : never; /* eslint-enable @typescript-eslint/indent */ @@ -874,7 +1008,9 @@ export type PaginatedResponseNextPageParams = Q extends /* 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 : 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/contexts/app.tsx b/lib/contexts/app.tsx index ec0a25e61b..256bf0bf56 100644 --- a/lib/contexts/app.tsx +++ b/lib/contexts/app.tsx @@ -1,5 +1,6 @@ import React, { createContext, useContext } from 'react'; +import type { Route } from 'nextjs-routes'; import type { Props as PageProps } from 'nextjs/getServerSideProps'; type Props = { @@ -10,12 +11,9 @@ type Props = { const AppContext = createContext({ cookies: '', referrer: '', - id: '', - height_or_hash: '', - hash: '', - number: '', - q: '', - name: '', + query: {}, + adBannerProvider: undefined, + apiData: null, }); export function AppContextProvider({ children, pageProps }: Props) { @@ -26,6 +24,6 @@ export function AppContextProvider({ children, pageProps }: Props) { ); } -export function useAppContext() { - return useContext(AppContext); +export function useAppContext() { + return useContext>(AppContext); } diff --git a/lib/contexts/marketplace.tsx b/lib/contexts/marketplace.tsx new file mode 100644 index 0000000000..2aba76e39b --- /dev/null +++ b/lib/contexts/marketplace.tsx @@ -0,0 +1,48 @@ +import { useRouter } from 'next/router'; +import React, { createContext, useContext, useEffect, useState, useMemo } from 'react'; + +type Props = { + children: React.ReactNode; +} + +type TMarketplaceContext = { + isAutoConnectDisabled: boolean; + setIsAutoConnectDisabled: (isAutoConnectDisabled: boolean) => void; +} + +const MarketplaceContext = createContext({ + isAutoConnectDisabled: false, + setIsAutoConnectDisabled: () => {}, +}); + +export function MarketplaceContextProvider({ children }: Props) { + const router = useRouter(); + const [ isAutoConnectDisabled, setIsAutoConnectDisabled ] = useState(false); + + useEffect(() => { + const handleRouteChange = () => { + setIsAutoConnectDisabled(false); + }; + + router.events.on('routeChangeStart', handleRouteChange); + + return () => { + router.events.off('routeChangeStart', handleRouteChange); + }; + }, [ router.events ]); + + const value = useMemo(() => ({ + isAutoConnectDisabled, + setIsAutoConnectDisabled, + }), [ isAutoConnectDisabled, setIsAutoConnectDisabled ]); + + return ( + + { children } + + ); +} + +export function useMarketplaceContext() { + return useContext(MarketplaceContext); +} 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/cookies.ts b/lib/cookies.ts index 7f3867f9e6..cef6a38a42 100644 --- a/lib/cookies.ts +++ b/lib/cookies.ts @@ -10,6 +10,7 @@ export enum NAMES { TXS_SORT='txs_sort', COLOR_MODE='chakra-ui-color-mode', COLOR_MODE_HEX='chakra-ui-color-mode-hex', + ADDRESS_IDENTICON_TYPE='address_identicon_type', INDEXING_ALERT='indexing_alert', ADBLOCK_DETECTED='adblock_detected', MIXPANEL_DEBUG='_mixpanel_debug', 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/useAdblockDetect.tsx b/lib/hooks/useAdblockDetect.tsx index e0ee312647..415b150f07 100644 --- a/lib/hooks/useAdblockDetect.tsx +++ b/lib/hooks/useAdblockDetect.tsx @@ -1,15 +1,35 @@ import { useEffect } from 'react'; +import type { AdBannerProviders } from 'types/client/adProviders'; + +import config from 'configs/app'; import { useAppContext } from 'lib/contexts/app'; import * as cookies from 'lib/cookies'; import isBrowser from 'lib/isBrowser'; +const DEFAULT_URL = 'https://request-global.czilladx.com'; + +// in general, detect should work with any ad-provider url (that is alive) +// but we see some false-positive results in certain browsers +const TEST_URLS: Record = { + slise: 'https://v1.slise.xyz/serve', + coinzilla: 'https://request-global.czilladx.com', + adbutler: 'https://servedbyadbutler.com/app.js', + hype: 'https://api.hypelab.com/v1/scripts/hp-sdk.js', + // I don't have an url for getit to test + getit: DEFAULT_URL, + none: DEFAULT_URL, +}; + +const feature = config.features.adsBanner; + export default function useAdblockDetect() { const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED, useAppContext().cookies); + const provider = feature.isEnabled && feature.provider; useEffect(() => { - if (isBrowser() && !hasAdblockCookie) { - const url = 'https://request-global.czilladx.com'; + if (isBrowser() && !hasAdblockCookie && provider) { + const url = TEST_URLS[provider] || DEFAULT_URL; fetch(url, { method: 'HEAD', mode: 'no-cors', diff --git a/lib/hooks/useContractTabs.tsx b/lib/hooks/useContractTabs.tsx index ca02f16ddd..f1be701882 100644 --- a/lib/hooks/useContractTabs.tsx +++ b/lib/hooks/useContractTabs.tsx @@ -1,37 +1,93 @@ +import { useRouter } from 'next/router'; import React from 'react'; import type { Address } from 'types/api/address'; +import useApiQuery from 'lib/api/useApiQuery'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import useSocketChannel from 'lib/socket/useSocketChannel'; +import * as stubs from 'stubs/contract'; import ContractCode from 'ui/address/contract/ContractCode'; import ContractRead from 'ui/address/contract/ContractRead'; import ContractWrite from 'ui/address/contract/ContractWrite'; -export default function useContractTabs(data: Address | undefined) { +const CONTRACT_TAB_IDS = [ + 'contract_code', + 'read_contract', + 'read_proxy', + 'read_custom_methods', + 'write_contract', + 'write_proxy', + 'write_custom_methods', +] as const; + +interface ContractTab { + id: typeof CONTRACT_TAB_IDS[number]; + title: string; + component: JSX.Element; +} + +interface ReturnType { + tabs: Array; + isLoading: boolean; +} + +export default function useContractTabs(data: Address | undefined, isPlaceholderData: boolean): ReturnType { + const [ isQueryEnabled, setIsQueryEnabled ] = React.useState(false); + + const router = useRouter(); + const tab = getQueryParamString(router.query.tab); + + const isEnabled = Boolean(data?.hash) && !isPlaceholderData && CONTRACT_TAB_IDS.concat('contract' as never).includes(tab); + + const enableQuery = React.useCallback(() => { + setIsQueryEnabled(true); + }, []); + + const contractQuery = useApiQuery('contract', { + pathParams: { hash: data?.hash }, + queryOptions: { + enabled: isEnabled && isQueryEnabled, + refetchOnMount: false, + placeholderData: data?.is_verified ? stubs.CONTRACT_CODE_VERIFIED : stubs.CONTRACT_CODE_UNVERIFIED, + }, + }); + + const channel = useSocketChannel({ + topic: `addresses:${ data?.hash?.toLowerCase() }`, + isDisabled: !isEnabled, + onJoin: enableQuery, + onSocketError: enableQuery, + }); + return React.useMemo(() => { - return [ - { id: 'contact_code', title: 'Code', component: }, - // this is not implemented in api yet - // data?.has_decompiled_code ? - // { id: 'contact_decompiled_code', title: 'Decompiled code', component:
Decompiled code
} : - // undefined, - data?.has_methods_read ? - { id: 'read_contract', title: 'Read contract', component: } : - undefined, - data?.has_methods_read_proxy ? - { id: 'read_proxy', title: 'Read proxy', component: } : - undefined, - data?.has_custom_methods_read ? - { id: 'read_custom_methods', title: 'Read custom', component: } : - undefined, - data?.has_methods_write ? - { id: 'write_contract', title: 'Write contract', component: } : - undefined, - data?.has_methods_write_proxy ? - { id: 'write_proxy', title: 'Write proxy', component: } : - undefined, - data?.has_custom_methods_write ? - { id: 'write_custom_methods', title: 'Write custom', component: } : - undefined, - ].filter(Boolean); - }, [ data ]); + return { + tabs: [ + { + id: 'contract_code' as const, + title: 'Code', + component: , + }, + contractQuery.data?.has_methods_read ? + { id: 'read_contract' as const, title: 'Read contract', component: } : + undefined, + contractQuery.data?.has_methods_read_proxy ? + { id: 'read_proxy' as const, title: 'Read proxy', component: } : + undefined, + contractQuery.data?.has_custom_methods_read ? + { id: 'read_custom_methods' as const, title: 'Read custom', component: } : + undefined, + contractQuery.data?.has_methods_write ? + { id: 'write_contract' as const, title: 'Write contract', component: } : + undefined, + contractQuery.data?.has_methods_write_proxy ? + { id: 'write_proxy' as const, title: 'Write proxy', component: } : + undefined, + contractQuery.data?.has_custom_methods_write ? + { id: 'write_custom_methods' as const, title: 'Write custom', component: } : + undefined, + ].filter(Boolean), + isLoading: contractQuery.isPlaceholderData, + }; + }, [ contractQuery, channel, data?.hash ]); } diff --git a/lib/hooks/useIsMounted.tsx b/lib/hooks/useIsMounted.tsx new file mode 100644 index 0000000000..d14880ae1b --- /dev/null +++ b/lib/hooks/useIsMounted.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +export default function useIsMounted() { + const [ isMounted, setIsMounted ] = React.useState(false); + + React.useEffect(() => { + setIsMounted(true); + }, [ ]); + + return isMounted; +} diff --git a/lib/hooks/useNavItems.tsx b/lib/hooks/useNavItems.tsx index 52f8b0ca2d..bc11f3e5f4 100644 --- a/lib/hooks/useNavItems.tsx +++ b/lib/hooks/useNavItems.tsx @@ -72,66 +72,79 @@ export default function useNavItems(): ReturnType { icon: 'validator', isActive: pathname === '/validators', } : null; + const rollupDeposits = { + text: `Deposits (L1${ rightLineArrow }L2)`, + nextRoute: { pathname: '/deposits' as const }, + icon: 'arrows/south-east', + isActive: pathname === '/deposits', + }; + const rollupWithdrawals = { + text: `Withdrawals (L2${ rightLineArrow }L1)`, + nextRoute: { pathname: '/withdrawals' as const }, + icon: 'arrows/north-east', + isActive: pathname === '/withdrawals', + }; + const rollupTxnBatches = { + text: 'Txn batches', + nextRoute: { pathname: '/batches' as const }, + icon: 'txn_batches', + isActive: pathname === '/batches', + }; + const rollupOutputRoots = { + text: 'Output roots', + nextRoute: { pathname: '/output-roots' as const }, + icon: 'output_roots', + isActive: pathname === '/output-roots', + }; const rollupFeature = config.features.rollup; - if (rollupFeature.isEnabled && rollupFeature.type === 'zkEvm') { + if (rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'zkEvm')) { blockchainNavItems = [ [ txs, - userOps, + rollupDeposits, + rollupWithdrawals, + ], + [ blocks, - { - text: 'Txn batches', - nextRoute: { pathname: '/batches' as const }, - icon: 'txn_batches', - isActive: pathname === '/batches' || pathname === '/batches/[number]', - }, + rollupTxnBatches, + rollupFeature.type === 'optimistic' ? rollupOutputRoots : undefined, ].filter(Boolean), [ + userOps, topAccounts, validators, verifiedContracts, ensLookup, ].filter(Boolean), ]; - } else if (rollupFeature.isEnabled && rollupFeature.type === 'optimistic') { + } 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' }, + rollupDeposits, + rollupWithdrawals, ], [ blocks, - // eslint-disable-next-line max-len - { text: 'Txn batches', nextRoute: { pathname: '/batches' as const }, icon: 'txn_batches', isActive: pathname === '/batches' }, - // eslint-disable-next-line max-len - { 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') { + } else if (rollupFeature.isEnabled && rollupFeature.type === 'zkSync') { 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, + blocks, + rollupTxnBatches, + ].filter(Boolean), + [ topAccounts, + validators, verifiedContracts, ensLookup, ].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/makePrettyLink.ts b/lib/makePrettyLink.ts new file mode 100644 index 0000000000..9e05a2d660 --- /dev/null +++ b/lib/makePrettyLink.ts @@ -0,0 +1,9 @@ +export default function makePrettyLink(url: string | undefined): { url: string; domain: string } | undefined { + try { + const urlObj = new URL(url ?? ''); + return { + url: urlObj.href, + domain: urlObj.hostname, + }; + } catch (error) {} +} diff --git a/lib/metadata/generate.test.ts b/lib/metadata/generate.test.ts index 5a37e98fe0..b9cbe2f806 100644 --- a/lib/metadata/generate.test.ts +++ b/lib/metadata/generate.test.ts @@ -4,26 +4,29 @@ import type { Route } from 'nextjs-routes'; import generate from './generate'; -interface TestCase { +interface TestCase { title: string; - route: R; - apiData?: ApiData; + route: { + pathname: Pathname; + query?: Route['query']; + }; + apiData?: ApiData; } -const TEST_CASES: Array> = [ +const TEST_CASES = [ { title: 'static route', route: { pathname: '/blocks', }, - }, + } as TestCase<'/blocks'>, { title: 'dynamic route', route: { pathname: '/tx/[hash]', query: { hash: '0x12345' }, }, - }, + } as TestCase<'/tx/[hash]'>, { title: 'dynamic route with API data', route: { @@ -31,7 +34,7 @@ const TEST_CASES: Array> = [ query: { hash: '0x12345' }, }, apiData: { symbol: 'USDT' }, - } as TestCase<{ pathname: '/token/[hash]'; query: { hash: string }}>, + } as TestCase<'/token/[hash]'>, ]; describe('generates correct metadata for:', () => { diff --git a/lib/metadata/generate.ts b/lib/metadata/generate.ts index 8d24fd1606..3c16903583 100644 --- a/lib/metadata/generate.ts +++ b/lib/metadata/generate.ts @@ -1,4 +1,5 @@ import type { ApiData, Metadata } from './types'; +import type { RouteParams } from 'nextjs/types'; import type { Route } from 'nextjs-routes'; @@ -9,7 +10,7 @@ import compileValue from './compileValue'; import getPageOgType from './getPageOgType'; import * as templates from './templates'; -export default function generate(route: R, apiData?: ApiData): Metadata { +export default function generate(route: RouteParams, apiData: ApiData = null): Metadata { const params = { ...route.query, ...apiData, @@ -17,7 +18,7 @@ export default function generate(route: R, apiData?: ApiData network_title: getNetworkTitle(), }; - const compiledTitle = compileValue(templates.title.make(route.pathname), params); + const compiledTitle = compileValue(templates.title.make(route.pathname, Boolean(apiData)), params); const title = compiledTitle ? compiledTitle + (config.meta.promoteBlockscoutInTitle ? ' | Blockscout' : '') : ''; const description = compileValue(templates.description.make(route.pathname), params); diff --git a/lib/metadata/getPageOgType.ts b/lib/metadata/getPageOgType.ts index 0c97b72dc7..a195848744 100644 --- a/lib/metadata/getPageOgType.ts +++ b/lib/metadata/getPageOgType.ts @@ -37,6 +37,7 @@ 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', @@ -47,6 +48,8 @@ const OG_TYPE_DICT: Record = { // service routes, added only to make typescript happy '/login': 'Regular page', + '/api/metrics': 'Regular page', + '/api/log': 'Regular page', '/api/media-type': 'Regular page', '/api/proxy': 'Regular page', '/api/csrf': 'Regular page', diff --git a/lib/metadata/index.ts b/lib/metadata/index.ts index 6cf182a7b7..903bd988e8 100644 --- a/lib/metadata/index.ts +++ b/lib/metadata/index.ts @@ -1,2 +1,3 @@ export { default as generate } from './generate'; export { default as update } from './update'; +export * from './types'; diff --git a/lib/metadata/templates/description.ts b/lib/metadata/templates/description.ts index 3348406afd..dbaaf5e0fc 100644 --- a/lib/metadata/templates/description.ts +++ b/lib/metadata/templates/description.ts @@ -40,6 +40,7 @@ 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, @@ -50,6 +51,8 @@ const TEMPLATE_MAP: Record = { // service routes, added only to make typescript happy '/login': DEFAULT_TEMPLATE, + '/api/metrics': DEFAULT_TEMPLATE, + '/api/log': DEFAULT_TEMPLATE, '/api/media-type': DEFAULT_TEMPLATE, '/api/proxy': DEFAULT_TEMPLATE, '/api/csrf': DEFAULT_TEMPLATE, diff --git a/lib/metadata/templates/title.ts b/lib/metadata/templates/title.ts index 57abbca8af..c82feb2c28 100644 --- a/lib/metadata/templates/title.ts +++ b/lib/metadata/templates/title.ts @@ -13,10 +13,10 @@ const TEMPLATE_MAP: Record = { '/contract-verification': 'verify contract', '/address/[hash]/contract-verification': 'contract verification for %hash%', '/tokens': 'tokens', - '/token/[hash]': '%symbol% token details', + '/token/[hash]': 'token details', '/token/[hash]/instance/[id]': 'NFT instance', '/apps': 'apps marketplace', - '/apps/[id]': '- %app_name%', + '/apps/[id]': 'marketplace app', '/stats': 'statistics', '/api-docs': 'REST API', '/graphiql': 'GraphQL', @@ -35,6 +35,7 @@ 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', @@ -45,6 +46,8 @@ const TEMPLATE_MAP: Record = { // service routes, added only to make typescript happy '/login': 'login', + '/api/metrics': 'node API prometheus metrics', + '/api/log': 'node API request log', '/api/media-type': 'node API media type', '/api/proxy': 'node API proxy', '/api/csrf': 'node API CSRF token', @@ -53,8 +56,15 @@ const TEMPLATE_MAP: Record = { '/auth/unverified-email': 'unverified email', }; -export function make(pathname: Route['pathname']) { - const template = TEMPLATE_MAP[pathname]; +const TEMPLATE_MAP_ENHANCED: Partial> = { + '/token/[hash]': '%symbol% token details', + '/token/[hash]/instance/[id]': 'token instance for %symbol%', + '/apps/[id]': '- %app_name%', + '/address/[hash]': 'address details for %domain_name%', +}; + +export function make(pathname: Route['pathname'], isEnriched = false) { + const template = (isEnriched ? TEMPLATE_MAP_ENHANCED[pathname] : undefined) ?? TEMPLATE_MAP[pathname]; return `%network_name% ${ template }`; } diff --git a/lib/metadata/types.ts b/lib/metadata/types.ts index 252dbc29cf..b7e7547717 100644 --- a/lib/metadata/types.ts +++ b/lib/metadata/types.ts @@ -1,11 +1,16 @@ +import type { TokenInfo } from 'types/api/token'; + import type { Route } from 'nextjs-routes'; /* eslint-disable @typescript-eslint/indent */ -export type ApiData = -R['pathname'] extends '/token/[hash]' ? { symbol: string } : -R['pathname'] extends '/token/[hash]/instance/[id]' ? { symbol: string } : -R['pathname'] extends '/apps/[id]' ? { app_name: string } : -never; +export type ApiData = +( + Pathname extends '/address/[hash]' ? { domain_name: string } : + Pathname extends '/token/[hash]' ? TokenInfo : + Pathname extends '/token/[hash]/instance/[id]' ? { symbol: string } : + Pathname extends '/apps/[id]' ? { app_name: string } : + never +) | null; export interface Metadata { title: string; diff --git a/lib/metadata/update.ts b/lib/metadata/update.ts index f6168c1ae6..123e3ca100 100644 --- a/lib/metadata/update.ts +++ b/lib/metadata/update.ts @@ -1,10 +1,11 @@ import type { ApiData } from './types'; +import type { RouteParams } from 'nextjs/types'; import type { Route } from 'nextjs-routes'; import generate from './generate'; -export default function update(route: R, apiData: ApiData) { +export default function update(route: RouteParams, apiData: ApiData) { const { title, description } = generate(route, apiData); window.document.title = title; diff --git a/lib/mixpanel/getGoogleAnalyticsClientId.ts b/lib/mixpanel/getGoogleAnalyticsClientId.ts new file mode 100644 index 0000000000..704cf14f2f --- /dev/null +++ b/lib/mixpanel/getGoogleAnalyticsClientId.ts @@ -0,0 +1,3 @@ +export default function getGoogleAnalyticsClientId() { + return window.ga?.getAll()[0].get('clientId'); +} diff --git a/lib/mixpanel/getPageType.ts b/lib/mixpanel/getPageType.ts index ba329c289d..f74a16fb51 100644 --- a/lib/mixpanel/getPageType.ts +++ b/lib/mixpanel/getPageType.ts @@ -35,6 +35,7 @@ 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', @@ -45,6 +46,8 @@ export const PAGE_TYPE_DICT: Record = { // service routes, added only to make typescript happy '/login': 'Login', + '/api/metrics': 'Node API: Prometheus metrics', + '/api/log': 'Node API: Request log', '/api/media-type': 'Node API: Media type', '/api/proxy': 'Node API: Proxy', '/api/csrf': 'Node API: CSRF token', diff --git a/lib/mixpanel/isGoogleAnalyticsLoaded.ts b/lib/mixpanel/isGoogleAnalyticsLoaded.ts new file mode 100644 index 0000000000..08667749b1 --- /dev/null +++ b/lib/mixpanel/isGoogleAnalyticsLoaded.ts @@ -0,0 +1,9 @@ +import config from 'configs/app'; +import delay from 'lib/delay'; + +export default function isGoogleAnalyticsLoaded(retries = 3): Promise { + if (!retries || !config.features.googleAnalytics.isEnabled) { + return Promise.resolve(false); + } + return typeof window.ga?.getAll === 'function' ? Promise.resolve(true) : delay(500).then(() => isGoogleAnalyticsLoaded(retries - 1)); +} diff --git a/lib/mixpanel/utils.ts b/lib/mixpanel/utils.ts index db7a89b34a..b901d7cc5e 100644 --- a/lib/mixpanel/utils.ts +++ b/lib/mixpanel/utils.ts @@ -15,9 +15,11 @@ export enum EventTypes { CONTRACT_VERIFICATION = 'Contract verification', QR_CODE = 'QR code', PAGE_WIDGET = 'Page widget', - TX_INTERPRETATION_INTERACTION = 'Transaction interpratetion interaction', + TX_INTERPRETATION_INTERACTION = 'Transaction interpretation interaction', EXPERIMENT_STARTED = 'Experiment started', - FILTERS = 'Filters' + FILTERS = 'Filters', + BUTTON_CLICK = 'Button click', + PROMO_BANNER = 'Promo banner', } /* eslint-disable @typescript-eslint/indent */ @@ -73,9 +75,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; @@ -91,8 +99,16 @@ Type extends EventTypes.PAGE_WIDGET ? ( { 'Type': 'Tokens dropdown' | 'Tokens show all (icon)' | 'Add to watchlist' | 'Address actions (more button)'; } | { - 'Type': 'Favorite app' | 'More button'; + 'Type': 'Favorite app' | 'More button' | 'Security score' | 'Total contracts' | 'Verified contracts' | 'Analyzed contracts'; 'Info': string; + 'Source': 'Discovery view' | 'Security view' | 'App modal' | 'App page' | 'Security score popup' | 'Banner'; + } | { + 'Type': 'Security score'; + 'Source': 'Analyzed contracts popup'; + } | { + 'Type': 'Address tag'; + 'Info': string; + 'URL': string; } ) : Type extends EventTypes.TX_INTERPRETATION_INTERACTION ? { @@ -107,5 +123,13 @@ Type extends EventTypes.FILTERS ? { 'Source': 'Marketplace'; 'Filter name': string; } : +Type extends EventTypes.BUTTON_CLICK ? { + 'Content': 'Swap button'; + 'Source': string; +} : +Type extends EventTypes.PROMO_BANNER ? { + 'Source': 'Marketplace'; + 'Link': string; +} : undefined; /* eslint-enable @typescript-eslint/indent */ diff --git a/lib/monitoring/metrics.ts b/lib/monitoring/metrics.ts new file mode 100644 index 0000000000..f6f6b1a3e7 --- /dev/null +++ b/lib/monitoring/metrics.ts @@ -0,0 +1,33 @@ +import * as promClient from 'prom-client'; + +const metrics = (() => { + // eslint-disable-next-line no-restricted-properties + if (process.env.PROMETHEUS_METRICS_ENABLED !== 'true') { + return; + } + + promClient.register.clear(); + + const socialPreviewBotRequests = new promClient.Counter({ + name: 'social_preview_bot_requests_total', + help: 'Number of incoming requests from social preview bots', + labelNames: [ 'route', 'bot' ] as const, + }); + + const searchEngineBotRequests = new promClient.Counter({ + name: 'search_engine_bot_requests_total', + help: 'Number of incoming requests from search engine bots', + labelNames: [ 'route', 'bot' ] as const, + }); + + const apiRequestDuration = new promClient.Histogram({ + name: 'api_request_duration_seconds', + help: 'Duration of requests to API in seconds', + labelNames: [ 'route', 'code' ], + buckets: [ 0.2, 0.5, 1, 3, 10 ], + }); + + return { socialPreviewBotRequests, searchEngineBotRequests, apiRequestDuration }; +})(); + +export default metrics; diff --git a/lib/socket/types.ts b/lib/socket/types.ts index 2bb2d47e71..d0f7564e56 100644 --- a/lib/socket/types.ts +++ b/lib/socket/types.ts @@ -22,11 +22,13 @@ SocketMessage.AddressTokenBalance | SocketMessage.AddressTokenBalancesErc20 | SocketMessage.AddressTokenBalancesErc721 | SocketMessage.AddressTokenBalancesErc1155 | +SocketMessage.AddressTokenBalancesErc404 | SocketMessage.AddressCoinBalance | SocketMessage.AddressTxs | SocketMessage.AddressTxsPending | SocketMessage.AddressTokenTransfer | SocketMessage.AddressChangedBytecode | +SocketMessage.AddressFetchedBytecode | SocketMessage.SmartContractWasVerified | SocketMessage.TokenTransfers | SocketMessage.TokenTotalSupply | @@ -57,11 +59,13 @@ export namespace SocketMessage { export type AddressTokenBalancesErc20 = SocketMessageParamsGeneric<'updated_token_balances_erc_20', AddressTokensBalancesSocketMessage>; export type AddressTokenBalancesErc721 = SocketMessageParamsGeneric<'updated_token_balances_erc_721', AddressTokensBalancesSocketMessage>; export type AddressTokenBalancesErc1155 = SocketMessageParamsGeneric<'updated_token_balances_erc_1155', AddressTokensBalancesSocketMessage>; + export type AddressTokenBalancesErc404 = SocketMessageParamsGeneric<'updated_token_balances_erc_404', AddressTokensBalancesSocketMessage>; export type AddressCoinBalance = SocketMessageParamsGeneric<'coin_balance', { coin_balance: AddressCoinBalanceHistoryItem }>; export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transactions: Array }>; export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transactions: Array }>; export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfers: Array }>; export type AddressChangedBytecode = SocketMessageParamsGeneric<'changed_bytecode', Record>; + export type AddressFetchedBytecode = SocketMessageParamsGeneric<'fetched_bytecode', { fetched_bytecode: string }>; export type SmartContractWasVerified = SocketMessageParamsGeneric<'smart_contract_was_verified', Record>; export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }>; export type TokenTotalSupply = SocketMessageParamsGeneric<'total_supply', {total_supply: number }>; 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/lib/token/tokenTypes.ts b/lib/token/tokenTypes.ts index 5246fc2418..4b7fabf9ea 100644 --- a/lib/token/tokenTypes.ts +++ b/lib/token/tokenTypes.ts @@ -3,6 +3,7 @@ import type { NFTTokenType, TokenType } from 'types/api/token'; export const NFT_TOKEN_TYPES: Array<{ title: string; id: NFTTokenType }> = [ { title: 'ERC-721', id: 'ERC-721' }, { title: 'ERC-1155', id: 'ERC-1155' }, + { title: 'ERC-404', id: 'ERC-404' }, ]; export const TOKEN_TYPES: Array<{ title: string; id: TokenType }> = [ diff --git a/lib/web3/client.ts b/lib/web3/client.ts index d89fcac808..8188f22869 100644 --- a/lib/web3/client.ts +++ b/lib/web3/client.ts @@ -3,7 +3,7 @@ import { createPublicClient, http } from 'viem'; import currentChain from './currentChain'; export const publicClient = (() => { - if (currentChain.rpcUrls.public.http.filter(Boolean).length === 0) { + if (currentChain.rpcUrls.default.http.filter(Boolean).length === 0) { return; } diff --git a/lib/web3/currentChain.ts b/lib/web3/currentChain.ts index 23a089e97b..dd3892859f 100644 --- a/lib/web3/currentChain.ts +++ b/lib/web3/currentChain.ts @@ -1,20 +1,16 @@ -import type { Chain } from 'wagmi'; +import { type Chain } from 'viem'; import config from 'configs/app'; -const currentChain: Chain = { +const currentChain = { id: Number(config.chain.id), name: config.chain.name ?? '', - network: config.chain.name ?? '', nativeCurrency: { decimals: config.chain.currency.decimals, name: config.chain.currency.name ?? '', symbol: config.chain.currency.symbol ?? '', }, rpcUrls: { - 'public': { - http: [ config.chain.rpcUrl ?? '' ], - }, 'default': { http: [ config.chain.rpcUrl ?? '' ], }, @@ -25,6 +21,7 @@ const currentChain: Chain = { url: config.app.baseUrl, }, }, -}; + testnet: config.chain.isTestnet, +} as const satisfies Chain; export default currentChain; diff --git a/lib/web3/useAccount.ts b/lib/web3/useAccount.ts new file mode 100644 index 0000000000..f3dfcd48c8 --- /dev/null +++ b/lib/web3/useAccount.ts @@ -0,0 +1,23 @@ +import type { UseAccountReturnType } from 'wagmi'; +import { useAccount } from 'wagmi'; + +import config from 'configs/app'; + +function useAccountFallback(): UseAccountReturnType { + return { + address: undefined, + addresses: undefined, + chain: undefined, + chainId: undefined, + connector: undefined, + isConnected: false, + isConnecting: false, + isDisconnected: true, + isReconnecting: false, + status: 'disconnected', + }; +} + +const hook = config.features.blockchainInteraction.isEnabled ? useAccount : useAccountFallback; + +export default hook; diff --git a/lib/web3/useProvider.tsx b/lib/web3/useProvider.tsx index 029eb24835..43cc7aa8fa 100644 --- a/lib/web3/useProvider.tsx +++ b/lib/web3/useProvider.tsx @@ -1,16 +1,14 @@ import React from 'react'; -import type { WindowProvider } from 'wagmi'; - -import 'wagmi/window'; import type { WalletType } from 'types/client/wallets'; +import type { WalletProvider } from 'types/web3'; import config from 'configs/app'; const feature = config.features.web3Wallet; export default function useProvider() { - const [ provider, setProvider ] = React.useState(); + const [ provider, setProvider ] = React.useState(); const [ wallet, setWallet ] = React.useState(); const initializeProvider = React.useMemo(() => async() => { diff --git a/lib/web3/wagmiConfig.ts b/lib/web3/wagmiConfig.ts new file mode 100644 index 0000000000..f90d0a069f --- /dev/null +++ b/lib/web3/wagmiConfig.ts @@ -0,0 +1,38 @@ +import { defaultWagmiConfig } from '@web3modal/wagmi/react/config'; +import { http } from 'viem'; +import type { CreateConfigParameters } from 'wagmi'; + +import config from 'configs/app'; +import currentChain from 'lib/web3/currentChain'; +const feature = config.features.blockchainInteraction; + +const wagmiConfig = (() => { + try { + if (!feature.isEnabled) { + throw new Error(); + } + + const chains: CreateConfigParameters['chains'] = [ currentChain ]; + + const wagmiConfig = defaultWagmiConfig({ + chains, + multiInjectedProviderDiscovery: true, + transports: { + [currentChain.id]: http(), + }, + projectId: feature.walletConnect.projectId, + metadata: { + name: `${ config.chain.name } explorer`, + description: `${ config.chain.name } explorer`, + url: config.app.baseUrl, + icons: [ config.UI.sidebar.icon.default ].filter(Boolean), + }, + enableEmail: true, + ssr: true, + }); + + return wagmiConfig; + } catch (error) {} +})(); + +export default wagmiConfig; diff --git a/mocks/ad/textAd.ts b/mocks/ad/textAd.ts index 6ee891bea8..0e5bb6feae 100644 --- a/mocks/ad/textAd.ts +++ b/mocks/ad/textAd.ts @@ -2,7 +2,7 @@ export const duck = { ad: { name: 'Hello utia!', description_short: 'Utia is the best! Go with utia! Utia is the best! Go with utia!', - thumbnail: 'https://utia.utia', + thumbnail: 'http://localhost:3100/utia.jpg', url: 'https://test.url', cta_button: 'Click me!', }, diff --git a/mocks/address/address.ts b/mocks/address/address.ts index 7c7bc612cb..8ed50dca22 100644 --- a/mocks/address/address.ts +++ b/mocks/address/address.ts @@ -30,6 +30,24 @@ export const withEns: AddressParam = { ens_domain_name: 'kitty.kitty.kitty.cat.eth', }; +export const withNameTag: AddressParam = { + hash: hash, + implementation_name: null, + is_contract: false, + is_verified: null, + name: 'ArianeeStore', + private_tags: [], + watchlist_names: [], + public_tags: [], + ens_domain_name: 'kitty.kitty.kitty.cat.eth', + metadata: { + reputation: null, + tags: [ + { tagType: 'name', name: 'Mrs. Duckie', slug: 'mrs-duckie', ordinal: 0, meta: null }, + ], + }, +}; + export const withoutName: AddressParam = { hash: hash, implementation_name: null, @@ -59,14 +77,8 @@ export const token: Address = { creator_address_hash: '0x34A9c688512ebdB575e82C50c9803F6ba2916E72', exchange_rate: null, implementation_address: null, - has_custom_methods_read: false, - has_custom_methods_write: false, has_decompiled_code: false, has_logs: false, - has_methods_read: false, - has_methods_read_proxy: false, - has_methods_write: false, - has_methods_write_proxy: false, has_token_transfers: true, has_tokens: true, has_validated_blocks: false, @@ -79,14 +91,8 @@ export const contract: Address = { creation_tx_hash: '0xf2aff6501b632604c39978b47d309813d8a1bcca721864bbe86abf59704f195e', creator_address_hash: '0x803ad3F50b9e1fF68615e8B053A186e1be288943', exchange_rate: '0.04311', - has_custom_methods_read: false, - has_custom_methods_write: false, has_decompiled_code: false, has_logs: true, - has_methods_read: true, - has_methods_read_proxy: true, - has_methods_write: true, - has_methods_write_proxy: true, has_token_transfers: false, has_tokens: false, has_validated_blocks: false, @@ -110,14 +116,8 @@ export const validator: Address = { creation_tx_hash: null, creator_address_hash: null, exchange_rate: '0.00432018', - has_custom_methods_read: false, - has_custom_methods_write: false, has_decompiled_code: false, has_logs: false, - has_methods_read: false, - has_methods_read_proxy: false, - has_methods_write: false, - has_methods_write_proxy: false, has_token_transfers: false, has_tokens: false, has_validated_blocks: true, diff --git a/mocks/address/coinBalanceHistory.ts b/mocks/address/coinBalanceHistory.ts index 7bd121dafe..cc78d75602 100644 --- a/mocks/address/coinBalanceHistory.ts +++ b/mocks/address/coinBalanceHistory.ts @@ -35,33 +35,36 @@ export const baseResponse: AddressCoinBalanceHistoryResponse = { next_page_params: null, }; -export const chartResponse: AddressCoinBalanceHistoryChart = [ - { - date: '2022-11-02', - value: '128238612887883515', - }, - { - date: '2022-11-03', - value: '199807583157570922', - }, - { - date: '2022-11-04', - value: '114487912907005778', - }, - { - date: '2022-11-05', - value: '219533112907005778', - }, - { - date: '2022-11-06', - value: '116487912907005778', - }, - { - date: '2022-11-07', - value: '199807583157570922', - }, - { - date: '2022-11-08', - value: '216488112907005778', - }, -]; +export const chartResponse: AddressCoinBalanceHistoryChart = { + items: [ + { + date: '2022-11-02', + value: '128238612887883515', + }, + { + date: '2022-11-03', + value: '199807583157570922', + }, + { + date: '2022-11-04', + value: '114487912907005778', + }, + { + date: '2022-11-05', + value: '219533112907005778', + }, + { + date: '2022-11-06', + value: '116487912907005778', + }, + { + date: '2022-11-07', + value: '199807583157570922', + }, + { + date: '2022-11-08', + value: '216488112907005778', + }, + ], + days: 10, +}; diff --git a/mocks/address/tabCounters.ts b/mocks/address/tabCounters.ts new file mode 100644 index 0000000000..3853ffab4d --- /dev/null +++ b/mocks/address/tabCounters.ts @@ -0,0 +1,11 @@ +import type { AddressTabsCounters } from 'types/api/address'; + +export const base: AddressTabsCounters = { + internal_txs_count: 13, + logs_count: 51, + token_balances_count: 3, + token_transfers_count: 3, + transactions_count: 51, + validations_count: 42, + withdrawals_count: 11, +}; diff --git a/mocks/address/tokens.ts b/mocks/address/tokens.ts index c8c7386134..f3fd58b8d5 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, @@ -94,6 +105,20 @@ export const erc1155LongId: AddressTokenBalance = { value: '42', }; +export const erc404a: AddressTokenBalance = { + token: tokens.tokenInfoERC404, + token_id: '42', + token_instance: tokenInstance.base, + value: '240000000000000', +}; + +export const erc404b: AddressTokenBalance = { + token: tokens.tokenInfoERC404, + token_instance: null, + value: '11', + token_id: null, +}; + export const erc20List = { items: [ erc20a, @@ -118,6 +143,13 @@ export const erc1155List = { ], }; +export const erc404List = { + items: [ + erc404a, + erc404b, + ], +}; + export const nfts: AddressNFTsResponse = { items: [ { @@ -132,6 +164,12 @@ export const nfts: AddressNFTsResponse = { token_type: 'ERC-721', value: '1', }, + { + ...tokenInstance.unique, + token: tokens.tokenInfoERC404, + token_type: 'ERC-404', + value: '11000', + }, ], next_page_params: 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/apps/securityReports.ts b/mocks/apps/securityReports.ts new file mode 100644 index 0000000000..824a6fbe13 --- /dev/null +++ b/mocks/apps/securityReports.ts @@ -0,0 +1,58 @@ +export const securityReports = [ + { + appName: 'token-approval-tracker', + doc: 'http://docs.li.fi/smart-contracts/deployments#mainnet', + chainsData: { + '1': { + overallInfo: { + verifiedNumber: 1, + totalContractsNumber: 1, + solidityScanContractsNumber: 1, + securityScore: 87.5, + issueSeverityDistribution: { + critical: 4, + gas: 1, + high: 0, + informational: 4, + low: 2, + medium: 0, + }, + }, + contractsData: [ + { + address: '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE', + isVerified: true, + solidityScanReport: { + connection_id: '', + contract_address: '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE', + contract_chain: 'optimism', + contract_platform: 'blockscout', + contract_url: 'http://optimism.blockscout.com/address/0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE', + contractname: 'LiFiDiamond', + is_quick_scan: true, + node_reference_id: null, + request_type: 'threat_scan', + scanner_reference_url: 'http://solidityscan.com/quickscan/0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE/blockscout/eth?ref=blockscout', + scan_status: 'scan_done', + scan_summary: { + issue_severity_distribution: { + critical: 0, + gas: 1, + high: 0, + informational: 4, + low: 2, + medium: 0, + }, + lines_analyzed_count: 72, + scan_time_taken: 1, + score: '4.38', + score_v2: '87.50', + threat_score: '100.00', + }, + }, + }, + ], + }, + }, + }, +]; 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/info.ts b/mocks/contract/info.ts index c5e1a7e040..4ff831e168 100644 --- a/mocks/contract/info.ts +++ b/mocks/contract/info.ts @@ -1,7 +1,7 @@ /* eslint-disable max-len */ import type { SmartContract } from 'types/api/contract'; -export const verified: Partial = { +export const verified: SmartContract = { abi: [ { anonymous: false, inputs: [ { indexed: true, internalType: 'address', name: 'src', type: 'address' }, { indexed: true, internalType: 'address', name: 'guy', type: 'address' }, { indexed: false, internalType: 'uint256', name: 'wad', type: 'uint256' } ], name: 'Approval', type: 'event' } ], can_be_visualized_via_sol2uml: true, compiler_version: 'v0.5.16+commit.9c3226ce', @@ -31,9 +31,27 @@ export const verified: Partial = { { address_hash: '0xa62744BeE8646e237441CDbfdedD3458861748A8', name: 'math' }, ], language: 'solidity', + license_type: 'gnu_gpl_v3', + has_methods_read: true, + has_methods_read_proxy: false, + has_methods_write: true, + has_methods_write_proxy: false, + has_custom_methods_read: false, + has_custom_methods_write: false, + is_self_destructed: false, + is_verified_via_eth_bytecode_db: null, + is_changed_bytecode: null, + is_verified_via_sourcify: null, + is_fully_verified: null, + is_partially_verified: null, + sourcify_repo_url: null, + file_path: '', + additional_sources: [], + verified_twin_address_hash: null, + minimal_proxy_address_hash: null, }; -export const withMultiplePaths: Partial = { +export const withMultiplePaths: SmartContract = { ...verified, file_path: './simple_storage.sol', additional_sources: [ @@ -44,7 +62,7 @@ export const withMultiplePaths: Partial = { ], }; -export const verifiedViaSourcify: Partial = { +export const verifiedViaSourcify: SmartContract = { ...verified, is_verified_via_sourcify: true, is_fully_verified: false, @@ -52,36 +70,67 @@ export const verifiedViaSourcify: Partial = { sourcify_repo_url: 'https://repo.sourcify.dev/contracts//full_match/99/0x51891596E158b2857e5356DC847e2D15dFbCF2d0/', }; -export const verifiedViaEthBytecodeDb: Partial = { +export const verifiedViaEthBytecodeDb: SmartContract = { ...verified, is_verified_via_eth_bytecode_db: true, }; -export const withTwinAddress: Partial = { +export const withTwinAddress: SmartContract = { ...verified, is_verified: false, verified_twin_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8', }; -export const withProxyAddress: Partial = { +export const withProxyAddress: SmartContract = { ...verified, is_verified: false, verified_twin_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8', minimal_proxy_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8', }; -export const selfDestructed: Partial = { +export const selfDestructed: SmartContract = { ...verified, is_self_destructed: true, }; -export const withChangedByteCode: Partial = { +export const withChangedByteCode: SmartContract = { ...verified, is_changed_bytecode: true, }; -export const nonVerified: Partial = { +export const nonVerified: SmartContract = { is_verified: false, creation_bytecode: 'creation_bytecode', deployed_bytecode: 'deployed_bytecode', + is_self_destructed: false, + abi: null, + compiler_version: null, + evm_version: null, + optimization_enabled: null, + optimization_runs: null, + name: null, + verified_at: null, + is_verified_via_eth_bytecode_db: null, + is_changed_bytecode: null, + has_methods_read: false, + has_methods_read_proxy: false, + has_methods_write: false, + has_methods_write_proxy: false, + has_custom_methods_read: false, + has_custom_methods_write: false, + is_verified_via_sourcify: null, + is_fully_verified: null, + is_partially_verified: null, + sourcify_repo_url: null, + source_code: null, + constructor_args: null, + decoded_constructor_args: null, + can_be_visualized_via_sol2uml: null, + file_path: '', + additional_sources: [], + external_libraries: null, + verified_twin_address_hash: null, + minimal_proxy_address_hash: null, + language: null, + license_type: null, }; diff --git a/mocks/contract/methods.ts b/mocks/contract/methods.ts index 6c1bdf367e..61ee20d666 100644 --- a/mocks/contract/methods.ts +++ b/mocks/contract/methods.ts @@ -1,6 +1,6 @@ import type { - SmartContractQueryMethodReadError, - SmartContractQueryMethodReadSuccess, + SmartContractQueryMethodError, + SmartContractQueryMethodSuccess, SmartContractReadMethod, SmartContractWriteMethod, } from 'types/api/contract'; @@ -94,7 +94,7 @@ export const read: Array = [ }, ]; -export const readResultSuccess: SmartContractQueryMethodReadSuccess = { +export const readResultSuccess: SmartContractQueryMethodSuccess = { is_error: false, result: { names: [ 'amount' ], @@ -104,7 +104,7 @@ export const readResultSuccess: SmartContractQueryMethodReadSuccess = { }, }; -export const readResultError: SmartContractQueryMethodReadError = { +export const readResultError: SmartContractQueryMethodError = { is_error: true, result: { message: 'Some shit happened', 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/l2withdrawals/withdrawals.ts b/mocks/l2withdrawals/withdrawals.ts index 0e0d69a22f..8882f8515d 100644 --- a/mocks/l2withdrawals/withdrawals.ts +++ b/mocks/l2withdrawals/withdrawals.ts @@ -1,4 +1,6 @@ -export const data = { +import type { OptimisticL2WithdrawalsResponse } from 'types/api/optimisticL2'; + +export const data: OptimisticL2WithdrawalsResponse = { items: [ { challenge_period_end: null, @@ -11,12 +13,12 @@ export const data = { private_tags: [], public_tags: [], watchlist_names: [], + ens_domain_name: null, }, l1_tx_hash: '0x1a235bee32ac10cb7efdad98415737484ca66386e491cde9e17d42b136dca684', l2_timestamp: '2022-02-15T12:50:02.000000Z', l2_tx_hash: '0x918cd8c5c24c17e06cd02b0379510c4ad56324bf153578fb9caaaa2fe4e7dc35', msg_nonce: 396, - msg_nonce_raw: '1766847064778384329583297500742918515827483896875618958121606201292620172', msg_nonce_version: 1, status: 'Ready to prove', }, @@ -27,7 +29,6 @@ export const data = { l2_timestamp: null, l2_tx_hash: '0x2f117bee32ac10cb7efdad98415737484ca66386e491cde9e17d42b136def593', msg_nonce: 391, - msg_nonce_raw: '1766847064778384329583297500742918515827483896875618958121606201292620167', msg_nonce_version: 1, status: 'Ready to prove', }, @@ -38,7 +39,6 @@ export const data = { l2_timestamp: null, l2_tx_hash: '0xe14b1f46838176702244a5343629bcecf728ca2d9881d47b4ce46e00c387d7e3', msg_nonce: 390, - msg_nonce_raw: '1766847064778384329583297500742918515827483896875618958121606201292620166', msg_nonce_version: 1, status: 'Ready for relay', }, diff --git a/mocks/metadata/address.ts b/mocks/metadata/address.ts new file mode 100644 index 0000000000..4e7849bb50 --- /dev/null +++ b/mocks/metadata/address.ts @@ -0,0 +1,63 @@ +/* eslint-disable max-len */ +import type { AddressMetadataTagApi } from 'types/api/addressMetadata'; + +export const nameTag: AddressMetadataTagApi = { + slug: 'quack-quack', + name: 'Quack quack', + tagType: 'name', + ordinal: 99, + meta: null, +}; + +export const customNameTag: AddressMetadataTagApi = { + slug: 'unicorn-uproar', + name: 'Unicorn Uproar', + tagType: 'name', + ordinal: 777, + meta: { + tagUrl: 'https://example.com', + bgColor: 'linear-gradient(45deg, deeppink, deepskyblue)', + textColor: '#FFFFFF', + }, +}; + +export const genericTag: AddressMetadataTagApi = { + slug: 'duck-owner', + name: 'duck owner 🦆', + tagType: 'generic', + ordinal: 55, + meta: { + bgColor: 'rgba(255,243,12,90%)', + }, +}; + +export const infoTagWithLink: AddressMetadataTagApi = { + slug: 'goosegang', + name: 'GooseGanG GooseGanG GooseGanG GooseGanG GooseGanG GooseGanG GooseGanG', + tagType: 'classifier', + ordinal: 11, + meta: { + tagUrl: 'https://example.com', + }, +}; + +export const tagWithTooltip: AddressMetadataTagApi = { + slug: 'blockscout-heroes', + name: 'BlockscoutHeroes', + tagType: 'classifier', + ordinal: 42, + meta: { + tooltipDescription: 'The Blockscout team, EVM blockchain aficionados, illuminate Ethereum networks with unparalleled insight and prowess, leading the way in blockchain exploration! 🚀🔎', + tooltipIcon: 'https://localhost:3100/icon.svg', + tooltipTitle: 'Blockscout team member', + tooltipUrl: 'https://blockscout.com', + }, +}; + +export const protocolTag: AddressMetadataTagApi = { + slug: 'aerodrome', + name: 'Aerodrome', + tagType: 'protocol', + ordinal: 0, + meta: null, +}; diff --git a/mocks/noves/transaction.ts b/mocks/noves/transaction.ts new file mode 100644 index 0000000000..6feb72a564 --- /dev/null +++ b/mocks/noves/transaction.ts @@ -0,0 +1,103 @@ +import type { NovesResponseData } from 'types/api/noves'; + +import type { TokensData } from 'ui/tx/assetFlows/utils/getTokensData'; + +export const hash = '0x380400d04ebb4179a35b1d7fdef260776915f015e978f8587ef2704b843d4e53'; + +export const transaction: NovesResponseData = { + accountAddress: '0xef6595A423c99f3f2821190A4d96fcE4DcD89a80', + chain: 'eth-goerli', + classificationData: { + description: 'Called function \'stake\' on contract 0xef326CdAdA59D3A740A76bB5f4F88Fb2.', + protocol: { + name: null, + }, + received: [], + sent: [ + { + action: 'sent', + actionFormatted: 'Sent', + amount: '3000', + from: { + address: '0xef6595A423c99f3f2821190A4d96fcE4DcD89a80', + name: 'This wallet', + }, + to: { + address: '0xdD15D2650387Fb6FEDE27ae7392C402a393F8A37', + name: null, + }, + token: { + address: '0x1bfe4298796198f8664b18a98640cec7c89b5baa', + decimals: 18, + name: 'PQR-Test', + symbol: 'PQR', + }, + }, + { + action: 'paidGas', + actionFormatted: 'Paid Gas', + amount: '0.000395521502109448', + from: { + address: '0xef6595A423c99f3f2821190A4d96fcE4DcD89a80', + name: 'This wallet', + }, + to: { + address: null, + name: 'Validators', + }, + token: { + address: 'ETH', + decimals: 18, + name: 'ETH', + symbol: 'ETH', + }, + }, + ], + source: { + type: null, + }, + type: 'unclassified', + typeFormatted: 'Unclassified', + }, + rawTransactionData: { + blockNumber: 10388918, + fromAddress: '0xef6595A423c99f3f2821190A4d96fcE4DcD89a80', + gas: 275079, + gasPrice: 1500000008, + timestamp: 1705488588, + toAddress: '0xef326CdAdA59D3A740A76bB5f4F88Fb2f1076164', + transactionFee: { + amount: '395521502109448', + token: { + decimals: 18, + symbol: 'ETH', + }, + }, + transactionHash: '0x380400d04ebb4179a35b1d7fdef260776915f015e978f8587ef2704b843d4e53', + }, + txTypeVersion: 2, +}; + +export const tokenData: TokensData = { + nameList: [ 'PQR-Test', 'ETH' ], + symbolList: [ 'PQR' ], + idList: [], + byName: { + 'PQR-Test': { + name: 'PQR-Test', + symbol: 'PQR', + address: '0x1bfe4298796198f8664b18a98640cec7c89b5baa', + id: undefined, + }, + ETH: { name: 'ETH', symbol: null, address: '', id: undefined }, + }, + bySymbol: { + PQR: { + name: 'PQR-Test', + symbol: 'PQR', + address: '0x1bfe4298796198f8664b18a98640cec7c89b5baa', + id: undefined, + }, + 'null': { name: 'ETH', symbol: null, address: '', id: undefined }, + }, +}; diff --git a/mocks/search/index.ts b/mocks/search/index.ts index ef384d1d70..571eb24976 100644 --- a/mocks/search/index.ts +++ b/mocks/search/index.ts @@ -6,6 +6,8 @@ import type { SearchResultLabel, SearchResult, SearchResultUserOp, + SearchResultBlob, + SearchResultDomain, } from 'types/api/search'; export const token1: SearchResultToken = { @@ -116,6 +118,26 @@ export const userOp1: SearchResultUserOp = { url: '/op/0xcb560d77b0f3af074fa05c1e5c691bcdfe457e630062b5907e9e71fc74b2ec61', }; +export const blob1: SearchResultBlob = { + blob_hash: '0x0108dd3e414da9f3255f7a831afa606e8dfaea93d082dfa9b15305583cbbdbbe', + type: 'blob' as const, + timestamp: null, +}; + +export const domain1: SearchResultDomain = { + address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + ens_info: { + address_hash: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + expiry_date: '2039-09-01T07:36:18.000Z', + name: 'vitalik.eth', + names_count: 1, + }, + is_smart_contract_verified: false, + name: null, + type: 'ens_domain', + url: '/address/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', +}; + export const baseResponse: SearchResult = { items: [ token1, @@ -124,6 +146,8 @@ export const baseResponse: SearchResult = { address1, contract1, tx1, + blob1, + domain1, ], next_page_params: null, }; 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 8634ff5a62..47d13cbb88 100644 --- a/mocks/stats/index.ts +++ b/mocks/stats/index.ts @@ -60,3 +60,16 @@ export const withoutBothPrices = { ...base, gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null, fiat_price: null }) : null), }; + +export const withSecondaryCoin = { + ...base, + secondary_coin_price: '3.398', +}; + +export const noChartData = { + ...base, + transactions_today: null, + coin_price: null, + market_cap: null, + tvl: 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/tokens/tokenInfo.ts b/mocks/tokens/tokenInfo.ts index 1034b9bc64..a732712775 100644 --- a/mocks/tokens/tokenInfo.ts +++ b/mocks/tokens/tokenInfo.ts @@ -28,7 +28,7 @@ export const tokenInfoERC20a: TokenInfo<'ERC-20'> = { symbol: 'HyFi', total_supply: '369000000000000000000000000', type: 'ERC-20', - icon_url: 'https://example.com/token-icon.png', + icon_url: 'http://localhost:3000/token-icon.png', }; export const tokenInfoERC20b: TokenInfo<'ERC-20'> = { @@ -174,6 +174,19 @@ export const tokenInfoERC1155WithoutName: TokenInfo<'ERC-1155'> = { icon_url: null, }; +export const tokenInfoERC404: TokenInfo<'ERC-404'> = { + address: '0xB5C457dDB4cE3312a6C5a2b056a1652bd542a208', + circulating_market_cap: '0.0', + decimals: '18', + exchange_rate: '1484.13', + holders: '81', + icon_url: null, + name: 'OMNI404', + symbol: 'O404', + total_supply: '6482275000000000000', + type: 'ERC-404', +}; + export const bridgedTokenA: TokenInfo<'ERC-20'> = { ...tokenInfoERC20a, is_bridged: true, diff --git a/mocks/tokens/tokenTransfer.ts b/mocks/tokens/tokenTransfer.ts index 1e0573d43e..e756d14617 100644 --- a/mocks/tokens/tokenTransfer.ts +++ b/mocks/tokens/tokenTransfer.ts @@ -170,6 +170,64 @@ export const erc1155D: TokenTransfer = { total: { token_id: '456', value: '42', decimals: null }, }; +export const erc404A: TokenTransfer = { + from: { + hash: '0x0000000000000000000000000000000000000000', + implementation_name: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + to: { + hash: '0xBb36c792B9B45Aaf8b848A1392B0d6559202729E', + implementation_name: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: 'kitty.kitty.cat.eth', + }, + token: { + address: '0xF56b7693E4212C584de4a83117f805B8E89224CB', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders: '1', + name: null, + symbol: 'MY_SYMBOL_IS_VERY_LONG', + type: 'ERC-404', + total_supply: '0', + icon_url: null, + }, + total: { + value: '42000000000000000000000000', + decimals: '18', + token_id: null, + }, + tx_hash: '0x05d6589367633c032d757a69c5fb16c0e33e3994b0d9d1483f82aeee1f05d746', + type: 'token_transfer', + method: 'swap', + timestamp: '2022-10-10T14:34:30.000000Z', + block_hash: '1', + log_index: '1', +}; + +export const erc404B: TokenTransfer = { + ...erc404A, + token: { + ...erc404A.token, + name: 'SastanaNFT', + symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY', + }, + total: { token_id: '4625304364899952' }, +}; + export const mixTokens: TokenTransferResponse = { items: [ erc20, @@ -178,6 +236,8 @@ export const mixTokens: TokenTransferResponse = { erc1155B, erc1155C, erc1155D, + erc404A, + erc404B, ], next_page_params: null, }; diff --git a/mocks/txs/stats.ts b/mocks/txs/stats.ts new file mode 100644 index 0000000000..7b05dc975a --- /dev/null +++ b/mocks/txs/stats.ts @@ -0,0 +1,8 @@ +import type { TransactionsStats } from 'types/api/transaction'; + +export const base: TransactionsStats = { + pending_transactions_count: '4200', + transaction_fees_avg_24h: '22342870314428', + transaction_fees_sum_24h: '22184012506492688277', + transactions_count_24h: '992890', +}; diff --git a/mocks/txs/tx.ts b/mocks/txs/tx.ts index 3ca7876b99..0b97508282 100644 --- a/mocks/txs/tx.ts +++ b/mocks/txs/tx.ts @@ -127,6 +127,8 @@ export const withTokenTransfer: Transaction = { tokenTransferMock.erc1155B, tokenTransferMock.erc1155C, tokenTransferMock.erc1155D, + tokenTransferMock.erc404A, + tokenTransferMock.erc404B, ], token_transfers_overflow: true, tx_types: [ @@ -341,3 +343,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/withdrawals/withdrawals.ts b/mocks/withdrawals/withdrawals.ts index 37d1da51e1..d58d901390 100644 --- a/mocks/withdrawals/withdrawals.ts +++ b/mocks/withdrawals/withdrawals.ts @@ -1,4 +1,7 @@ -export const data = { +import type { AddressParam } from 'types/api/addressParams'; +import type { WithdrawalsResponse } from 'types/api/withdrawals'; + +export const data: WithdrawalsResponse = { items: [ { amount: '192175000000000', @@ -10,7 +13,7 @@ export const data = { is_contract: false, is_verified: null, name: null, - }, + } as AddressParam, timestamp: '2022-06-07T18:12:24.000000Z', validator_index: 49622, }, @@ -24,7 +27,7 @@ export const data = { is_contract: false, is_verified: null, name: null, - }, + } as AddressParam, timestamp: '2022-05-07T18:12:24.000000Z', validator_index: 49621, }, @@ -38,7 +41,7 @@ export const data = { is_contract: false, is_verified: null, name: null, - }, + } as AddressParam, timestamp: '2022-04-07T18:12:24.000000Z', validator_index: 49620, }, diff --git a/mocks/zkEvm/deposits.ts b/mocks/zkEvm/deposits.ts new file mode 100644 index 0000000000..91ecc077c3 --- /dev/null +++ b/mocks/zkEvm/deposits.ts @@ -0,0 +1,28 @@ +import type { ZkEvmL2DepositsResponse } from 'types/api/zkEvmL2'; + +export const baseResponse: ZkEvmL2DepositsResponse = { + items: [ + { + block_number: 19681943, + index: 182177, + l1_transaction_hash: '0x29074452f976064aca1ca5c6e7c82d890c10454280693e6eca0257ae000c8e85', + l2_transaction_hash: null, + symbol: 'DAI', + timestamp: '2022-04-18T11:08:11.000000Z', + value: '0.003', + }, + { + block_number: 19681894, + index: 182176, + l1_transaction_hash: '0x0b7d58c0a6b4695ba28d99df928591fb931c812c0aab6d0093ff5040d2f9bc5e', + l2_transaction_hash: '0x210d9f70f411de1079e32a98473b04345a5ea6ff2340a8511ebc2df641274436', + symbol: 'ETH', + timestamp: '2022-04-18T10:58:23.000000Z', + value: '0.0046651390188845', + }, + ], + next_page_params: { + items_count: 50, + index: 1, + }, +}; diff --git a/mocks/zkEvm/txnBatches.ts b/mocks/zkEvm/txnBatches.ts new file mode 100644 index 0000000000..bcaf55d941 --- /dev/null +++ b/mocks/zkEvm/txnBatches.ts @@ -0,0 +1,40 @@ +import type { ZkEvmL2TxnBatch, ZkEvmL2TxnBatchesResponse } from 'types/api/zkEvmL2'; + +export const txnBatchData: ZkEvmL2TxnBatch = { + acc_input_hash: '0x4bf88aabe33713b7817266d7860912c58272d808da7397cdc627ca53b296fad3', + global_exit_root: '0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5', + number: 5, + sequence_tx_hash: '0x7ae010e9758441b302db10282807358af460f38c49c618d26a897592f64977f7', + state_root: '0x183b4a38a4a6027947ceb93b323cc94c548c8c05cf605d73ca88351d77cae1a3', + status: 'Finalized', + timestamp: '2023-10-20T10:08:18.000000Z', + transactions: [ + '0xb5d432c270057c223b973f3b5f00dbad32823d9ef26f3e8d97c819c7c573453a', + ], + verify_tx_hash: '0x6f7eeaa0eb966e63d127bba6bf8f9046d303c2a1185b542f0b5083f682a0e87f', +}; + +export const txnBatchesData: ZkEvmL2TxnBatchesResponse = { + items: [ + { + timestamp: '2023-06-01T14:46:48.000000Z', + status: 'Finalized', + verify_tx_hash: '0x48139721f792d3a68c3781b4cf50e66e8fc7dbb38adff778e09066ea5be9adb8', + sequence_tx_hash: '0x6aa081e8e33a085e4ec7124fcd8a5f7d36aac0828f176e80d4b70e313a11695b', + number: 5218590, + tx_count: 9, + }, + { + timestamp: '2023-06-01T14:46:48.000000Z', + status: 'Unfinalized', + verify_tx_hash: null, + sequence_tx_hash: null, + number: 5218591, + tx_count: 9, + }, + ], + next_page_params: { + number: 5902834, + items_count: 50, + }, +}; diff --git a/mocks/zkEvm/withdrawals.ts b/mocks/zkEvm/withdrawals.ts new file mode 100644 index 0000000000..c89635f4e5 --- /dev/null +++ b/mocks/zkEvm/withdrawals.ts @@ -0,0 +1,28 @@ +import type { ZkEvmL2WithdrawalsResponse } from 'types/api/zkEvmL2'; + +export const baseResponse: ZkEvmL2WithdrawalsResponse = { + items: [ + { + block_number: 11722417, + index: 47040, + l1_transaction_hash: null, + l2_transaction_hash: '0x68c378e412e51553524545ef1d3f00f69496fb37827c0b3b7e0870d245970408', + symbol: 'ETH', + timestamp: '2022-04-18T09:20:37.000000Z', + value: '0.025', + }, + { + block_number: 11722480, + index: 47041, + l1_transaction_hash: '0xbf76feb85b8b8f24dacb17f962dd359f82efc512928d7b11ffca92fb812ad6a5', + l2_transaction_hash: '0xfe3c168ac1751b8399f1e819f1d83ee4cf764128bc604d454abee29114dabf49', + symbol: 'ETH', + timestamp: '2022-04-18T09:23:45.000000Z', + value: '4', + }, + ], + next_page_params: { + items_count: 50, + index: 1, + }, +}; diff --git a/mocks/zkSync/zkSyncTxnBatch.ts b/mocks/zkSync/zkSyncTxnBatch.ts new file mode 100644 index 0000000000..ab865dc513 --- /dev/null +++ b/mocks/zkSync/zkSyncTxnBatch.ts @@ -0,0 +1,20 @@ +import type { ZkSyncBatch } from 'types/api/zkSyncL2'; + +export const base: ZkSyncBatch = { + commit_transaction_hash: '0x7cd80c88977c2b310f79196b0b2136da18012be015ce80d0d9e9fe6cfad52b16', + commit_transaction_timestamp: '2022-03-19T09:37:38.726996Z', + end_block: 1245490, + execute_transaction_hash: '0x110b9a19afbabd5818a996ab2b493a9b23c888d73d95f1ab5272dbae503e103a', + execute_transaction_timestamp: '2022-03-19T10:29:05.358066Z', + l1_gas_price: '4173068062', + l1_tx_count: 0, + l2_fair_gas_price: '100000000', + l2_tx_count: 287, + number: 8051, + prove_transaction_hash: '0xb424162ba5afe17c710dceb5fc8d15d7d46a66223454dae8c74aa39f6802625b', + prove_transaction_timestamp: '2022-03-19T10:29:05.279179Z', + root_hash: '0x108c635b94f941fcabcb85500daec2f6be4f0747dff649b1cdd9dd7a7a264792', + start_block: 1245209, + status: 'Executed on L1', + timestamp: '2022-03-19T09:05:49.000000Z', +}; diff --git a/mocks/zkSync/zkSyncTxnBatches.ts b/mocks/zkSync/zkSyncTxnBatches.ts new file mode 100644 index 0000000000..a717308641 --- /dev/null +++ b/mocks/zkSync/zkSyncTxnBatches.ts @@ -0,0 +1,49 @@ +import type { ZkSyncBatchesItem, ZkSyncBatchesResponse } from 'types/api/zkSyncL2'; + +export const sealed: ZkSyncBatchesItem = { + commit_transaction_hash: null, + commit_transaction_timestamp: null, + execute_transaction_hash: null, + execute_transaction_timestamp: null, + number: 8055, + prove_transaction_hash: null, + prove_transaction_timestamp: null, + status: 'Sealed on L2', + timestamp: '2022-03-19T12:53:36.000000Z', + tx_count: 738, +}; + +export const sent: ZkSyncBatchesItem = { + commit_transaction_hash: '0x262e7215739d6a7e33b2c20b45a838801a0f5f080f20bec8e54eb078420c4661', + commit_transaction_timestamp: '2022-03-19T13:09:07.357570Z', + execute_transaction_hash: null, + execute_transaction_timestamp: null, + number: 8054, + prove_transaction_hash: null, + prove_transaction_timestamp: null, + status: 'Sent to L1', + timestamp: '2022-03-19T11:36:45.000000Z', + tx_count: 766, +}; + +export const executed: ZkSyncBatchesItem = { + commit_transaction_hash: '0xa2628f477e1027ac1c60fa75c186b914647769ac1cb9c7e1cab50b13506a0035', + commit_transaction_timestamp: '2022-03-19T11:52:18.963659Z', + execute_transaction_hash: '0xb7bd6b2b17498c66d3f6e31ac3685133a81b7f728d4f6a6f42741daa257d0d68', + execute_transaction_timestamp: '2022-03-19T13:28:16.712656Z', + number: 8053, + prove_transaction_hash: '0x9d44f2b775bd771f8a53205755b3897929aa672d2cd419b3b988c16d41d4f21e', + prove_transaction_timestamp: '2022-03-19T13:28:16.603104Z', + status: 'Executed on L1', + timestamp: '2022-03-19T10:01:52.000000Z', + tx_count: 1071, +}; + +export const baseResponse: ZkSyncBatchesResponse = { + items: [ + sealed, + sent, + executed, + ], + next_page_params: null, +}; diff --git a/mocks/zkevmL2txnBatches/zkevmL2txnBatch.ts b/mocks/zkevmL2txnBatches/zkevmL2txnBatch.ts deleted file mode 100644 index 56a0f67c33..0000000000 --- a/mocks/zkevmL2txnBatches/zkevmL2txnBatch.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { ZkEvmL2TxnBatch } from 'types/api/zkEvmL2'; - -export const txnBatchData: ZkEvmL2TxnBatch = { - acc_input_hash: '0x4bf88aabe33713b7817266d7860912c58272d808da7397cdc627ca53b296fad3', - global_exit_root: '0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5', - number: 5, - sequence_tx_hash: '0x7ae010e9758441b302db10282807358af460f38c49c618d26a897592f64977f7', - state_root: '0x183b4a38a4a6027947ceb93b323cc94c548c8c05cf605d73ca88351d77cae1a3', - status: 'Finalized', - timestamp: '2023-10-20T10:08:18.000000Z', - transactions: [ - '0xb5d432c270057c223b973f3b5f00dbad32823d9ef26f3e8d97c819c7c573453a', - ], - verify_tx_hash: '0x6f7eeaa0eb966e63d127bba6bf8f9046d303c2a1185b542f0b5083f682a0e87f', -}; diff --git a/mocks/zkevmL2txnBatches/zkevmL2txnBatches.ts b/mocks/zkevmL2txnBatches/zkevmL2txnBatches.ts deleted file mode 100644 index 895f9a7744..0000000000 --- a/mocks/zkevmL2txnBatches/zkevmL2txnBatches.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ZkEvmL2TxnBatchesResponse } from 'types/api/zkEvmL2'; - -export const txnBatchesData: ZkEvmL2TxnBatchesResponse = { - items: [ - { - timestamp: '2023-06-01T14:46:48.000000Z', - status: 'Finalized', - verify_tx_hash: '0x48139721f792d3a68c3781b4cf50e66e8fc7dbb38adff778e09066ea5be9adb8', - sequence_tx_hash: '0x6aa081e8e33a085e4ec7124fcd8a5f7d36aac0828f176e80d4b70e313a11695b', - number: 5218590, - tx_count: 9, - }, - { - timestamp: '2023-06-01T14:46:48.000000Z', - status: 'Unfinalized', - verify_tx_hash: null, - sequence_tx_hash: null, - number: 5218591, - tx_count: 9, - }, - ], - next_page_params: { - number: 5902834, - items_count: 50, - }, -}; diff --git a/next.config.js b/next.config.js index 8789a1a641..79d1b38dca 100644 --- a/next.config.js +++ b/next.config.js @@ -10,6 +10,7 @@ const headers = require('./nextjs/headers'); const redirects = require('./nextjs/redirects'); const rewrites = require('./nextjs/rewrites'); +/** @type {import('next').NextConfig} */ const moduleExports = { transpilePackages: [ 'react-syntax-highlighter', @@ -46,6 +47,14 @@ const moduleExports = { productionBrowserSourceMaps: true, experimental: { instrumentationHook: true, + turbo: { + rules: { + '*.svg': { + loaders: [ '@svgr/webpack' ], + as: '*.js', + }, + }, + }, }, }; diff --git a/nextjs/PageNextJs.tsx b/nextjs/PageNextJs.tsx index 1c15c9f419..fe99868cb7 100644 --- a/nextjs/PageNextJs.tsx +++ b/nextjs/PageNextJs.tsx @@ -2,21 +2,26 @@ import Head from 'next/head'; import React from 'react'; import type { Route } from 'nextjs-routes'; +import type { Props as PageProps } from 'nextjs/getServerSideProps'; +import config from 'configs/app'; import useAdblockDetect from 'lib/hooks/useAdblockDetect'; import useGetCsrfToken from 'lib/hooks/useGetCsrfToken'; import * as metadata from 'lib/metadata'; import * as mixpanel from 'lib/mixpanel'; import { init as initSentry } from 'lib/sentry/config'; -type Props = Route & { +interface Props { + pathname: Pathname; children: React.ReactNode; + query?: PageProps['query']; + apiData?: PageProps['apiData']; } initSentry(); -const PageNextJs = (props: Props) => { - const { title, description, opengraph } = metadata.generate(props); +const PageNextJs = (props: Props) => { + const { title, description, opengraph } = metadata.generate(props, props.apiData); useGetCsrfToken(); useAdblockDetect(); @@ -34,9 +39,14 @@ const PageNextJs = (props: Props) => { { opengraph.description && } + + + { /* Twitter Meta Tags */ } + + + { opengraph.description && } - { props.children } diff --git a/nextjs/csp/policies/ad.ts b/nextjs/csp/policies/ad.ts index cd1dd5aa8a..24ac82a0bb 100644 --- a/nextjs/csp/policies/ad.ts +++ b/nextjs/csp/policies/ad.ts @@ -13,12 +13,20 @@ export function ad(): CspDev.DirectiveDescriptor { '*.coinzilla.com', 'https://request-global.czilladx.com', + // adbutler + 'servedbyadbutler.com', + // slise '*.slise.xyz', // hype 'api.hypelab.com', '*.ixncdn.com', + '*.cloudfront.net', + + //getit + 'v1.getittech.io', + 'ipapi.co', ], 'frame-src': [ // coinzilla diff --git a/nextjs/csp/policies/walletConnect.ts b/nextjs/csp/policies/walletConnect.ts index 41cb948066..e4c0c0e3fa 100644 --- a/nextjs/csp/policies/walletConnect.ts +++ b/nextjs/csp/policies/walletConnect.ts @@ -16,6 +16,10 @@ export function walletConnect(): CspDev.DirectiveDescriptor { 'wss://relay.walletconnect.com', 'wss://www.walletlink.org', ], + 'frame-ancestors': [ + '*.walletconnect.org', + '*.walletconnect.com', + ], 'img-src': [ KEY_WORDS.BLOB, '*.walletconnect.com', diff --git a/nextjs/getServerSideProps.ts b/nextjs/getServerSideProps.ts index 413b19bf29..d353a9f67b 100644 --- a/nextjs/getServerSideProps.ts +++ b/nextjs/getServerSideProps.ts @@ -1,30 +1,47 @@ -import type { GetServerSideProps } from 'next'; +import type { GetServerSideProps, GetServerSidePropsContext, GetServerSidePropsResult } from 'next'; + +import type { AdBannerProviders } from 'types/client/adProviders'; + +import type { Route } from 'nextjs-routes'; import config from 'configs/app'; +import isNeedProxy from 'lib/api/isNeedProxy'; const rollupFeature = config.features.rollup; +const adBannerFeature = config.features.adsBanner; +import type * as metadata from 'lib/metadata'; -export type Props = { +export interface Props { + query: Route['query']; cookies: string; referrer: string; - id: string; - height_or_hash: string; - hash: string; - number: string; - q: string; - name: string; + adBannerProvider: AdBannerProviders | undefined; + // if apiData is undefined, Next.js will complain that it is not serializable + // so we force it to be always present in the props but it can be null + apiData: metadata.ApiData | null; } -export const base: GetServerSideProps = async({ req, query }) => { +export const base = async ({ req, query }: GetServerSidePropsContext): +Promise>> => { + const adBannerProvider = (() => { + if (adBannerFeature.isEnabled) { + if ('additionalProvider' in adBannerFeature && adBannerFeature.additionalProvider) { + // we need to get a random ad provider on the server side to keep it consistent with the client side + const randomIndex = Math.round(Math.random()); + return [ adBannerFeature.provider, adBannerFeature.additionalProvider ][randomIndex]; + } else { + return adBannerFeature.provider; + } + } + return; + })(); + return { props: { + query, cookies: req.headers.cookie || '', referrer: req.headers.referer || '', - id: query.id?.toString() || '', - hash: query.hash?.toString() || '', - height_or_hash: query.height_or_hash?.toString() || '', - number: query.number?.toString() || '', - q: query.q?.toString() || '', - name: query.name?.toString() || '', + adBannerProvider, + apiData: null, }, }; }; @@ -50,7 +67,7 @@ export const verifiedAddresses: GetServerSideProps = async(context) => { }; export const deposits: GetServerSideProps = async(context) => { - if (!(rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'shibarium'))) { + if (!(rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'shibarium' || rollupFeature.type === 'zkEvm'))) { return { notFound: true, }; @@ -62,7 +79,7 @@ export const deposits: GetServerSideProps = async(context) => { export const withdrawals: GetServerSideProps = async(context) => { if ( !config.features.beaconChain.isEnabled && - !(rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'shibarium')) + !(rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'shibarium' || rollupFeature.type === 'zkEvm')) ) { return { notFound: true, @@ -92,8 +109,8 @@ export const optimisticRollup: GetServerSideProps = async(context) => { return base(context); }; -export const zkEvmRollup: GetServerSideProps = async(context) => { - if (!(rollupFeature.isEnabled && rollupFeature.type === 'zkEvm')) { +export const batch: GetServerSideProps = async(context) => { + if (!(rollupFeature.isEnabled && (rollupFeature.type === 'zkEvm' || rollupFeature.type === 'zkSync'))) { return { notFound: true, }; @@ -102,14 +119,15 @@ export const zkEvmRollup: GetServerSideProps = async(context) => { return base(context); }; -export const marketplace: GetServerSideProps = async(context) => { +export const marketplace = async (context: GetServerSidePropsContext): +Promise>> => { if (!config.features.marketplace.isEnabled) { return { notFound: true, }; } - return base(context); + return base(context); }; export const apiDocs: GetServerSideProps = async(context) => { @@ -201,3 +219,24 @@ export const gasTracker: GetServerSideProps = async(context) => { return base(context); }; + +export const dataAvailability: GetServerSideProps = async(context) => { + if (!config.features.dataAvailability.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const login: GetServerSideProps = async(context) => { + + if (!isNeedProxy()) { + return { + notFound: true, + }; + } + + return base(context); +}; diff --git a/nextjs/nextjs-routes.d.ts b/nextjs/nextjs-routes.d.ts index 966e57ddba..5f5a8edd27 100644 --- a/nextjs/nextjs-routes.d.ts +++ b/nextjs/nextjs-routes.d.ts @@ -18,7 +18,9 @@ declare module "nextjs-routes" { | DynamicRoute<"/address/[hash]", { "hash": string }> | StaticRoute<"/api/csrf"> | StaticRoute<"/api/healthz"> + | StaticRoute<"/api/log"> | StaticRoute<"/api/media-type"> + | StaticRoute<"/api/metrics"> | StaticRoute<"/api/proxy"> | StaticRoute<"/api-docs"> | DynamicRoute<"/apps/[id]", { "id": string }> @@ -28,6 +30,7 @@ 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"> diff --git a/nextjs/types.ts b/nextjs/types.ts index 60c007daff..c0366ae090 100644 --- a/nextjs/types.ts +++ b/nextjs/types.ts @@ -1,6 +1,13 @@ import type { NextPage } from 'next'; +import type { Route } from 'nextjs-routes'; + // eslint-disable-next-line @typescript-eslint/ban-types export type NextPageWithLayout

= NextPage & { getLayout?: (page: React.ReactElement) => React.ReactNode; } + +export interface RouteParams { + pathname: Pathname; + query?: Route['query']; +} diff --git a/nextjs/utils/detectBotRequest.ts b/nextjs/utils/detectBotRequest.ts new file mode 100644 index 0000000000..1eecf6333a --- /dev/null +++ b/nextjs/utils/detectBotRequest.ts @@ -0,0 +1,52 @@ +import type { IncomingMessage } from 'http'; + +type SocialPreviewBot = 'twitter' | 'facebook' | 'telegram' | 'slack'; +type SearchEngineBot = 'google' | 'bing' | 'yahoo' | 'duckduckgo'; + +type ReturnType = { + type: 'social_preview'; + bot: SocialPreviewBot; +} | { + type: 'search_engine'; + bot: SearchEngineBot; +} | undefined + +export default function detectBotRequest(req: IncomingMessage): ReturnType { + const userAgent = req.headers['user-agent']; + + if (!userAgent) { + return; + } + + if (userAgent.toLowerCase().includes('twitter')) { + return { type: 'social_preview', bot: 'twitter' }; + } + + if (userAgent.toLowerCase().includes('facebook')) { + return { type: 'social_preview', bot: 'facebook' }; + } + + if (userAgent.toLowerCase().includes('telegram')) { + return { type: 'social_preview', bot: 'telegram' }; + } + + if (userAgent.toLowerCase().includes('slack')) { + return { type: 'social_preview', bot: 'slack' }; + } + + if (userAgent.toLowerCase().includes('googlebot')) { + return { type: 'search_engine', bot: 'google' }; + } + + if (userAgent.toLowerCase().includes('bingbot')) { + return { type: 'search_engine', bot: 'bing' }; + } + + if (userAgent.toLowerCase().includes('yahoo')) { + return { type: 'search_engine', bot: 'yahoo' }; + } + + if (userAgent.toLowerCase().includes('duckduck')) { + return { type: 'search_engine', bot: 'duckduckgo' }; + } +} diff --git a/nextjs/utils/fetchApi.ts b/nextjs/utils/fetchApi.ts new file mode 100644 index 0000000000..63eff42384 --- /dev/null +++ b/nextjs/utils/fetchApi.ts @@ -0,0 +1,53 @@ +import fetch, { AbortError } from 'node-fetch'; + +import buildUrl from 'nextjs/utils/buildUrl'; +import { httpLogger } from 'nextjs/utils/logger'; + +import { RESOURCES } from 'lib/api/resources'; +import type { ResourceName, ResourcePathParams, ResourcePayload } from 'lib/api/resources'; +import { SECOND } from 'lib/consts'; +import metrics from 'lib/monitoring/metrics'; + +type Params = ( + { + resource: R; + pathParams?: ResourcePathParams; + } | { + url: string; + route: string; + } +) & { + timeout?: number; +} + +export default async function fetchApi>(params: Params): Promise { + const controller = new AbortController(); + + const timeout = setTimeout(() => { + controller.abort(); + }, params.timeout || SECOND); + + const url = 'url' in params ? params.url : buildUrl(params.resource, params.pathParams); + const route = 'route' in params ? params.route : RESOURCES[params.resource]['path']; + + const end = metrics?.apiRequestDuration.startTimer(); + + try { + const response = await fetch(url, { signal: controller.signal }); + + const duration = end?.({ route, code: response.status }); + if (response.status === 200) { + httpLogger.logger.info({ message: 'API fetch', url, code: response.status, duration }); + } else { + httpLogger.logger.error({ message: 'API fetch', url, code: response.status, duration }); + } + + return await response.json() as Promise; + } catch (error) { + const code = error instanceof AbortError ? 504 : 500; + const duration = end?.({ route, code }); + httpLogger.logger.error({ message: 'API fetch', url, code, duration }); + } finally { + clearTimeout(timeout); + } +} diff --git a/nextjs/utils/fetch.ts b/nextjs/utils/fetchProxy.ts similarity index 91% rename from nextjs/utils/fetch.ts rename to nextjs/utils/fetchProxy.ts index 22bfc32ff6..0081781215 100644 --- a/nextjs/utils/fetch.ts +++ b/nextjs/utils/fetchProxy.ts @@ -30,14 +30,9 @@ export default function fetchFactory( }; httpLogger.logger.info({ - message: 'Trying to call API', + message: 'API fetch via Next.js proxy', url, - req: _req, - }); - - httpLogger.logger.info({ - message: 'API request headers', - headers, + // headers, }); const body = (() => { diff --git a/nextjs/utils/logRequestFromBot.ts b/nextjs/utils/logRequestFromBot.ts new file mode 100644 index 0000000000..6c22b63258 --- /dev/null +++ b/nextjs/utils/logRequestFromBot.ts @@ -0,0 +1,28 @@ +import type { IncomingMessage, ServerResponse } from 'http'; + +import metrics from 'lib/monitoring/metrics'; + +import detectBotRequest from './detectBotRequest'; + +export default async function logRequestFromBot(req: IncomingMessage | undefined, res: ServerResponse | undefined, pathname: string) { + if (!req || !res || !metrics) { + return; + } + + const botInfo = detectBotRequest(req); + + if (!botInfo) { + return; + } + + switch (botInfo.type) { + case 'search_engine': { + metrics.searchEngineBotRequests.inc({ route: pathname, bot: botInfo.bot }); + return; + } + case 'social_preview': { + metrics.socialPreviewBotRequests.inc({ route: pathname, bot: botInfo.bot }); + return; + } + } +} diff --git a/package.json b/package.json index 1d4db0acbd..fde1113c2e 100644 --- a/package.json +++ b/package.json @@ -25,11 +25,14 @@ "test:pw": "./tools/scripts/pw.sh", "test:pw:local": "export NODE_PATH=$(pwd)/node_modules && yarn test:pw", "test:pw:docker": "docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-focal ./tools/scripts/pw.docker.sh", + "test:pw:docker:deps": "docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-focal ./tools/scripts/pw.docker.deps.sh", "test:pw:ci": "yarn test:pw --project=$PW_PROJECT", "test:pw:detect-affected": "node ./deploy/tools/affected-tests/index.js", "test:jest": "jest", "test:jest:watch": "jest --watch", - "favicon:generate:dev": "./tools/scripts/favicon-generator.dev.sh" + "favicon:generate:dev": "./tools/scripts/favicon-generator.dev.sh", + "monitoring:prometheus:local": "docker run --name blockscout_prometheus -d -p 127.0.0.1:9090:9090 -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus", + "monitoring:grafana:local": "docker run -d -p 4000:3000 --name=blockscout_grafana --user $(id -u) --volume $(pwd)/grafana:/var/lib/grafana grafana/grafana-enterprise" }, "dependencies": { "@chakra-ui/react": "2.7.1", @@ -41,7 +44,7 @@ "@metamask/post-message-stream": "^7.0.0", "@metamask/providers": "^10.2.1", "@monaco-editor/react": "^4.4.6", - "@next/bundle-analyzer": "^14.0.1", + "@next/bundle-analyzer": "14.2.3", "@opentelemetry/auto-instrumentations-node": "^0.39.4", "@opentelemetry/exporter-metrics-otlp-proto": "^0.45.1", "@opentelemetry/exporter-trace-otlp-http": "^0.45.0", @@ -57,26 +60,28 @@ "@tanstack/react-query-devtools": "^5.4.3", "@types/papaparse": "^5.3.5", "@types/react-scroll": "^1.8.4", - "@web3modal/wagmi": "3.5.0", + "@web3modal/wagmi": "4.1.3", "bignumber.js": "^9.1.0", "blo": "^1.1.1", "chakra-react-select": "^4.4.3", "crypto-js": "^4.2.0", "d3": "^7.6.1", - "dappscout-iframe": "^0.1.0", + "dappscout-iframe": "0.2.1", "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", + "next": "14.2.3", "nextjs-routes": "^1.0.8", "node-fetch": "^3.2.9", "papaparse": "^5.3.2", @@ -84,6 +89,7 @@ "phoenix": "^1.6.15", "pino-http": "^8.2.1", "pino-pretty": "^9.1.1", + "prom-client": "15.1.1", "qrcode": "^1.5.1", "react": "18.2.0", "react-device-detect": "^2.2.3", @@ -97,8 +103,8 @@ "react-scroll": "^1.8.7", "swagger-ui-react": "^5.9.0", "use-font-face-observer": "^1.2.1", - "viem": "1.20.1", - "wagmi": "1.4.12", + "viem": "2.9.6", + "wagmi": "2.5.16", "xss": "^1.0.14" }, "devDependencies": { @@ -146,7 +152,7 @@ "svgo": "^2.8.0", "ts-jest": "^29.0.3", "ts-node": "^10.9.1", - "typescript": "^5.1.0", + "typescript": "5.4.2", "vite-plugin-svgr": "^2.2.2", "vite-tsconfig-paths": "^3.5.2", "ws": "^8.11.0" diff --git a/pages/_app.tsx b/pages/_app.tsx index 5086499177..38b3c1d358 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -12,9 +12,11 @@ import config from 'configs/app'; import useQueryClientConfig from 'lib/api/useQueryClientConfig'; import { AppContextProvider } from 'lib/contexts/app'; import { ChakraProvider } from 'lib/contexts/chakra'; +import { MarketplaceContextProvider } from 'lib/contexts/marketplace'; 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 +46,7 @@ const ERROR_SCREEN_STYLES: ChakraProps = { function MyApp({ Component, pageProps }: AppPropsWithLayout) { useLoadFeatures(); + useNotifyOnNavigation(); const queryClient = useQueryClientConfig(); @@ -65,7 +68,9 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { - { getLayout() } + + { getLayout() } + diff --git a/pages/_document.tsx b/pages/_document.tsx index c309324515..9ad96d5c25 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -3,6 +3,7 @@ import type { DocumentContext } from 'next/document'; import Document, { Html, Head, Main, NextScript } from 'next/document'; import React from 'react'; +import logRequestFromBot from 'nextjs/utils/logRequestFromBot'; import * as serverTiming from 'nextjs/utils/serverTiming'; import config from 'configs/app'; @@ -22,6 +23,8 @@ class MyDocument extends Document { return result; }; + await logRequestFromBot(ctx.req, ctx.res, ctx.pathname); + const initialProps = await Document.getInitialProps(ctx); return initialProps; diff --git a/pages/address/[hash]/contract-verification.tsx b/pages/address/[hash]/contract-verification.tsx index 7e7fd704d2..25f56fa4c6 100644 --- a/pages/address/[hash]/contract-verification.tsx +++ b/pages/address/[hash]/contract-verification.tsx @@ -8,7 +8,7 @@ import ContractVerificationForAddress from 'ui/pages/ContractVerificationForAddr const Page: NextPage = (props: Props) => { return ( - + ); diff --git a/pages/address/[hash]/index.tsx b/pages/address/[hash]/index.tsx index 2319a31605..fc13029cc0 100644 --- a/pages/address/[hash]/index.tsx +++ b/pages/address/[hash]/index.tsx @@ -1,15 +1,22 @@ -import type { NextPage } from 'next'; -import dynamic from 'next/dynamic'; +import type { GetServerSideProps, NextPage } from 'next'; import React from 'react'; +import type { Route } from 'nextjs-routes'; import type { Props } from 'nextjs/getServerSideProps'; +import * as gSSP from 'nextjs/getServerSideProps'; import PageNextJs from 'nextjs/PageNextJs'; +import detectBotRequest from 'nextjs/utils/detectBotRequest'; +import fetchApi from 'nextjs/utils/fetchApi'; -const Address = dynamic(() => import('ui/pages/Address'), { ssr: false }); +import config from 'configs/app'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import Address from 'ui/pages/Address'; -const Page: NextPage = (props: Props) => { +const pathname: Route['pathname'] = '/address/[hash]'; + +const Page: NextPage> = (props: Props) => { return ( - +

); @@ -17,4 +24,24 @@ const Page: NextPage = (props: Props) => { export default Page; -export { base as getServerSideProps } from 'nextjs/getServerSideProps'; +export const getServerSideProps: GetServerSideProps> = async(ctx) => { + const baseResponse = await gSSP.base(ctx); + + if (config.meta.og.enhancedDataEnabled && 'props' in baseResponse) { + const botInfo = detectBotRequest(ctx.req); + + if (botInfo?.type === 'social_preview') { + const addressData = await fetchApi({ + resource: 'address', + pathParams: { hash: getQueryParamString(ctx.query.hash) }, + timeout: 1_000, + }); + + (await baseResponse.props).apiData = addressData && addressData.ens_domain_name ? { + domain_name: addressData.ens_domain_name, + } : null; + } + } + + return baseResponse; +}; diff --git a/pages/api/csrf.ts b/pages/api/csrf.ts index 8d4857b6f1..409ab8a1da 100644 --- a/pages/api/csrf.ts +++ b/pages/api/csrf.ts @@ -1,7 +1,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import buildUrl from 'nextjs/utils/buildUrl'; -import fetchFactory from 'nextjs/utils/fetch'; +import fetchFactory from 'nextjs/utils/fetchProxy'; import { httpLogger } from 'nextjs/utils/logger'; export default async function csrfHandler(_req: NextApiRequest, res: NextApiResponse) { diff --git a/pages/api/log.ts b/pages/api/log.ts new file mode 100644 index 0000000000..cb89b35f59 --- /dev/null +++ b/pages/api/log.ts @@ -0,0 +1,9 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +import { httpLogger } from 'nextjs/utils/logger'; + +export default async function logHandler(req: NextApiRequest, res: NextApiResponse) { + httpLogger(req, res); + + res.status(200).send('ok'); +} diff --git a/pages/api/media-type.ts b/pages/api/media-type.ts index f64301f094..95d4b3728a 100644 --- a/pages/api/media-type.ts +++ b/pages/api/media-type.ts @@ -3,16 +3,20 @@ import nodeFetch from 'node-fetch'; import { httpLogger } from 'nextjs/utils/logger'; +import metrics from 'lib/monitoring/metrics'; import getQueryParamString from 'lib/router/getQueryParamString'; export default async function mediaTypeHandler(req: NextApiRequest, res: NextApiResponse) { - httpLogger(req, res); try { const url = getQueryParamString(req.query.url); + + const end = metrics?.apiRequestDuration.startTimer(); const response = await nodeFetch(url, { method: 'HEAD' }); + const duration = end?.({ route: '/media-type', code: response.status }); if (response.status !== 200) { + httpLogger.logger.error({ message: 'API fetch', url, code: response.status, duration }); throw new Error(); } @@ -22,12 +26,16 @@ 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'; })(); + httpLogger.logger.info({ message: 'API fetch', url, code: response.status, duration }); + res.status(200).json({ type: mediaType }); } catch (error) { res.status(200).json({ type: undefined }); diff --git a/pages/api/metrics.ts b/pages/api/metrics.ts new file mode 100644 index 0000000000..c474dd7552 --- /dev/null +++ b/pages/api/metrics.ts @@ -0,0 +1,13 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import * as promClient from 'prom-client'; + +// eslint-disable-next-line no-restricted-properties +const isEnabled = process.env.PROMETHEUS_METRICS_ENABLED === 'true'; + +isEnabled && promClient.collectDefaultMetrics({ prefix: 'frontend_' }); + +export default async function metricsHandler(req: NextApiRequest, res: NextApiResponse) { + const metrics = await promClient.register.metrics(); + res.setHeader('Content-type', promClient.register.contentType); + res.send(metrics); +} diff --git a/pages/api/proxy.ts b/pages/api/proxy.ts index 0756f3639c..95b168413b 100644 --- a/pages/api/proxy.ts +++ b/pages/api/proxy.ts @@ -2,7 +2,7 @@ import _pick from 'lodash/pick'; import _pickBy from 'lodash/pickBy'; import type { NextApiRequest, NextApiResponse } from 'next'; -import fetchFactory from 'nextjs/utils/fetch'; +import fetchFactory from 'nextjs/utils/fetchProxy'; import appConfig from 'configs/app'; diff --git a/pages/apps/[id].tsx b/pages/apps/[id].tsx index f99be74026..936c485f01 100644 --- a/pages/apps/[id].tsx +++ b/pages/apps/[id].tsx @@ -1,18 +1,29 @@ +import type { GetServerSideProps } from 'next'; import dynamic from 'next/dynamic'; import React from 'react'; import type { NextPageWithLayout } from 'nextjs/types'; +import type { MarketplaceAppOverview } from 'types/client/marketplace'; +import type { Route } from 'nextjs-routes'; +import * as gSSP from 'nextjs/getServerSideProps'; import type { Props } from 'nextjs/getServerSideProps'; import PageNextJs from 'nextjs/PageNextJs'; +import detectBotRequest from 'nextjs/utils/detectBotRequest'; +import fetchApi from 'nextjs/utils/fetchApi'; +import config from 'configs/app'; +import getQueryParamString from 'lib/router/getQueryParamString'; import LayoutApp from 'ui/shared/layout/LayoutApp'; const MarketplaceApp = dynamic(() => import('ui/pages/MarketplaceApp'), { ssr: false }); -const Page: NextPageWithLayout = (props: Props) => { +const pathname: Route['pathname'] = '/apps/[id]'; +const feature = config.features.marketplace; + +const Page: NextPageWithLayout> = (props: Props) => { return ( - + ); @@ -28,4 +39,40 @@ Page.getLayout = function getLayout(page: React.ReactElement) { export default Page; -export { marketplace as getServerSideProps } from 'nextjs/getServerSideProps'; +export const getServerSideProps: GetServerSideProps> = async(ctx) => { + const baseResponse = await gSSP.marketplace(ctx); + + if (config.meta.og.enhancedDataEnabled && 'props' in baseResponse && feature.isEnabled) { + const botInfo = detectBotRequest(ctx.req); + + if (botInfo?.type === 'social_preview') { + + const appData = await(async() => { + if ('configUrl' in feature) { + const appList = await fetchApi>({ + url: config.app.baseUrl + feature.configUrl, + route: '/marketplace_config', + timeout: 1_000, + }); + + if (appList && Array.isArray(appList)) { + return appList.find(app => app.id === getQueryParamString(ctx.query.id)); + } + + } else { + return await fetchApi({ + resource: 'marketplace_dapp', + pathParams: { dappId: getQueryParamString(ctx.query.id), chainId: config.chain.id }, + timeout: 1_000, + }); + } + })(); + + (await baseResponse.props).apiData = appData && appData.title ? { + app_name: appData.title, + } : null; + } + } + + return baseResponse; +}; diff --git a/pages/batches/[number].tsx b/pages/batches/[number].tsx index 68e4356a78..814f8358ea 100644 --- a/pages/batches/[number].tsx +++ b/pages/batches/[number].tsx @@ -5,16 +5,32 @@ import React from 'react'; import type { Props } from 'nextjs/getServerSideProps'; import PageNextJs from 'nextjs/PageNextJs'; -const ZkEvmL2TxnBatch = dynamic(() => import('ui/pages/ZkEvmL2TxnBatch'), { ssr: false }); +import config from 'configs/app'; + +const rollupFeature = config.features.rollup; + +const Batch = dynamic(() => { + if (!rollupFeature.isEnabled) { + throw new Error('Rollup feature is not enabled.'); + } + + switch (rollupFeature.type) { + case 'zkEvm': + return import('ui/pages/ZkEvmL2TxnBatch'); + case 'zkSync': + return import('ui/pages/ZkSyncL2TxnBatch'); + } + throw new Error('Txn batches feature is not enabled.'); +}, { ssr: false }); const Page: NextPage = (props: Props) => { return ( - - + + ); }; export default Page; -export { zkEvmRollup as getServerSideProps } from 'nextjs/getServerSideProps'; +export { batch as getServerSideProps } from 'nextjs/getServerSideProps'; diff --git a/pages/batches/index.tsx b/pages/batches/index.tsx index e14ff7ca49..f5bea98b4b 100644 --- a/pages/batches/index.tsx +++ b/pages/batches/index.tsx @@ -15,6 +15,8 @@ const Batches = dynamic(() => { switch (rollupFeature.type) { case 'zkEvm': return import('ui/pages/ZkEvmL2TxnBatches'); + case 'zkSync': + return import('ui/pages/ZkSyncL2TxnBatches'); case 'optimistic': return import('ui/pages/OptimisticL2TxnBatches'); } diff --git a/pages/blobs/[hash].tsx b/pages/blobs/[hash].tsx new file mode 100644 index 0000000000..c042c862aa --- /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 { dataAvailability as getServerSideProps } from 'nextjs/getServerSideProps'; diff --git a/pages/block/[height_or_hash].tsx b/pages/block/[height_or_hash].tsx index 7d74d59c49..ef2c7652d2 100644 --- a/pages/block/[height_or_hash].tsx +++ b/pages/block/[height_or_hash].tsx @@ -9,7 +9,7 @@ const Block = dynamic(() => import('ui/pages/Block'), { ssr: false }); const Page: NextPage = (props: Props) => { return ( - + ); diff --git a/pages/contract-verification.tsx b/pages/contract-verification.tsx index a14df711e1..59dc7064b7 100644 --- a/pages/contract-verification.tsx +++ b/pages/contract-verification.tsx @@ -8,7 +8,7 @@ import ContractVerification from 'ui/pages/ContractVerification'; const Page: NextPage = (props: Props) => { return ( - + ); diff --git a/pages/deposits/index.tsx b/pages/deposits/index.tsx index 789dd39657..04e5492fda 100644 --- a/pages/deposits/index.tsx +++ b/pages/deposits/index.tsx @@ -16,7 +16,11 @@ const Deposits = dynamic(() => { return import('ui/pages/ShibariumDeposits'); } - throw new Error('Withdrawals feature is not enabled.'); + if (rollupFeature.isEnabled && rollupFeature.type === 'zkEvm') { + return import('ui/pages/ZkEvmL2Deposits'); + } + + throw new Error('Deposits feature is not enabled.'); }, { ssr: false }); const Page: NextPage = () => { diff --git a/pages/login.tsx b/pages/login.tsx index a74410f613..11562f856b 100644 --- a/pages/login.tsx +++ b/pages/login.tsx @@ -15,4 +15,4 @@ const Page: NextPage = () => { export default Page; -export { base as getServerSideProps } from 'nextjs/getServerSideProps'; +export { login as getServerSideProps } from 'nextjs/getServerSideProps'; diff --git a/pages/name-domains/[name].tsx b/pages/name-domains/[name].tsx index 7a01829baa..d9346260b4 100644 --- a/pages/name-domains/[name].tsx +++ b/pages/name-domains/[name].tsx @@ -9,7 +9,7 @@ const NameDomain = dynamic(() => import('ui/pages/NameDomain'), { ssr: false }); const Page: NextPage = (props: Props) => { return ( - + ); diff --git a/pages/op/[hash].tsx b/pages/op/[hash].tsx index 63080f817c..8a37663e5e 100644 --- a/pages/op/[hash].tsx +++ b/pages/op/[hash].tsx @@ -9,7 +9,7 @@ const UserOp = dynamic(() => import('ui/pages/UserOp'), { ssr: false }); const Page: NextPage = (props: Props) => { return ( - + ); diff --git a/pages/search-results.tsx b/pages/search-results.tsx index 460b71b6e3..f69311ef51 100644 --- a/pages/search-results.tsx +++ b/pages/search-results.tsx @@ -12,7 +12,7 @@ const SearchResults = dynamic(() => import('ui/pages/SearchResults'), { ssr: fal const Page: NextPageWithLayout = (props: Props) => { return ( - + ); diff --git a/pages/token/[hash]/index.tsx b/pages/token/[hash]/index.tsx index 70f41b66fd..84fd9e7972 100644 --- a/pages/token/[hash]/index.tsx +++ b/pages/token/[hash]/index.tsx @@ -1,15 +1,22 @@ -import type { NextPage } from 'next'; -import dynamic from 'next/dynamic'; +import type { GetServerSideProps, NextPage } from 'next'; import React from 'react'; +import type { Route } from 'nextjs-routes'; import type { Props } from 'nextjs/getServerSideProps'; +import * as gSSP from 'nextjs/getServerSideProps'; import PageNextJs from 'nextjs/PageNextJs'; +import detectBotRequest from 'nextjs/utils/detectBotRequest'; +import fetchApi from 'nextjs/utils/fetchApi'; -const Token = dynamic(() => import('ui/pages/Token'), { ssr: false }); +import config from 'configs/app'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import Token from 'ui/pages/Token'; -const Page: NextPage = (props: Props) => { +const pathname: Route['pathname'] = '/token/[hash]'; + +const Page: NextPage> = (props: Props) => { return ( - + ); @@ -17,4 +24,23 @@ const Page: NextPage = (props: Props) => { export default Page; -export { base as getServerSideProps } from 'nextjs/getServerSideProps'; +export const getServerSideProps: GetServerSideProps> = async(ctx) => { + const baseResponse = await gSSP.base(ctx); + + if ('props' in baseResponse) { + if ( + config.meta.seo.enhancedDataEnabled || + (config.meta.og.enhancedDataEnabled && detectBotRequest(ctx.req)?.type === 'social_preview') + ) { + const tokenData = await fetchApi({ + resource: 'token', + pathParams: { hash: getQueryParamString(ctx.query.hash) }, + timeout: 500, + }); + + (await baseResponse.props).apiData = tokenData ?? null; + } + } + + return baseResponse; +}; diff --git a/pages/token/[hash]/instance/[id].tsx b/pages/token/[hash]/instance/[id].tsx index 62f0e71b96..973c8fd168 100644 --- a/pages/token/[hash]/instance/[id].tsx +++ b/pages/token/[hash]/instance/[id].tsx @@ -1,15 +1,24 @@ -import type { NextPage } from 'next'; +import type { GetServerSideProps, NextPage } from 'next'; import dynamic from 'next/dynamic'; import React from 'react'; +import type { Route } from 'nextjs-routes'; import type { Props } from 'nextjs/getServerSideProps'; +import * as gSSP from 'nextjs/getServerSideProps'; import PageNextJs from 'nextjs/PageNextJs'; +import detectBotRequest from 'nextjs/utils/detectBotRequest'; +import fetchApi from 'nextjs/utils/fetchApi'; + +import config from 'configs/app'; +import getQueryParamString from 'lib/router/getQueryParamString'; const TokenInstance = dynamic(() => import('ui/pages/TokenInstance'), { ssr: false }); -const Page: NextPage = (props: Props) => { +const pathname: Route['pathname'] = '/token/[hash]/instance/[id]'; + +const Page: NextPage> = (props: Props) => { return ( - + ); @@ -17,4 +26,24 @@ const Page: NextPage = (props: Props) => { export default Page; -export { base as getServerSideProps } from 'nextjs/getServerSideProps'; +export const getServerSideProps: GetServerSideProps> = async(ctx) => { + const baseResponse = await gSSP.base(ctx); + + if (config.meta.og.enhancedDataEnabled && 'props' in baseResponse) { + const botInfo = detectBotRequest(ctx.req); + + if (botInfo?.type === 'social_preview') { + const tokenData = await fetchApi({ + resource: 'token', + pathParams: { hash: getQueryParamString(ctx.query.hash) }, + timeout: 1_000, + }); + + (await baseResponse.props).apiData = tokenData && tokenData.symbol ? { + symbol: tokenData.symbol, + } : null; + } + } + + return baseResponse; +}; diff --git a/pages/tx/[hash].tsx b/pages/tx/[hash].tsx index 5eae4aedaa..f90b534fc8 100644 --- a/pages/tx/[hash].tsx +++ b/pages/tx/[hash].tsx @@ -9,7 +9,7 @@ const Transaction = dynamic(() => import('ui/pages/Transaction'), { ssr: false } const Page: NextPage = (props: Props) => { return ( - + ); diff --git a/pages/txs.tsx b/pages/txs.tsx new file mode 100644 index 0000000000..952a0b895a --- /dev/null +++ b/pages/txs.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 Transactions = dynamic(() => import('ui/pages/Transactions'), { ssr: false }); + +const Page: NextPage = () => { + return ( + + + + ); +}; + +export default Page; + +export { base as getServerSideProps } from 'nextjs/getServerSideProps'; diff --git a/pages/txs/kettle/[hash].tsx b/pages/txs/kettle/[hash].tsx index 9b62f6dd0d..35a4470cb2 100644 --- a/pages/txs/kettle/[hash].tsx +++ b/pages/txs/kettle/[hash].tsx @@ -9,7 +9,7 @@ const KettleTxs = dynamic(() => import('ui/pages/KettleTxs'), { ssr: false }); const Page: NextPage = (props: Props) => { return ( - + ); diff --git a/pages/withdrawals/index.tsx b/pages/withdrawals/index.tsx index 661ada21eb..bcfc230a76 100644 --- a/pages/withdrawals/index.tsx +++ b/pages/withdrawals/index.tsx @@ -17,6 +17,10 @@ const Withdrawals = dynamic(() => { return import('ui/pages/ShibariumWithdrawals'); } + if (rollupFeature.isEnabled && rollupFeature.type === 'zkEvm') { + return import('ui/pages/ZkEvmL2Withdrawals'); + } + if (beaconChainFeature.isEnabled) { return import('ui/pages/BeaconChainWithdrawals'); } diff --git a/playwright-ct.config.ts b/playwright-ct.config.ts index eeb35835bf..d6c7c6c231 100644 --- a/playwright-ct.config.ts +++ b/playwright-ct.config.ts @@ -4,6 +4,8 @@ import react from '@vitejs/plugin-react'; import svgr from 'vite-plugin-svgr'; import tsconfigPaths from 'vite-tsconfig-paths'; +import appConfig from 'configs/app'; + /** * See https://playwright.dev/docs/test-configuration. */ @@ -36,6 +38,7 @@ const config: PlaywrightTestConfig = defineConfig({ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { + baseURL: appConfig.app.baseUrl, /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', @@ -60,17 +63,26 @@ const config: PlaywrightTestConfig = defineConfig({ minify: false, }, resolve: { - alias: { + alias: [ // There is an issue with building these package using vite that I cannot resolve // The solution described here - https://github.com/vitejs/vite/issues/9703#issuecomment-1216662109 // doesn't seam to work well with our setup // so for now we just mock these modules in tests - '@metamask/post-message-stream': './playwright/mocks/modules/@metamask/post-message-stream.js', - '@metamask/providers': './playwright/mocks/modules/@metamask/providers.js', + { find: '@metamask/post-message-stream', replacement: './playwright/mocks/modules/@metamask/post-message-stream.js' }, + { find: '@metamask/providers', replacement: './playwright/mocks/modules/@metamask/providers.js' }, + + // '@metamask/sdk imports the browser module as UMD, but @wagmi/connectors expects it to be ESM + // so we do a little remapping here + { find: '@metamask/sdk', replacement: './node_modules/@metamask/sdk/dist/browser/es/metamask-sdk.js' }, // Mock for growthbook to test feature flags - 'lib/growthbook/useFeatureValue': './playwright/mocks/lib/growthbook/useFeatureValue.js', - }, + { find: 'lib/growthbook/useFeatureValue', replacement: './playwright/mocks/lib/growthbook/useFeatureValue.js' }, + + // The createWeb3Modal() function from web3modal/wagmi/react somehow pollutes the global styles which causes the tests to fail + // We don't call this function in TestApp and since we use useWeb3Modal() and useWeb3ModalState() hooks in the code, we have to mock the module + // Otherwise it will complain that createWeb3Modal() is no called before the hooks are used + { find: /^@web3modal\/wagmi\/react$/, replacement: './playwright/mocks/modules/@web3modal/wagmi/react.js' }, + ], }, define: { 'process.env': '__envs', // Port process.env over window.__envs diff --git a/playwright/TestApp.tsx b/playwright/TestApp.tsx index b4b246a621..ca65a6e0c6 100644 --- a/playwright/TestApp.tsx +++ b/playwright/TestApp.tsx @@ -1,21 +1,24 @@ import { ChakraProvider } from '@chakra-ui/react'; import { GrowthBookProvider } from '@growthbook/growthbook-react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { createWeb3Modal, defaultWagmiConfig } from '@web3modal/wagmi/react'; import React from 'react'; -import { WagmiConfig } from 'wagmi'; -import { mainnet } from 'wagmi/chains'; +import { http } from 'viem'; +import { WagmiProvider, createConfig } from 'wagmi'; +import { sepolia } from 'wagmi/chains'; +import { mock } from 'wagmi/connectors'; import type { Props as PageProps } from 'nextjs/getServerSideProps'; +import config from 'configs/app'; import { AppContextProvider } from 'lib/contexts/app'; import { SocketProvider } from 'lib/socket/context'; import * as app from 'playwright/utils/app'; import theme from 'theme'; -type Props = { +export type Props = { children: React.ReactNode; withSocket?: boolean; + withWalletClient?: boolean; appContext?: { pageProps: PageProps; }; @@ -25,32 +28,40 @@ const defaultAppContext = { pageProps: { cookies: '', referrer: '', - id: '', - height_or_hash: '', - hash: '', - number: '', - q: '', - name: '', + query: {}, + adBannerProvider: 'slise' as const, + apiData: null, }, }; -// >>> Web3 stuff -const chains = [ mainnet ]; -const WALLET_CONNECT_PROJECT_ID = 'PROJECT_ID'; - -const wagmiConfig = defaultWagmiConfig({ - chains, - projectId: WALLET_CONNECT_PROJECT_ID, +const wagmiConfig = createConfig({ + chains: [ sepolia ], + connectors: [ + mock({ + accounts: [ + '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + ], + }), + ], + transports: { + [sepolia.id]: http(), + }, }); -createWeb3Modal({ - wagmiConfig, - projectId: WALLET_CONNECT_PROJECT_ID, - chains, -}); -// <<<< +const WalletClientProvider = ({ children, withWalletClient }: { children: React.ReactNode; withWalletClient?: boolean }) => { + if (withWalletClient) { + return ( + + { children } + + ); + } + + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{ children }; +}; -const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props) => { +const TestApp = ({ children, withSocket, withWalletClient = true, appContext = defaultAppContext }: Props) => { const [ queryClient ] = React.useState(() => new QueryClient({ defaultOptions: { queries: { @@ -63,12 +74,12 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props return ( - + - + { children } - + diff --git a/playwright/fixtures/auth.ts b/playwright/fixtures/auth.ts index 4a2aea71f1..0bda117392 100644 --- a/playwright/fixtures/auth.ts +++ b/playwright/fixtures/auth.ts @@ -1,8 +1,13 @@ -import type { BrowserContext } from '@playwright/test'; +import type { BrowserContext, TestFixture } from '@playwright/test'; +import config from 'configs/app'; import * as cookies from 'lib/cookies'; -import { domain } from 'playwright/utils/app'; -export default function authFixture(context: BrowserContext) { - context.addCookies([ { name: cookies.NAMES.API_TOKEN, value: 'foo', domain, path: '/' } ]); +export function authenticateUser(context: BrowserContext) { + context.addCookies([ { name: cookies.NAMES.API_TOKEN, value: 'foo', domain: config.app.host, path: '/' } ]); } + +export const contextWithAuth: TestFixture = async({ context }, use) => { + authenticateUser(context); + use(context); +}; diff --git a/playwright/fixtures/contextWithEnvs.ts b/playwright/fixtures/contextWithEnvs.ts index e703a922fb..d9c1464462 100644 --- a/playwright/fixtures/contextWithEnvs.ts +++ b/playwright/fixtures/contextWithEnvs.ts @@ -7,7 +7,13 @@ interface Env { value: string; } -// keep in mind that all passed variables here should be present in env config files (.env.pw or .env.poa) +/** + * @deprecated please use mockEnvs fixture + * + * @export + * @param {Array} envs + * @return {*} {Parameters[0]['context']} + */ export default function contextWithEnvsFixture(envs: Array): Parameters[0]['context'] { return async({ browser }, use) => { const context = await createContextWithStorage(browser, envs); diff --git a/playwright/fixtures/contextWithFeatures.ts b/playwright/fixtures/contextWithFeatures.ts index a9c7836937..84f8a55f20 100644 --- a/playwright/fixtures/contextWithFeatures.ts +++ b/playwright/fixtures/contextWithFeatures.ts @@ -7,6 +7,13 @@ interface Feature { value: unknown; } +/** + * @deprecated please use mockFeatures fixture + * + * @export + * @param {Array} envs + * @return {*} {Parameters[0]['context']} + */ export default function contextWithFeaturesFixture(envs: Array): Parameters[0]['context'] { return async({ browser }, use) => { const storageItems = envs.map(({ id, value }) => ({ name: `pw_feature:${ id }`, value: JSON.stringify(value) })); diff --git a/playwright/fixtures/createContextWithStorage.ts b/playwright/fixtures/createContextWithStorage.ts index f2555b37c0..178dbf8a30 100644 --- a/playwright/fixtures/createContextWithStorage.ts +++ b/playwright/fixtures/createContextWithStorage.ts @@ -1,12 +1,20 @@ import type { Browser } from '@playwright/test'; -import * as app from 'playwright/utils/app'; +import config from 'configs/app'; +/** + * @deprecated please use mockEnvs or mockFeatures fixture + * + * @export + * @param {Browser} browser + * @param {Array<{ name: string; value: string }>} localStorage + * @return {*} + */ export default async function createContextWithEnvs(browser: Browser, localStorage: Array<{ name: string; value: string }>) { return browser.newContext({ storageState: { origins: [ - { origin: app.url, localStorage }, + { origin: config.app.baseUrl, localStorage }, ], cookies: [], }, diff --git a/playwright/fixtures/injectMetaMaskProvider.ts b/playwright/fixtures/injectMetaMaskProvider.ts new file mode 100644 index 0000000000..f09ee81a43 --- /dev/null +++ b/playwright/fixtures/injectMetaMaskProvider.ts @@ -0,0 +1,18 @@ +import type { TestFixture, Page } from '@playwright/test'; + +import type { WalletProvider } from 'types/web3'; + +export type InjectMetaMaskProvider = () => Promise; + +const fixture: TestFixture = async({ page }, use) => { + await use(async() => { + await page.evaluate(() => { + window.ethereum = { + isMetaMask: true, + _events: {}, + } as WalletProvider; + }); + }); +}; + +export default fixture; diff --git a/playwright/fixtures/render.tsx b/playwright/fixtures/render.tsx new file mode 100644 index 0000000000..88f7fe669f --- /dev/null +++ b/playwright/fixtures/render.tsx @@ -0,0 +1,35 @@ +import type { MountOptions } from '@playwright/experimental-ct-react'; +import type { Locator, TestFixture } from '@playwright/test'; +import type router from 'next/router'; +import React from 'react'; + +import type { JsonObject } from '@playwright/experimental-ct-core/types/component'; + +import type { Props as TestAppProps } from 'playwright/TestApp'; +import TestApp from 'playwright/TestApp'; + +interface MountResult extends Locator { + unmount(): Promise; + update(component: JSX.Element): Promise; +} + +type Mount = (component: JSX.Element, options?: MountOptions) => Promise; + +interface Options extends JsonObject { + hooksConfig?: { + router: Partial>; + }; +} + +export type RenderFixture = (component: JSX.Element, options?: Options, props?: Omit) => Promise + +const fixture: TestFixture = async({ mount }, use) => { + await use((component, options, props) => { + return mount( + { component }, + options, + ); + }); +}; + +export default fixture; diff --git a/playwright/fixtures/socketServer.ts b/playwright/fixtures/socketServer.ts index 43c3726371..f6d730ce42 100644 --- a/playwright/fixtures/socketServer.ts +++ b/playwright/fixtures/socketServer.ts @@ -10,16 +10,16 @@ import type { Transaction } from 'types/api/transaction'; import * as app from 'playwright/utils/app'; -type ReturnType = () => Promise; +export type CreateSocketFixture = () => Promise; type Channel = [string, string, string]; export interface SocketServerFixture { - createSocket: ReturnType; + createSocket: CreateSocketFixture; } // eslint-disable-next-line @typescript-eslint/no-unused-vars -export const createSocket: TestFixture = async({ page }, use) => { +export const createSocket: TestFixture = async({ page }, use) => { const socketServer = new WebSocketServer({ port: app.socketPort }); const connectionPromise = new Promise((resolve) => { @@ -62,6 +62,7 @@ export function sendMessage(socket: WebSocket, channel: Channel, msg: 'token_bal export function sendMessage(socket: WebSocket, channel: Channel, msg: 'updated_token_balances_erc_20', payload: AddressTokensBalancesSocketMessage): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'updated_token_balances_erc_721', payload: AddressTokensBalancesSocketMessage): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'updated_token_balances_erc_1155', payload: AddressTokensBalancesSocketMessage): void; +export function sendMessage(socket: WebSocket, channel: Channel, msg: 'updated_token_balances_erc_404', payload: AddressTokensBalancesSocketMessage): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transaction', payload: { transaction: number }): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transaction', payload: { transactions: Array }): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'pending_transaction', payload: { pending_transaction: number }): void; @@ -70,6 +71,7 @@ export function sendMessage(socket: WebSocket, channel: Channel, msg: 'new_block export function sendMessage(socket: WebSocket, channel: Channel, msg: 'verification_result', payload: SmartContractVerificationResponse): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'total_supply', payload: { total_supply: number}): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'changed_bytecode', payload: Record): void; +export function sendMessage(socket: WebSocket, channel: Channel, msg: 'fetched_bytecode', payload: { fetched_bytecode: string }): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'smart_contract_was_verified', payload: Record): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'token_transfer', payload: { token_transfers: Array }): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: string, payload: unknown): void { 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/make-envs-script.sh b/playwright/make-envs-script.sh new file mode 100644 index 0000000000..f48f9ec314 --- /dev/null +++ b/playwright/make-envs-script.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +targetFile='./playwright/envs.js' + +declare -a envFiles=('./configs/envs/.env.pw') + +touch $targetFile; +truncate -s 0 $targetFile; + +echo "Creating script file with envs" + +echo "window.process = { env: { } };" >> $targetFile; + +for envFile in "${envFiles[@]}" +do + # read each env file + while read line; do + # if it is a comment or an empty line, continue to next one + if [ "${line:0:1}" == "#" ] || [ "${line}" == "" ]; then + continue + fi + + # split by "=" sign to get variable name and value + configName="$(cut -d'=' -f1 <<<"$line")"; + configValue="$(cut -d'=' -f2- <<<"$line")"; + + # if there is a value, escape it and add line to target file + escapedConfigValue=$(echo $configValue | sed s/\'/\"/g); + echo "window.process.env.${configName} = localStorage.getItem('${configName}') ?? '${escapedConfigValue}';" >> $targetFile; + done < $envFile +done + +echo "Done" \ No newline at end of file diff --git a/playwright/mocks/modules/@web3modal/wagmi/react.js b/playwright/mocks/modules/@web3modal/wagmi/react.js new file mode 100644 index 0000000000..7765272c71 --- /dev/null +++ b/playwright/mocks/modules/@web3modal/wagmi/react.js @@ -0,0 +1,26 @@ +function useWeb3Modal() { + return { + open: () => {}, + }; +} + +function useWeb3ModalState() { + return { + isOpen: false, + }; +} + +function useWeb3ModalTheme() { + return { + setThemeMode: () => {}, + }; +} + +function createWeb3Modal() {} + +export { + createWeb3Modal, + useWeb3Modal, + useWeb3ModalState, + useWeb3ModalTheme, +}; diff --git a/playwright/run-tests.sh b/playwright/run-tests.sh new file mode 100644 index 0000000000..57ef908da6 --- /dev/null +++ b/playwright/run-tests.sh @@ -0,0 +1,5 @@ +#!/bin/sh +yarn install --modules-folder node_modules_linux +export NODE_PATH=$(pwd)/node_modules_linux +rm -rf ./playwright/.cache +yarn test:pw "$@" \ No newline at end of file diff --git a/playwright/utils/app.ts b/playwright/utils/app.ts index 475376830e..466757ee9b 100644 --- a/playwright/utils/app.ts +++ b/playwright/utils/app.ts @@ -1,5 +1 @@ -export const url = `${ process.env.NEXT_PUBLIC_APP_PROTOCOL }://${ process.env.NEXT_PUBLIC_APP_HOST }:${ process.env.NEXT_PUBLIC_APP_PORT }`; - -export const domain = process.env.NEXT_PUBLIC_APP_HOST; - export const socketPort = 3200; diff --git a/playwright/utils/buildApiUrl.ts b/playwright/utils/buildApiUrl.ts index 4279efb71d..346828c618 100644 --- a/playwright/utils/buildApiUrl.ts +++ b/playwright/utils/buildApiUrl.ts @@ -1,11 +1,20 @@ import { compile } from 'path-to-regexp'; +import config from 'configs/app'; import type { ResourceName, ResourcePathParams } from 'lib/api/resources'; import { RESOURCES } from 'lib/api/resources'; +/** + * @deprecated please use fixture mockApiResponse from playwright/lib.tsx for rendering test suite + * + * @export + * @template R + * @param {R} resourceName + * @param {ResourcePathParams} [pathParams] + * @return {*} string + */ export default function buildApiUrl(resourceName: R, pathParams?: ResourcePathParams) { const resource = RESOURCES[resourceName]; - const defaultApi = 'https://' + process.env.NEXT_PUBLIC_API_HOST + ':' + process.env.NEXT_PUBLIC_API_PORT; - const origin = 'endpoint' in resource && resource.endpoint ? resource.endpoint + (resource.basePath ?? '') : defaultApi; + const origin = 'endpoint' in resource && resource.endpoint ? resource.endpoint + (resource.basePath ?? '') : config.api.endpoint; return origin + compile(resource.path)(pathParams); } diff --git a/playwright/utils/configs.ts b/playwright/utils/configs.ts index c167a76fed..ec83566ac7 100644 --- a/playwright/utils/configs.ts +++ b/playwright/utils/configs.ts @@ -10,61 +10,3 @@ export const viewport = { export const maskColor = '#4299E1'; // blue.400 export const adsBannerSelector = '.adsbyslise'; - -export const featureEnvs = { - beaconChain: [ - { name: 'NEXT_PUBLIC_HAS_BEACON_CHAIN', value: 'true' }, - ], - optimisticRollup: [ - { name: 'NEXT_PUBLIC_ROLLUP_TYPE', value: 'optimistic' }, - { 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', - value: '[{"id":"1","title":"Ethereum","short_title":"ETH","base_url":"https://eth.blockscout.com/token/"},{"id":"56","title":"Binance Smart Chain","short_title":"BSC","base_url":"https://bscscan.com/token/"},{"id":"99","title":"POA","short_title":"POA","base_url":"https://blockscout.com/poa/core/token/"}]', - }, - { - name: 'NEXT_PUBLIC_BRIDGED_TOKENS_BRIDGES', - value: '[{"type":"omni","title":"OmniBridge","short_title":"OMNI"},{"type":"amb","title":"Arbitrary Message Bridge","short_title":"AMB"}]', - }, - ], - txInterpretation: [ - { name: 'NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER', value: 'blockscout' }, - ], - zkEvmRollup: [ - { name: 'NEXT_PUBLIC_ROLLUP_TYPE', value: 'zkEvm' }, - { name: 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL', value: 'https://localhost:3101' }, - ], - userOps: [ - { name: 'NEXT_PUBLIC_HAS_USER_OPS', value: 'true' }, - ], - validators: [ - { name: 'NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE', value: 'stability' }, - ], -}; - -export const viewsEnvs = { - block: { - hiddenFields: [ - { name: 'NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS', value: '["burnt_fees", "total_reward", "nonce"]' }, - ], - }, -}; - -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"]' }, - { name: 'NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS', value: '["fee_per_gas"]' }, -]; diff --git a/prometheus.yml b/prometheus.yml new file mode 100644 index 0000000000..89b95e0ecb --- /dev/null +++ b/prometheus.yml @@ -0,0 +1,16 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +rule_files: + # - "first.rules" + # - "second.rules" + +scrape_configs: + - job_name: prometheus + static_configs: + - targets: ['localhost:9090'] + - job_name: frontend + metrics_path: /node-api/metrics + static_configs: + - targets: ['host.docker.internal:3000'] \ No newline at end of file diff --git a/public/icons/name.d.ts b/public/icons/name.d.ts index 39f3b95a57..82909a5a3b 100644 --- a/public/icons/name.d.ts +++ b/public/icons/name.d.ts @@ -4,6 +4,8 @@ | "ABI_slim" | "ABI" | "API" + | "apps_list" + | "apps_xs" | "apps" | "arrows/down-right" | "arrows/east-mini" @@ -11,9 +13,16 @@ | "arrows/north-east" | "arrows/south-east" | "arrows/up-down" + | "beta_xs" + | "beta" + | "blob" + | "blobs/image" + | "blobs/raw" + | "blobs/text" | "block_slim" | "block" | "brands/safe" + | "brands/solidity_scan" | "burger" | "check" | "clock-light" @@ -22,6 +31,8 @@ | "collection" | "contract_verified" | "contract" + | "contracts_verified" + | "contracts" | "copy" | "cross" | "delete" @@ -51,6 +62,7 @@ | "flame" | "gas_xl" | "gas" + | "gear_slim" | "gear" | "globe-b" | "globe" @@ -107,8 +119,8 @@ | "social/stats" | "social/telega" | "social/telegram_filled" - | "social/tweet" | "social/twitter_filled" + | "social/twitter" | "star_filled" | "star_outline" | "stats" @@ -130,6 +142,7 @@ | "txn_batches" | "unfinalized" | "uniswap" + | "up" | "user_op_slim" | "user_op" | "validator" 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/public/static/identicon_logos/blockies.png b/public/static/identicon_logos/blockies.png new file mode 100644 index 0000000000..d9ccead353 Binary files /dev/null and b/public/static/identicon_logos/blockies.png differ diff --git a/public/static/identicon_logos/github.png b/public/static/identicon_logos/github.png new file mode 100644 index 0000000000..346f9d212e Binary files /dev/null and b/public/static/identicon_logos/github.png differ diff --git a/public/static/identicon_logos/gradient_avatar.png b/public/static/identicon_logos/gradient_avatar.png new file mode 100644 index 0000000000..396ee27848 Binary files /dev/null and b/public/static/identicon_logos/gradient_avatar.png differ diff --git a/public/static/identicon_logos/jazzicon.png b/public/static/identicon_logos/jazzicon.png new file mode 100644 index 0000000000..c06bc880ca Binary files /dev/null and b/public/static/identicon_logos/jazzicon.png differ diff --git a/stubs/RPC.ts b/stubs/RPC.ts index 333f33e605..0da0947c7c 100644 --- a/stubs/RPC.ts +++ b/stubs/RPC.ts @@ -84,6 +84,8 @@ export const GET_BLOCK: GetBlockReturnType = { withdrawals: Array(10).fill(WITHDRAWAL), withdrawalsRoot: TX_HASH, sealFields: [ '0x00' ], + blobGasUsed: BigInt(0), + excessBlobGas: BigInt(0), }; export const GET_BLOCK_WITH_TRANSACTIONS: GetBlockReturnType = { diff --git a/stubs/account.ts b/stubs/account.ts index cdb1bab556..92bd0861ce 100644 --- a/stubs/account.ts +++ b/stubs/account.ts @@ -48,6 +48,10 @@ export const WATCH_LIST_ITEM_WITH_TOKEN_INFO: WatchlistAddress = { incoming: true, outcoming: true, }, + 'ERC-404': { + incoming: true, + outcoming: true, + }, 'native': { incoming: true, outcoming: true, diff --git a/stubs/address.ts b/stubs/address.ts index 19f2a398ad..1c5ec210a9 100644 --- a/stubs/address.ts +++ b/stubs/address.ts @@ -10,7 +10,7 @@ import type { import type { AddressesItem } from 'types/api/addresses'; import { ADDRESS_HASH } from './addressParams'; -import { TOKEN_INFO_ERC_1155, TOKEN_INFO_ERC_20, TOKEN_INFO_ERC_721, TOKEN_INSTANCE } from './token'; +import { TOKEN_INFO_ERC_1155, TOKEN_INFO_ERC_20, TOKEN_INFO_ERC_721, TOKEN_INFO_ERC_404, TOKEN_INSTANCE } from './token'; import { TX_HASH } from './tx'; export const ADDRESS_INFO: Address = { @@ -19,22 +19,16 @@ export const ADDRESS_INFO: Address = { creation_tx_hash: null, creator_address_hash: ADDRESS_HASH, exchange_rate: null, - has_custom_methods_read: false, - has_custom_methods_write: false, has_decompiled_code: false, has_logs: true, - has_methods_read: false, - has_methods_read_proxy: false, - has_methods_write: false, - has_methods_write_proxy: false, has_token_transfers: false, has_tokens: false, has_validated_blocks: false, hash: ADDRESS_HASH, implementation_address: null, implementation_name: null, - is_contract: false, - is_verified: false, + is_contract: true, + is_verified: true, name: 'ChainLink Token (goerli)', token: TOKEN_INFO_ERC_20, private_tags: [], @@ -104,6 +98,13 @@ export const ADDRESS_NFT_1155: AddressNFT = { ...TOKEN_INSTANCE, }; +export const ADDRESS_NFT_404: AddressNFT = { + token_type: 'ERC-404', + token: TOKEN_INFO_ERC_404, + value: '10', + ...TOKEN_INSTANCE, +}; + export const ADDRESS_COLLECTION: AddressCollection = { token: TOKEN_INFO_ERC_1155, amount: '4', 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 e9d6afc352..5ed6bee714 100644 --- a/stubs/contract.ts +++ b/stubs/contract.ts @@ -7,6 +7,12 @@ export const CONTRACT_CODE_UNVERIFIED = { creation_bytecode: '0x60806040526e', deployed_bytecode: '0x608060405233', is_self_destructed: false, + has_methods_read: true, + has_methods_read_proxy: true, + has_methods_write: true, + has_methods_write_proxy: true, + has_custom_methods_read: true, + has_custom_methods_write: true, } as SmartContract; export const CONTRACT_CODE_VERIFIED = { @@ -40,6 +46,13 @@ export const CONTRACT_CODE_VERIFIED = { optimization_runs: 200, source_code: 'source_code', verified_at: '2023-02-21T14:39:16.906760Z', + license_type: 'mit', + has_methods_read: true, + has_methods_read_proxy: true, + has_methods_write: true, + has_methods_write_proxy: true, + has_custom_methods_read: true, + has_custom_methods_write: true, } as unknown as SmartContract; export const VERIFIED_CONTRACT_INFO: VerifiedContract = { @@ -52,6 +65,7 @@ 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 = { @@ -63,6 +77,7 @@ export const VERIFIED_CONTRACTS_COUNTERS: VerifiedContractsCounters = { export const SOLIDITYSCAN_REPORT: SolidityscanReport = { scan_report: { + contractname: 'BullRunners', scan_status: 'scan_done', scan_summary: { issue_severity_distribution: { diff --git a/stubs/noves/NovesTranslate.ts b/stubs/noves/NovesTranslate.ts new file mode 100644 index 0000000000..848ed6dab9 --- /dev/null +++ b/stubs/noves/NovesTranslate.ts @@ -0,0 +1,43 @@ +import type { NovesResponseData, NovesClassificationData, NovesRawTransactionData } from 'types/api/noves'; + +const NOVES_TRANSLATE_CLASSIFIED: NovesClassificationData = { + description: 'Sent 0.04 ETH', + received: [ { + action: 'Sent Token', + actionFormatted: 'Sent Token', + amount: '45', + from: { name: '', address: '0xa0393A76b132526a70450273CafeceB45eea6dEE' }, + to: { name: '', address: '0xa0393A76b132526a70450273CafeceB45eea6dEE' }, + token: { + address: '', + name: 'ETH', + symbol: 'ETH', + decimals: 18, + }, + } ], + sent: [], + source: { + type: '', + }, + type: '0x2', + typeFormatted: 'Send NFT', +}; + +const NOVES_TRANSLATE_RAW: NovesRawTransactionData = { + blockNumber: 1, + fromAddress: '0xCFC123a23dfeD71bDAE054e487989d863C525C73', + gas: 2, + gasPrice: 3, + timestamp: 20000, + toAddress: '0xCFC123a23dfeD71bDAE054e487989d863C525C73', + transactionFee: 2, + transactionHash: '0x128b79937a0eDE33258992c9668455f997f1aF24', +}; + +export const NOVES_TRANSLATE: NovesResponseData = { + accountAddress: '0x2b824349b320cfa72f292ab26bf525adb00083ba9fa097141896c3c8c74567cc', + chain: 'base', + txTypeVersion: 2, + rawTransactionData: NOVES_TRANSLATE_RAW, + classificationData: NOVES_TRANSLATE_CLASSIFIED, +}; diff --git a/stubs/token.ts b/stubs/token.ts index e60e004bad..1c90f3d16a 100644 --- a/stubs/token.ts +++ b/stubs/token.ts @@ -1,4 +1,13 @@ -import type { TokenCounters, TokenHolder, TokenInfo, TokenInstance, TokenType } from 'types/api/token'; +import type { + TokenCounters, + TokenHolder, + TokenHolders, + TokenHoldersPagination, + TokenInfo, + TokenInstance, + TokenType, +} from 'types/api/token'; +import type { TokenInstanceTransferPagination, TokenInstanceTransferResponse } from 'types/api/tokens'; import type { TokenTransfer, TokenTransferPagination, TokenTransferResponse } from 'types/api/tokenTransfer'; import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams'; @@ -31,6 +40,12 @@ export const TOKEN_INFO_ERC_1155: TokenInfo<'ERC-1155'> = { type: 'ERC-1155', }; +export const TOKEN_INFO_ERC_404: TokenInfo<'ERC-404'> = { + ...TOKEN_INFO_ERC_20, + circulating_market_cap: null, + type: 'ERC-404', +}; + export const TOKEN_COUNTERS: TokenCounters = { token_holders_count: '123456', transfers_count: '123456', @@ -38,17 +53,41 @@ 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', }; +export const getTokenHoldersStub = (type?: TokenType, pagination: TokenHoldersPagination | null = null): TokenHolders => { + switch (type) { + case 'ERC-721': + return generateListStub<'token_holders'>(TOKEN_HOLDER_ERC_20, 50, { next_page_params: pagination }); + case 'ERC-1155': + return generateListStub<'token_holders'>(TOKEN_HOLDER_ERC_1155, 50, { next_page_params: pagination }); + case 'ERC-404': + return generateListStub<'token_holders'>(TOKEN_HOLDER_ERC_1155, 50, { next_page_params: pagination }); + default: + return generateListStub<'token_holders'>(TOKEN_HOLDER_ERC_20, 50, { next_page_params: pagination }); + } +}; + +export const getTokenInstanceHoldersStub = (type?: TokenType, pagination: TokenHoldersPagination | null = null): TokenHolders => { + switch (type) { + case 'ERC-721': + return generateListStub<'token_instance_holders'>(TOKEN_HOLDER_ERC_20, 10, { next_page_params: pagination }); + case 'ERC-1155': + return generateListStub<'token_instance_holders'>(TOKEN_HOLDER_ERC_1155, 10, { next_page_params: pagination }); + case 'ERC-404': + return generateListStub<'token_instance_holders'>(TOKEN_HOLDER_ERC_1155, 10, { next_page_params: pagination }); + default: + return generateListStub<'token_instance_holders'>(TOKEN_HOLDER_ERC_20, 10, { next_page_params: pagination }); + } +}; + export const TOKEN_TRANSFER_ERC_20: TokenTransfer = { block_hash: BLOCK_HASH, from: ADDRESS_PARAMS, @@ -83,17 +122,42 @@ export const TOKEN_TRANSFER_ERC_1155: TokenTransfer = { token: TOKEN_INFO_ERC_1155, }; +export const TOKEN_TRANSFER_ERC_404: TokenTransfer = { + ...TOKEN_TRANSFER_ERC_20, + total: { + token_id: '35870', + value: '123', + decimals: '18', + }, + token: TOKEN_INFO_ERC_404, +}; + export const getTokenTransfersStub = (type?: TokenType, pagination: TokenTransferPagination | null = null): TokenTransferResponse => { switch (type) { case 'ERC-721': return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_721, 50, { next_page_params: pagination }); case 'ERC-1155': return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_1155, 50, { next_page_params: pagination }); + case 'ERC-404': + return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_404, 50, { next_page_params: pagination }); default: return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_20, 50, { next_page_params: pagination }); } }; +export const getTokenInstanceTransfersStub = (type?: TokenType, pagination: TokenInstanceTransferPagination | null = null): TokenInstanceTransferResponse => { + switch (type) { + case 'ERC-721': + return generateListStub<'token_instance_transfers'>(TOKEN_TRANSFER_ERC_721, 10, { next_page_params: pagination }); + case 'ERC-1155': + return generateListStub<'token_instance_transfers'>(TOKEN_TRANSFER_ERC_1155, 10, { next_page_params: pagination }); + case 'ERC-404': + return generateListStub<'token_instance_transfers'>(TOKEN_TRANSFER_ERC_404, 10, { next_page_params: pagination }); + default: + return generateListStub<'token_instance_transfers'>(TOKEN_TRANSFER_ERC_20, 10, { next_page_params: pagination }); + } +}; + export const TOKEN_INSTANCE: TokenInstance = { animation_url: null, external_app_url: 'https://vipsland.com/nft/collections/genesis/188882', diff --git a/stubs/tx.ts b/stubs/tx.ts index 1f7eb1c0a6..2e80408689 100644 --- a/stubs/tx.ts +++ b/stubs/tx.ts @@ -1,5 +1,5 @@ import type { RawTracesResponse } from 'types/api/rawTrace'; -import type { Transaction } from 'types/api/transaction'; +import type { Transaction, TransactionsStats } from 'types/api/transaction'; import { ADDRESS_PARAMS } from './addressParams'; @@ -59,3 +59,10 @@ export const TX_ZKEVM_L2: Transaction = { }; export const TX_RAW_TRACE: RawTracesResponse = []; + +export const TXS_STATS: TransactionsStats = { + pending_transactions_count: '4200', + transaction_fees_avg_24h: '22342870314428', + transaction_fees_sum_24h: '22184012506492688277', + transactions_count_24h: '992890', +}; diff --git a/stubs/zkEvmL2.ts b/stubs/zkEvmL2.ts index 7dea0f57b8..46015e0ccf 100644 --- a/stubs/zkEvmL2.ts +++ b/stubs/zkEvmL2.ts @@ -1,7 +1,27 @@ -import type { ZkEvmL2TxnBatch, ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2'; +import type { ZkEvmL2DepositsItem, ZkEvmL2TxnBatch, ZkEvmL2TxnBatchesItem, ZkEvmL2WithdrawalsItem } from 'types/api/zkEvmL2'; import { TX_HASH } from './tx'; +export const ZKEVM_DEPOSITS_ITEM: ZkEvmL2DepositsItem = { + block_number: 19674901, + index: 181920, + l1_transaction_hash: '0xa74edfa5824a07a5f95ca1145140ed589df7f05bb17796bf18090b14c4566b5d', + l2_transaction_hash: '0x436d1c7ada270466ca0facdb96ecc22934d68d13b8a08f541b8df11b222967b5', + symbol: 'ETH', + timestamp: '2023-06-01T14:46:48.000000Z', + value: '0.13040262', +}; + +export const ZKEVM_WITHDRAWALS_ITEM: ZkEvmL2WithdrawalsItem = { + block_number: 11692968, + index: 47003, + l1_transaction_hash: '0x230cf46dabea287ac7d0ba83b8ea120bb83c1de58a81d34f44788f0459096c52', + l2_transaction_hash: '0x519d9f025ec47f08a48d708964d177189d2246ddf988686c481f5debcf097e34', + symbol: 'ETH', + timestamp: '2024-04-17T08:51:58.000000Z', + value: '110.35', +}; + export const ZKEVM_L2_TXN_BATCHES_ITEM: ZkEvmL2TxnBatchesItem = { timestamp: '2023-06-01T14:46:48.000000Z', status: 'Finalized', diff --git a/stubs/zkSyncL2.ts b/stubs/zkSyncL2.ts new file mode 100644 index 0000000000..9d3782abcf --- /dev/null +++ b/stubs/zkSyncL2.ts @@ -0,0 +1,27 @@ +import type { ZkSyncBatch, ZkSyncBatchesItem } from 'types/api/zkSyncL2'; + +import { TX_HASH } from './tx'; + +export const ZKSYNC_L2_TXN_BATCHES_ITEM: ZkSyncBatchesItem = { + commit_transaction_hash: TX_HASH, + commit_transaction_timestamp: '2022-03-17T19:33:04.519145Z', + execute_transaction_hash: TX_HASH, + execute_transaction_timestamp: '2022-03-17T20:49:48.856345Z', + number: 8002, + prove_transaction_hash: TX_HASH, + prove_transaction_timestamp: '2022-03-17T20:49:48.772442Z', + status: 'Executed on L1', + timestamp: '2022-03-17T17:00:11.000000Z', + tx_count: 1215, +}; + +export const ZKSYNC_L2_TXN_BATCH: ZkSyncBatch = { + ...ZKSYNC_L2_TXN_BATCHES_ITEM, + start_block: 1245209, + end_block: 1245490, + l1_gas_price: '4173068062', + l1_tx_count: 0, + l2_fair_gas_price: '100000000', + l2_tx_count: 287, + root_hash: '0x108c635b94f941fcabcb85500daec2f6be4f0747dff649b1cdd9dd7a7a264792', +}; diff --git a/theme/components/Badge.ts b/theme/components/Badge.ts index 81f650a014..b673b528f6 100644 --- a/theme/components/Badge.ts +++ b/theme/components/Badge.ts @@ -1,5 +1,5 @@ import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system'; -import { mode, transparentize } from '@chakra-ui/theme-tools'; +import { mode } from '@chakra-ui/theme-tools'; const baseStyle = defineStyle({ fontSize: 'xs', @@ -8,19 +8,25 @@ const baseStyle = defineStyle({ }); const variantSubtle = defineStyle((props) => { - const { colorScheme: c, theme } = props; - const darkBg = transparentize(`${ c }.200`, 0.16)(theme); + const { colorScheme: c } = props; if (c === 'gray') { return { - bg: mode('blackAlpha.100', 'whiteAlpha.400')(props), - color: mode('gray.600', 'gray.50')(props), + bg: mode('blackAlpha.50', 'whiteAlpha.100')(props), + color: mode('blackAlpha.800', 'whiteAlpha.800')(props), + }; + } + + if (c === 'gray-blue') { + return { + bg: mode('gray.100', 'gray.800')(props), + color: mode('blackAlpha.800', 'whiteAlpha.800')(props), }; } return { - bg: mode(`${ c }.50`, darkBg)(props), - color: mode(`${ c }.500`, `${ c }.200`)(props), + bg: mode(`${ c }.50`, `${ c }.800`)(props), + color: mode(`${ c }.500`, `${ c }.100`)(props), }; }); diff --git a/theme/components/Menu.ts b/theme/components/Menu.ts index fb314ddcfd..5e425ff738 100644 --- a/theme/components/Menu.ts +++ b/theme/components/Menu.ts @@ -25,15 +25,15 @@ const baseStyleList = defineStyle({ const baseStyleItem = defineStyle({ _focus: { - [$bg.variable]: 'colors.blue.50', + [$bg.variable]: 'transparent', _dark: { - [$bg.variable]: 'colors.gray.800', + [$bg.variable]: 'transparent', }, }, _hover: { [$bg.variable]: 'colors.blue.50', _dark: { - [$bg.variable]: 'colors.gray.800', + [$bg.variable]: 'colors.whiteAlpha.100', }, }, bg: $bg.reference, diff --git a/theme/components/Tag/Tag.pw.tsx b/theme/components/Tag/Tag.pw.tsx index 6141db546d..2f02a6cefd 100644 --- a/theme/components/Tag/Tag.pw.tsx +++ b/theme/components/Tag/Tag.pw.tsx @@ -4,7 +4,7 @@ import React from 'react'; import TestApp from 'playwright/TestApp'; -[ 'blue', 'gray', 'orange', 'green', 'purple', 'cyan', 'teal' ].forEach((colorScheme) => { +[ 'blue', 'gray', 'gray-blue', 'orange', 'green', 'purple', 'cyan', 'teal' ].forEach((colorScheme) => { test(`${ colorScheme } color scheme +@dark-mode`, async({ mount }) => { const component = await mount( diff --git a/theme/components/Tag/Tag.ts b/theme/components/Tag/Tag.ts index 5cd5b2bd97..047ce7d698 100644 --- a/theme/components/Tag/Tag.ts +++ b/theme/components/Tag/Tag.ts @@ -18,12 +18,12 @@ const variants = { }; const sizes = { - md: definePartsStyle({ + sm: definePartsStyle({ container: { minH: 6, minW: 6, fontSize: 'sm', - px: 2, + px: 1, py: '2px', lineHeight: 5, }, @@ -48,7 +48,7 @@ const Tag = defineMultiStyleConfig({ variants, sizes, defaultProps: { - size: 'md', + size: 'sm', variant: 'subtle', colorScheme: 'gray', }, diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_blue-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_blue-color-scheme-dark-mode-1.png index e2964560a2..5fc7bad237 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_blue-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_blue-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_cyan-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_cyan-color-scheme-dark-mode-1.png index 8a1efa746b..daf45113b1 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_cyan-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_cyan-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_gray-blue-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_gray-blue-color-scheme-dark-mode-1.png new file mode 100644 index 0000000000..05fe7cfea4 Binary files /dev/null and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_gray-blue-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_gray-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_gray-color-scheme-dark-mode-1.png index a956655fb5..6afe64a5a7 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_gray-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_gray-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_green-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_green-color-scheme-dark-mode-1.png index 18f0765090..a14814713f 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_green-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_green-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_orange-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_orange-color-scheme-dark-mode-1.png index 51d848855b..fd9bb6cfc6 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_orange-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_orange-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_purple-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_purple-color-scheme-dark-mode-1.png index 9fd2e54028..a9b36a72cd 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_purple-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_purple-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_teal-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_teal-color-scheme-dark-mode-1.png index e4b5437ff4..bbaed04293 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_teal-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_teal-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_blue-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_blue-color-scheme-dark-mode-1.png index 475e4b3500..ce7946c035 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_blue-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_blue-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_cyan-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_cyan-color-scheme-dark-mode-1.png index e2df67339c..1cac3c3c0c 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_cyan-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_cyan-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_gray-blue-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_gray-blue-color-scheme-dark-mode-1.png new file mode 100644 index 0000000000..1d150ca7d3 Binary files /dev/null and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_gray-blue-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_gray-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_gray-color-scheme-dark-mode-1.png index 533fb98865..f37bae331a 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_gray-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_gray-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_green-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_green-color-scheme-dark-mode-1.png index 89b0f21d9b..24fb120a47 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_green-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_green-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_orange-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_orange-color-scheme-dark-mode-1.png index 78612cf01f..7a3f917391 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_orange-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_orange-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_purple-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_purple-color-scheme-dark-mode-1.png index bcad901b56..f9b325adf1 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_purple-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_purple-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_teal-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_teal-color-scheme-dark-mode-1.png index 6d1b5b96e0..cccd73491f 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_teal-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_teal-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_with-long-text-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_with-long-text-1.png index 3b47811eaa..ee9d7d6dbd 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_with-long-text-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_with-long-text-1.png differ diff --git a/theme/foundations/colors.ts b/theme/foundations/colors.ts index 8ad22f3fbd..f8b2ca521f 100644 --- a/theme/foundations/colors.ts +++ b/theme/foundations/colors.ts @@ -55,7 +55,7 @@ const colors = { linkedin: '#1564BA', discord: '#9747FF', slack: '#1BA27A', - twitter: '#63B3ED', + twitter: '#000000', opensea: '#2081E2', facebook: '#4460A0', medium: '#231F20', diff --git a/theme/global.ts b/theme/global.ts index 5ddff6bcf7..e50b7eeaf6 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/tools/scripts/dev.preset.sh b/tools/scripts/dev.preset.sh index c8566819e0..c3384ca41f 100755 --- a/tools/scripts/dev.preset.sh +++ b/tools/scripts/dev.preset.sh @@ -28,5 +28,5 @@ dotenv \ -v NEXT_PUBLIC_GIT_TAG=$(git describe --tags --abbrev=0) \ -e $config_file \ -e $secrets_file \ - -- bash -c './deploy/scripts/make_envs_script.sh && next dev -- -p $NEXT_PUBLIC_APP_PORT' | + -- bash -c './deploy/scripts/make_envs_script.sh && next dev -p $NEXT_PUBLIC_APP_PORT --turbo' | pino-pretty \ No newline at end of file diff --git a/tools/scripts/pw.docker.deps.sh b/tools/scripts/pw.docker.deps.sh new file mode 100755 index 0000000000..719930361f --- /dev/null +++ b/tools/scripts/pw.docker.deps.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +yarn install --modules-folder node_modules_linux diff --git a/tools/scripts/pw.docker.sh b/tools/scripts/pw.docker.sh index 30179e8c4c..49c763f9ad 100755 --- a/tools/scripts/pw.docker.sh +++ b/tools/scripts/pw.docker.sh @@ -1,7 +1,5 @@ #!/bin/bash -yarn install --modules-folder node_modules_linux - export NODE_PATH=$(pwd)/node_modules_linux yarn test:pw "$@" diff --git a/types/api/account.ts b/types/api/account.ts index 101b746ed0..4279884406 100644 --- a/types/api/account.ts +++ b/types/api/account.ts @@ -36,6 +36,7 @@ export interface NotificationSettings { 'native': NotificationDirection; 'ERC-20': NotificationDirection; 'ERC-721': NotificationDirection; + 'ERC-404': NotificationDirection; } export interface NotificationMethods { @@ -69,7 +70,7 @@ export type Transactions = Array export interface UserInfo { name?: string; nickname?: string; - email: string; + email: string | null; avatar?: string; } diff --git a/types/api/address.ts b/types/api/address.ts index 37955b7f28..9098b916f5 100644 --- a/types/api/address.ts +++ b/types/api/address.ts @@ -15,14 +15,8 @@ export interface Address extends UserTags { ens_domain_name: string | null; // TODO: if we are happy with tabs-counters method, should we delete has_something fields? has_beacon_chain_withdrawals?: boolean; - has_custom_methods_read: boolean; - has_custom_methods_write: boolean; has_decompiled_code: boolean; has_logs: boolean; - has_methods_read: boolean; - has_methods_read_proxy: boolean; - has_methods_write: boolean; - has_methods_write_proxy: boolean; has_token_transfers: boolean; has_tokens: boolean; has_validated_blocks: boolean; @@ -148,11 +142,20 @@ export interface AddressCoinBalanceHistoryResponse { } | null; } -export type AddressCoinBalanceHistoryChart = Array<{ +// remove after api release +export type AddressCoinBalanceHistoryChartOld = Array<{ date: string; value: string; }> +export type AddressCoinBalanceHistoryChart = { + items: Array<{ + date: string; + value: string; + }>; + days: number; +}; + export interface AddressBlocksValidatedResponse { items: Array; next_page_params: { diff --git a/types/api/addressMetadata.ts b/types/api/addressMetadata.ts new file mode 100644 index 0000000000..18579bdead --- /dev/null +++ b/types/api/addressMetadata.ts @@ -0,0 +1,30 @@ +export interface AddressMetadataInfo { + addresses: Record; + reputation: number | null; + }>; +} + +export type AddressMetadataTagType = 'name' | 'generic' | 'classifier' | 'information' | 'note' | 'protocol'; + +// Response model from Metadata microservice API +export interface AddressMetadataTag { + slug: string; + name: string; + tagType: AddressMetadataTagType; + ordinal: number; + meta: string | null; +} + +// Response model from Blockscout API with parsed meta field +export interface AddressMetadataTagApi extends Omit { + meta: { + textColor?: string; + bgColor?: string; + tagUrl?: string; + tooltipIcon?: string; + tooltipTitle?: string; + tooltipDescription?: string; + tooltipUrl?: string; + } | null; +} diff --git a/types/api/addressParams.ts b/types/api/addressParams.ts index fe8cb3d90c..8432062c4f 100644 --- a/types/api/addressParams.ts +++ b/types/api/addressParams.ts @@ -1,3 +1,5 @@ +import type { AddressMetadataTagApi } from './addressMetadata'; + export interface AddressTag { label: string; display_name: string; @@ -22,6 +24,10 @@ export type AddressParamBasic = { is_contract: boolean; is_verified: boolean | null; ens_domain_name: string | null; + metadata?: { + reputation: number | null; + tags: Array; + } | null; } export type AddressParam = UserTags & AddressParamBasic; 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 1f2a508b08..2fc14d2745 100644 --- a/types/api/block.ts +++ b/types/api/block.ts @@ -2,6 +2,8 @@ import type { AddressParam } from 'types/api/addressParams'; import type { Reward } from 'types/api/reward'; import type { Transaction } from 'types/api/transaction'; +import type { ZkSyncBatchesItem } from './zkSyncL2'; + export type BlockType = 'block' | 'reorg' | 'uncle'; export interface Block { @@ -36,6 +38,16 @@ 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; + // ZKSYNC FIELDS + zksync?: Omit & { + 'batch_number': number | null; + }; } export interface BlocksResponse { diff --git a/types/api/charts.ts b/types/api/charts.ts index 5414f68e60..a5504ded12 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; } @@ -18,3 +18,8 @@ export interface ChartMarketResponse { available_supply: string; chart_data: Array; } + +export interface ChartSecondaryCoinPriceResponse { + available_supply: string; + chart_data: Array; +} diff --git a/types/api/contract.ts b/types/api/contract.ts index e5e01463e6..50f4098d27 100644 --- a/types/api/contract.ts +++ b/types/api/contract.ts @@ -1,8 +1,24 @@ -import type { Abi, AbiType } from 'abitype'; +import type { Abi, AbiType, AbiFallback, AbiFunction, AbiReceive } from 'abitype'; export type SmartContractMethodArgType = AbiType; export type SmartContractMethodStateMutability = 'view' | 'nonpayable' | 'payable'; +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; @@ -17,6 +33,14 @@ export interface SmartContract { is_verified: boolean | null; is_verified_via_eth_bytecode_db: boolean | null; is_changed_bytecode: boolean | null; + + has_methods_read: boolean; + has_methods_read_proxy: boolean; + has_methods_write: boolean; + has_methods_write_proxy: boolean; + has_custom_methods_read: boolean; + has_custom_methods_write: boolean; + // sourcify info >>> is_verified_via_sourcify: boolean | null; is_fully_verified: boolean | null; @@ -37,6 +61,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 = [ @@ -53,49 +78,19 @@ export interface SmartContractExternalLibrary { name: string; } -export interface SmartContractMethodBase { - inputs: Array; - outputs?: Array; - constant: boolean; - name: string; - stateMutability: SmartContractMethodStateMutability; - type: 'function'; - payable: boolean; - error?: string; +export type SmartContractMethodOutputValue = string | boolean | object; +export type SmartContractMethodOutput = AbiFunction['outputs'][number] & { value?: SmartContractMethodOutputValue }; +export type SmartContractMethodBase = Omit & { method_id: string; -} - + outputs: Array; + constant?: boolean; + error?: string; +}; export type SmartContractReadMethod = SmartContractMethodBase; - -export interface SmartContractWriteFallback { - payable?: true; - stateMutability: 'payable'; - type: 'fallback'; -} - -export interface SmartContractWriteReceive { - payable?: true; - stateMutability: 'payable'; - type: 'receive'; -} - -export type SmartContractWriteMethod = SmartContractMethodBase | SmartContractWriteFallback | SmartContractWriteReceive; - +export type SmartContractWriteMethod = SmartContractMethodBase | AbiFallback | AbiReceive; export type SmartContractMethod = SmartContractReadMethod | SmartContractWriteMethod; -export interface SmartContractMethodInput { - internalType?: string; // there could be any string, e.g "enum MyEnum" - name: string; - type: SmartContractMethodArgType; - components?: Array; - fieldType?: 'native_coin'; -} - -export interface SmartContractMethodOutput extends SmartContractMethodInput { - value?: string | boolean | object; -} - -export interface SmartContractQueryMethodReadSuccess { +export interface SmartContractQueryMethodSuccess { is_error: false; result: { names: Array ]>; @@ -106,7 +101,7 @@ export interface SmartContractQueryMethodReadSuccess { }; } -export interface SmartContractQueryMethodReadError { +export interface SmartContractQueryMethodError { is_error: true; result: { code: number; @@ -122,7 +117,7 @@ export interface SmartContractQueryMethodReadError { }; } -export type SmartContractQueryMethodRead = SmartContractQueryMethodReadSuccess | SmartContractQueryMethodReadError; +export type SmartContractQueryMethod = SmartContractQueryMethodSuccess | SmartContractQueryMethodError; // VERIFICATION @@ -136,6 +131,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 { @@ -160,6 +156,7 @@ export interface SmartContractVerificationError { export type SolidityscanReport = { scan_report: { + contractname: string; scan_status: string; scan_summary: { issue_severity_distribution: { 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/l2Deposits.ts b/types/api/l2Deposits.ts new file mode 100644 index 0000000000..1704414da5 --- /dev/null +++ b/types/api/l2Deposits.ts @@ -0,0 +1,17 @@ +export type L2DepositsItem = { + l1_block_number: number; + l1_tx_hash: string; + l1_block_timestamp: string; + l1_tx_origin: string; + l2_tx_gas_limit: string; + l2_tx_hash: string; +} + +export type L2DepositsResponse = { + items: Array; + next_page_params: { + items_count: number; + l1_block_number: number; + tx_hash: string; + }; +} diff --git a/types/api/l2OutputRoots.ts b/types/api/l2OutputRoots.ts new file mode 100644 index 0000000000..8bcfe07d75 --- /dev/null +++ b/types/api/l2OutputRoots.ts @@ -0,0 +1,16 @@ +export type L2OutputRootsItem = { + l1_block_number: number; + l1_timestamp: string; + l1_tx_hash: string; + l2_block_number: number; + l2_output_index: number; + output_root: string; +} + +export type L2OutputRootsResponse = { + items: Array; + next_page_params: { + index: number; + items_count: number; + }; +} diff --git a/types/api/l2TxnBatches.ts b/types/api/l2TxnBatches.ts new file mode 100644 index 0000000000..62f320e14e --- /dev/null +++ b/types/api/l2TxnBatches.ts @@ -0,0 +1,15 @@ +export type L2TxnBatchesItem = { + epoch_number: number; + l1_tx_hashes: Array; + l1_timestamp: string; + l2_block_number: number; + tx_count: number; +} + +export type L2TxnBatchesResponse = { + items: Array; + next_page_params: { + block_number: number; + items_count: number; + }; +} diff --git a/types/api/l2Withdrawals.ts b/types/api/l2Withdrawals.ts new file mode 100644 index 0000000000..e3f69005a3 --- /dev/null +++ b/types/api/l2Withdrawals.ts @@ -0,0 +1,27 @@ +import type { AddressParam } from './addressParams'; + +export type L2WithdrawalsItem = { + 'challenge_period_end': string | null; + 'from': AddressParam | null; + 'l1_tx_hash': string | null; + 'l2_timestamp': string | null; + 'l2_tx_hash': string; + 'msg_nonce': number; + 'msg_nonce_version': number; + 'status': string; +} + +export type L2WithdrawalStatus = + 'In challenge period' | + 'Ready for relay' | + 'Relayed' | + 'Waiting for state root' | + 'Ready to prove'; + +export type L2WithdrawalsResponse = { + items: Array; + 'next_page_params': { + 'items_count': number; + 'nonce': string; + }; +} diff --git a/types/api/noves.ts b/types/api/noves.ts new file mode 100644 index 0000000000..2f85ca6391 --- /dev/null +++ b/types/api/noves.ts @@ -0,0 +1,125 @@ +export interface NovesResponseData { + txTypeVersion: number; + chain: string; + accountAddress: string; + classificationData: NovesClassificationData; + rawTransactionData: NovesRawTransactionData; +} + +export interface NovesClassificationData { + type: string; + typeFormatted?: string; + description: string; + sent: Array; + received: Array; + approved?: Approved; + protocol?: { + name: string | null; + }; + source: { + type: string | null; + }; + message?: string; + deployedContractAddress?: string; +} + +export interface Approved { + amount: string; + spender: string; + token?: NovesToken; + nft?: NovesNft; +} + +export interface NovesSentReceived { + action: string; + actionFormatted?: string; + amount: string; + to: NovesTo; + from: NovesFrom; + token?: NovesToken; + nft?: NovesNft; +} + +export interface NovesToken { + symbol: string; + name: string; + decimals: number; + address: string; + id?: string; +} + +export interface NovesNft { + name: string; + id: string; + symbol: string; + address: string; +} + +export interface NovesFrom { + name: string | null; + address: string; +} + +export interface NovesTo { + name: string | null; + address: string | null; +} + +export interface NovesRawTransactionData { + transactionHash: string; + fromAddress: string; + toAddress: string; + blockNumber: number; + gas: number; + gasPrice: number; + transactionFee: NovesTransactionFee | number; + timestamp: number; +} + +export interface NovesTransactionFee { + amount: string; + currency?: string; + token?: { + decimals: number; + symbol: string; + }; +} + +export interface NovesAccountHistoryResponse { + hasNextPage: boolean; + items: Array; + pageNumber: number; + pageSize: number; + next_page_params?: { + startBlock: string; + endBlock: string; + pageNumber: number; + pageSize: number; + ignoreTransactions: string; + viewAsAccountAddress: string; + }; +} + +export const NovesHistoryFilterValues = [ 'received', 'sent' ] as const; + +export type NovesHistoryFilterValue = typeof NovesHistoryFilterValues[number] | undefined; + +export interface NovesHistoryFilters { + filter?: NovesHistoryFilterValue; +} + +export interface NovesDescribeResponse { + type: string; + description: string; +} + +export interface NovesDescribeTxsResponse { + txHash: string; + type: string; + description: string; +}[]; + +export interface NovesTxTranslation { + data?: NovesDescribeTxsResponse; + isLoading: boolean; +} diff --git a/types/api/search.ts b/types/api/search.ts index 49b1e87ec3..bb9330a8a0 100644 --- a/types/api/search.ts +++ b/types/api/search.ts @@ -31,6 +31,20 @@ export interface SearchResultAddressOrContract { }; } +export interface SearchResultDomain { + type: 'ens_domain'; + name: string | null; + address: string; + is_smart_contract_verified: boolean; + url?: string; // not used by the frontend, we build the url ourselves + ens_info: { + address_hash: string; + expiry_date?: string; + name: string; + names_count: number; + }; +} + export interface SearchResultLabel { type: 'label'; address: string; @@ -55,6 +69,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 +82,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 | SearchResultDomain; export interface SearchResult { items: Array; @@ -86,5 +107,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/stats.ts b/types/api/stats.ts index 40fe7d34e2..5f3467597c 100644 --- a/types/api/stats.ts +++ b/types/api/stats.ts @@ -16,6 +16,8 @@ export type HomeStats = { network_utilization_percentage: number; tvl: string | null; rootstock_locked_btc?: string | null; + last_output_root_size?: string | null; + secondary_coin_price?: string | null; } export type GasPrices = { diff --git a/types/api/token.ts b/types/api/token.ts index 417c74aab6..7cae408f7d 100644 --- a/types/api/token.ts +++ b/types/api/token.ts @@ -1,7 +1,7 @@ import type { TokenInfoApplication } from './account'; import type { AddressParam } from './addressParams'; -export type NFTTokenType = 'ERC-721' | 'ERC-1155'; +export type NFTTokenType = 'ERC-721' | 'ERC-1155' | 'ERC-404'; export type TokenType = 'ERC-20' | NFTTokenType; export interface TokenInfo { @@ -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/tokenTransfer.ts b/types/api/tokenTransfer.ts index 860d14455a..bb580516dd 100644 --- a/types/api/tokenTransfer.ts +++ b/types/api/tokenTransfer.ts @@ -16,6 +16,14 @@ export type Erc1155TotalPayload = { token_id: string | null; } +export type Erc404TotalPayload = { + decimals: string; + value: string; + token_id: null; +} | { + token_id: string; +}; + export type TokenTransfer = ( { token: TokenInfo<'ERC-20'>; @@ -28,6 +36,10 @@ export type TokenTransfer = ( { token: TokenInfo<'ERC-1155'>; total: Erc1155TotalPayload; + } | + { + token: TokenInfo<'ERC-404'>; + total: Erc404TotalPayload; } ) & TokenTransferBase diff --git a/types/api/tokens.ts b/types/api/tokens.ts index adeca8d768..6538e690ec 100644 --- a/types/api/tokens.ts +++ b/types/api/tokens.ts @@ -8,7 +8,7 @@ export type TokensResponse = { items_count: number; name: string; market_cap: string | null; - }; + } | null; } export type TokensFilters = { q: string; type: Array | undefined }; diff --git a/types/api/transaction.ts b/types/api/transaction.ts index 5d077cb634..f6a3841354 100644 --- a/types/api/transaction.ts +++ b/types/api/transaction.ts @@ -2,10 +2,12 @@ import type { AddressParam } from './addressParams'; import type { BlockTransactionsResponse } from './block'; import type { DecodedInput } from './decodedInput'; import type { Fee } from './fee'; +import type { NovesTxTranslation } from './noves'; import type { OptimisticL2WithdrawalStatus } from './optimisticL2'; import type { TokenInfo } from './token'; import type { TokenTransfer } from './tokenTransfer'; import type { TxAction } from './txAction'; +import type { ZkSyncBatchesItem } from './zkSyncL2'; export type TransactionRevertReason = { raw: string; @@ -79,10 +81,29 @@ export type Transaction = { zkevm_batch_number?: number; zkevm_status?: typeof ZKEVM_L2_TX_STATUSES[number]; zkevm_sequence_hash?: string; + // zkSync FIELDS + zksync?: Omit & { + 'batch_number': number | null; + }; + // 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; + // Noves-fi + translation?: NovesTxTranslation; } export const ZKEVM_L2_TX_STATUSES = [ 'Confirmed by Sequencer', 'L1 Confirmed' ]; +export interface TransactionsStats { + pending_transactions_count: string; + transaction_fees_avg_24h: string; + transaction_fees_sum_24h: string; + transactions_count_24h: string; +} + export type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending; export interface TransactionsResponseValidated { @@ -104,6 +125,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: { @@ -119,7 +149,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/txStateChanges.ts b/types/api/txStateChanges.ts index 459f196aff..284a921699 100644 --- a/types/api/txStateChanges.ts +++ b/types/api/txStateChanges.ts @@ -41,6 +41,13 @@ export interface TxStateChangeTokenErc1155 { token_id: string; } +export interface TxStateChangeTokenErc404 { + type: 'token'; + token: TokenInfo<'ERC-404'>; + change: string; + token_id: string; +} + export type TxStateChanges = { items: Array; next_page_params: { 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/zkEvmL2.ts b/types/api/zkEvmL2.ts index 3a82b375ae..04ebc265c0 100644 --- a/types/api/zkEvmL2.ts +++ b/types/api/zkEvmL2.ts @@ -1,11 +1,47 @@ import type { Transaction } from './transaction'; +export type ZkEvmL2DepositsItem = { + block_number: number; + index: number; + l1_transaction_hash: string; + l2_transaction_hash: string | null; + timestamp: string; + value: string; + symbol: string; +} + +export type ZkEvmL2DepositsResponse = { + items: Array; + next_page_params: { + items_count: number; + index: number; + }; +} + +export type ZkEvmL2WithdrawalsItem = { + block_number: number; + index: number; + l1_transaction_hash: string | null; + l2_transaction_hash: string; + timestamp: string; + value: string; + symbol: string; +} + +export type ZkEvmL2WithdrawalsResponse = { + items: Array; + next_page_params: { + items_count: number; + index: number; + }; +} + export type ZkEvmL2TxnBatchesItem = { number: number; verify_tx_hash: string | null; sequence_tx_hash: string | null; status: string; - timestamp: string; + timestamp: string | null; tx_count: number; } @@ -26,7 +62,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/api/zkSyncL2.ts b/types/api/zkSyncL2.ts new file mode 100644 index 0000000000..4d038477ff --- /dev/null +++ b/types/api/zkSyncL2.ts @@ -0,0 +1,52 @@ +import type { Transaction } from './transaction'; + +export const ZKSYNC_L2_TX_BATCH_STATUSES = [ + 'Processed on L2' as const, + 'Sealed on L2' as const, + 'Sent to L1' as const, + 'Validated on L1' as const, + 'Executed on L1' as const, +]; + +export type ZkSyncBatchStatus = typeof ZKSYNC_L2_TX_BATCH_STATUSES[number]; + +export interface ZkSyncBatchesItem { + commit_transaction_hash: string | null; + commit_transaction_timestamp: string | null; + execute_transaction_hash: string | null; + execute_transaction_timestamp: string | null; + number: number; + prove_transaction_hash: string | null; + prove_transaction_timestamp: string | null; + status: ZkSyncBatchStatus; + timestamp: string; + tx_count: number; +} + +export type ZkSyncBatchesResponse = { + items: Array; + next_page_params: { + number: number; + items_count: number; + } | null; +} + +export interface ZkSyncBatch extends Omit { + start_block: number; + end_block: number; + l1_gas_price: string; + l1_tx_count: number; + l2_fair_gas_price: string; + l2_tx_count: number; + root_hash: string; +} + +export type ZkSyncBatchTxs = { + items: Array; + next_page_params: { + batch_number: string; + block_number: number; + index: number; + items_count: number; + } | null; +} diff --git a/types/client/adProviders.ts b/types/client/adProviders.ts index d972932190..ac0418496d 100644 --- a/types/client/adProviders.ts +++ b/types/client/adProviders.ts @@ -1,7 +1,10 @@ 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_BANNER_ADDITIONAL_PROVIDERS = [ 'adbutler' ] as const; +export type AdBannerAdditionalProviders = ArrayElement; + export const SUPPORTED_AD_TEXT_PROVIDERS = [ 'coinzilla', 'none' ] as const; export type AdTextProviders = ArrayElement; diff --git a/types/client/address.ts b/types/client/address.ts index 719aa339bf..e7a181ee3b 100644 --- a/types/client/address.ts +++ b/types/client/address.ts @@ -8,4 +8,8 @@ export type CsvExportParams = { type: 'logs'; filterType?: 'topic'; filterValue?: string; +} | { + type: 'holders'; + filterType?: undefined; + filterValue?: undefined; } diff --git a/types/client/addressMetadata.ts b/types/client/addressMetadata.ts new file mode 100644 index 0000000000..8281e1cf1b --- /dev/null +++ b/types/client/addressMetadata.ts @@ -0,0 +1,10 @@ +import type { AddressMetadataTagApi } from 'types/api/addressMetadata'; + +export interface AddressMetadataInfoFormatted { + addresses: Record; + reputation: number | null; + }>; +} + +export type AddressMetadataTagFormatted = AddressMetadataTagApi; 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/marketplace.ts b/types/client/marketplace.ts index eb132aac20..9063f538af 100644 --- a/types/client/marketplace.ts +++ b/types/client/marketplace.ts @@ -1,3 +1,5 @@ +import type { SolidityscanReport } from 'types/api/contract'; + export type MarketplaceAppPreview = { id: string; external?: boolean; @@ -11,16 +13,58 @@ 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 type MarketplaceAppWithSecurityReport = MarketplaceAppOverview & { + securityReport?: MarketplaceAppSecurityReport; } export enum MarketplaceCategory { ALL = 'All', FAVORITES = 'Favorites', } + +export enum ContractListTypes { + ANALYZED = 'Analyzed', + ALL = 'All', + VERIFIED = 'Verified', +} + +export enum MarketplaceDisplayType { + DEFAULT = 'default', + SCORES = 'scores', +} + +export type MarketplaceAppSecurityReport = { + overallInfo: { + verifiedNumber: number; + totalContractsNumber: number; + solidityScanContractsNumber: number; + securityScore: number; + totalIssues?: number; + issueSeverityDistribution: SolidityscanReport['scan_report']['scan_summary']['issue_severity_distribution']; + }; + contractsData: Array<{ + address: string; + isVerified: boolean; + solidityScanReport?: SolidityscanReport['scan_report'] | null; + }>; +} + +export type MarketplaceAppSecurityReportRaw = { + appName: string; + chainsData: { + [chainId: string]: MarketplaceAppSecurityReport; + }; +} diff --git a/types/client/rollup.ts b/types/client/rollup.ts index 2c62078efc..3e5e5da91d 100644 --- a/types/client/rollup.ts +++ b/types/client/rollup.ts @@ -4,6 +4,7 @@ export const ROLLUP_TYPES = [ 'optimistic', 'shibarium', 'zkEvm', + 'zkSync', ] as const; export type RollupType = ArrayElement; diff --git a/types/client/txInterpretation.ts b/types/client/txInterpretation.ts index e264b267bc..23f55ed217 100644 --- a/types/client/txInterpretation.ts +++ b/types/client/txInterpretation.ts @@ -2,6 +2,7 @@ import type { ArrayElement } from 'types/utils'; export const PROVIDERS = [ 'blockscout', + 'noves', 'none', ] as const; diff --git a/types/client/txs-sort.ts b/types/client/txs-sort.ts new file mode 100644 index 0000000000..501b625a56 --- /dev/null +++ b/types/client/txs-sort.ts @@ -0,0 +1 @@ +export type Sort = 'val-desc' | 'val-asc' | 'fee-desc' | 'fee-asc' | ''; diff --git a/types/envs.ts b/types/envs.ts new file mode 100644 index 0000000000..2b796fc911 --- /dev/null +++ b/types/envs.ts @@ -0,0 +1,146 @@ +export type NextPublicEnvs = { + // app envs + NEXT_PUBLIC_APP_PROTOCOL?: 'http' | 'https'; + NEXT_PUBLIC_APP_HOST: string; + NEXT_PUBLIC_APP_PORT?: string; + + // blockchain parameters + NEXT_PUBLIC_NETWORK_NAME: string; + NEXT_PUBLIC_NETWORK_SHORT_NAME?: string; + NEXT_PUBLIC_NETWORK_ID: string; + NEXT_PUBLIC_NETWORK_RPC_URL?: string; + NEXT_PUBLIC_NETWORK_CURRENCY_NAME?: string; + NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL?: string; + NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS?: string; + NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE?: 'validation' | 'mining'; + NEXT_PUBLIC_IS_TESTNET?: 'true' | ''; + + // api envs + NEXT_PUBLIC_API_PROTOCOL?: 'http' | 'https'; + NEXT_PUBLIC_API_HOST: string; + NEXT_PUBLIC_API_PORT?: string; + NEXT_PUBLIC_API_BASE_PATH?: string; + NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL?: 'ws' | 'wss'; + + // UI configuration envs + // homepage + NEXT_PUBLIC_HOMEPAGE_CHARTS?: string; + NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR?: string; + NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND?: string; + NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER?: 'true' | 'false'; + NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME?: 'true' | 'false'; + // sidebar + NEXT_PUBLIC_FEATURED_NETWORKS?: string; + NEXT_PUBLIC_OTHER_LINKS?: string; + NEXT_PUBLIC_NETWORK_LOGO?: string; + NEXT_PUBLIC_NETWORK_LOGO_DARK?: string; + NEXT_PUBLIC_NETWORK_ICON?: string; + NEXT_PUBLIC_NETWORK_ICON_DARK?: string; + // footer + NEXT_PUBLIC_FOOTER_LINKS?: string; + // views + NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS?: string; + // misc + NEXT_PUBLIC_NETWORK_EXPLORERS?: string; + NEXT_PUBLIC_HIDE_INDEXING_ALERT?: 'true' | 'false'; + + // features envs + NEXT_PUBLIC_API_SPEC_URL?: string; + NEXT_PUBLIC_GRAPHIQL_TRANSACTION?: string; + NEXT_PUBLIC_WEB3_WALLETS?: string; + NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET?: 'true' | 'false'; + NEXT_PUBLIC_AD_TEXT_PROVIDER?: 'coinzilla' | 'none'; + NEXT_PUBLIC_STATS_API_HOST?: string; + NEXT_PUBLIC_VISUALIZE_API_HOST?: string; + NEXT_PUBLIC_CONTRACT_INFO_API_HOST?: string; + + // external services envs + NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID?: string; + NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY?: string; + NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID?: string; + NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN?: string; + + // utilities + NEXT_PUBLIC_GIT_TAG?: string; + NEXT_PUBLIC_GIT_COMMIT_SHA?: string; +} +& NextPublicEnvsAccount +& NextPublicEnvsMarketplace +& NextPublicEnvsRollup +& NextPublicEnvsBeacon +& NextPublicEnvsAdsBanner +& NextPublicEnvsSentry; + +type NextPublicEnvsAccount = +{ + NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED?: undefined; + NEXT_PUBLIC_AUTH_URL?: undefined; + NEXT_PUBLIC_LOGOUT_URL?: undefined; + NEXT_PUBLIC_AUTH0_CLIENT_ID?: undefined; +} | +{ + NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED: 'true'; + NEXT_PUBLIC_AUTH_URL?: string; + NEXT_PUBLIC_LOGOUT_URL: string; + NEXT_PUBLIC_AUTH0_CLIENT_ID: string; + NEXT_PUBLIC_ADMIN_SERVICE_API_HOST?: string; +} + +type NextPublicEnvsMarketplace = +{ + NEXT_PUBLIC_MARKETPLACE_CONFIG_URL: string; + NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: string; +} | +{ + NEXT_PUBLIC_MARKETPLACE_CONFIG_URL?: undefined; + NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM?: undefined; +} + +type NextPublicEnvsRollup = +{ + NEXT_PUBLIC_IS_L2_NETWORK: 'true'; + NEXT_PUBLIC_L1_BASE_URL: string; + NEXT_PUBLIC_L2_WITHDRAWAL_URL: string; +} | +{ + NEXT_PUBLIC_IS_L2_NETWORK?: undefined; + NEXT_PUBLIC_L1_BASE_URL?: undefined; + NEXT_PUBLIC_L2_WITHDRAWAL_URL?: undefined; +} + +type NextPublicEnvsBeacon = +{ + NEXT_PUBLIC_HAS_BEACON_CHAIN: 'true'; + NEXT_PUBLIC_BEACON_CHAIN_CURRENCY_SYMBOL?: string; +} | +{ + NEXT_PUBLIC_HAS_BEACON_CHAIN?: undefined; + NEXT_PUBLIC_BEACON_CHAIN_CURRENCY_SYMBOL?: undefined; +} + +type NextPublicEnvsAdsBanner = +{ + NEXT_PUBLIC_AD_BANNER_PROVIDER: 'slise' | 'coinzilla' | 'none'; +} | +{ + NEXT_PUBLIC_AD_BANNER_PROVIDER: 'adbutler'; + NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP: string; + NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE: string; +} | +{ + NEXT_PUBLIC_AD_BANNER_PROVIDER?: undefined; +} + +type NextPublicEnvsSentry = +{ + NEXT_PUBLIC_SENTRY_DSN: string; + SENTRY_CSP_REPORT_URI?: string; + NEXT_PUBLIC_APP_INSTANCE?: string; + NEXT_PUBLIC_APP_ENV?: string; +} | +{ + NEXT_PUBLIC_SENTRY_DSN?: undefined; + SENTRY_CSP_REPORT_URI?: undefined; + NEXT_PUBLIC_APP_INSTANCE?: undefined; + NEXT_PUBLIC_APP_ENV?: undefined; +} diff --git a/types/homepage.ts b/types/homepage.ts index b773176288..2492134e97 100644 --- a/types/homepage.ts +++ b/types/homepage.ts @@ -1 +1,2 @@ -export type ChainIndicatorId = 'daily_txs' | 'coin_price' | 'market_cap' | 'tvl'; +export const CHAIN_INDICATOR_IDS = [ 'daily_txs', 'coin_price', 'secondary_coin_price', 'market_cap', 'tvl' ] as const; +export type ChainIndicatorId = typeof CHAIN_INDICATOR_IDS[number]; diff --git a/types/utils.ts b/types/utils.ts index 6a41f16f0c..06bb70df3e 100644 --- a/types/utils.ts +++ b/types/utils.ts @@ -7,3 +7,7 @@ export type ExcludeNull = T extends null ? never : T; export type ExcludeUndefined = T extends undefined ? never : T; export type KeysOfObjectOrNull = keyof ExcludeNull; + +/** Combines members of an intersection into a readable type. */ +// https://twitter.com/mattpocockuk/status/1622730173446557697?s=20&t=NdpAcmEFXY01xkqU3KO0Mg +export type Evaluate = { [key in keyof Type]: Type[key] } & unknown diff --git a/types/views/block.ts b/types/views/block.ts index e924d6189b..838dc523b7 100644 --- a/types/views/block.ts +++ b/types/views/block.ts @@ -5,6 +5,8 @@ export const BLOCK_FIELDS_IDS = [ 'total_reward', 'nonce', 'miner', + 'L1_status', + 'batch', ] as const; export type BlockFieldId = ArrayElement; diff --git a/types/views/tx.ts b/types/views/tx.ts index 21800d80e0..d3a30adcc5 100644 --- a/types/views/tx.ts +++ b/types/views/tx.ts @@ -7,6 +7,8 @@ export const TX_FIELDS_IDS = [ 'tx_fee', 'gas_fees', 'burnt_fees', + 'L1_status', + 'batch', ] as const; export type TxFieldsId = ArrayElement; diff --git a/types/web3.ts b/types/web3.ts new file mode 100644 index 0000000000..1eab7a8cfa --- /dev/null +++ b/types/web3.ts @@ -0,0 +1,65 @@ +// copied from node_modules/@wagmi/core/src/connectors/injected.ts +import type { EIP1193Provider } from 'viem'; + +import type { Evaluate } from './utils'; + +type WalletProviderFlags = + | 'isApexWallet' + | 'isAvalanche' + | 'isBackpack' + | 'isBifrost' + | 'isBitKeep' + | 'isBitski' + | 'isBlockWallet' + | 'isBraveWallet' + | 'isCoinbaseWallet' + | 'isDawn' + | 'isEnkrypt' + | 'isExodus' + | 'isFrame' + | 'isFrontier' + | 'isGamestop' + | 'isHyperPay' + | 'isImToken' + | 'isKuCoinWallet' + | 'isMathWallet' + | 'isMetaMask' + | 'isOkxWallet' + | 'isOKExWallet' + | 'isOneInchAndroidWallet' + | 'isOneInchIOSWallet' + | 'isOpera' + | 'isPhantom' + | 'isPortal' + | 'isRabby' + | 'isRainbow' + | 'isStatus' + | 'isTally' + | 'isTokenPocket' + | 'isTokenary' + | 'isTrust' + | 'isTrustWallet' + | 'isXDEFI' + | 'isZerion' + +export type WalletProvider = Evaluate< +EIP1193Provider & { + [key in WalletProviderFlags]?: true | undefined +} & { + providers?: Array | undefined; + + /** Only exists in MetaMask as of 2022/04/03 */ + _events?: { connect?: (() => void) | undefined } | undefined; + + /** Only exists in MetaMask as of 2022/04/03 */ + _state?: + | { + accounts?: Array; + initialized?: boolean; + isConnected?: boolean; + isPermanentlyDisconnected?: boolean; + isUnlocked?: boolean; + } + | undefined; +} +> diff --git a/ui/address/AddressAccountHistory.tsx b/ui/address/AddressAccountHistory.tsx new file mode 100644 index 0000000000..6d76d04302 --- /dev/null +++ b/ui/address/AddressAccountHistory.tsx @@ -0,0 +1,132 @@ +import { Box, Hide, Show, Table, + Tbody, Th, Tr } from '@chakra-ui/react'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { NovesHistoryFilterValue } from 'types/api/noves'; +import { NovesHistoryFilterValues } from 'types/api/noves'; + +import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; +import useIsMounted from 'lib/hooks/useIsMounted'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import { NOVES_TRANSLATE } from 'stubs/noves/NovesTranslate'; +import { generateListStub } from 'stubs/utils'; +import AddressAccountHistoryTableItem from 'ui/address/accountHistory/AddressAccountHistoryTableItem'; +import ActionBar from 'ui/shared/ActionBar'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import { getFromToValue } from 'ui/shared/Noves/utils'; +import Pagination from 'ui/shared/pagination/Pagination'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; +import TheadSticky from 'ui/shared/TheadSticky'; + +import AddressAccountHistoryListItem from './accountHistory/AddressAccountHistoryListItem'; +import AccountHistoryFilter from './AddressAccountHistoryFilter'; + +const getFilterValue = (getFilterValueFromQuery).bind(null, NovesHistoryFilterValues); + +type Props = { + scrollRef?: React.RefObject; + shouldRender?: boolean; +} + +const AddressAccountHistory = ({ scrollRef, shouldRender = true }: Props) => { + const router = useRouter(); + const isMounted = useIsMounted(); + + const currentAddress = getQueryParamString(router.query.hash).toLowerCase(); + + const [ filterValue, setFilterValue ] = React.useState(getFilterValue(router.query.filter)); + + const { data, isError, pagination, isPlaceholderData } = useQueryWithPages({ + resourceName: 'noves_address_history', + pathParams: { address: currentAddress }, + scrollRef, + options: { + placeholderData: generateListStub<'noves_address_history'>(NOVES_TRANSLATE, 10, { hasNextPage: false, pageNumber: 1, pageSize: 10 }), + }, + }); + + const handleFilterChange = React.useCallback((val: string | Array) => { + + const newVal = getFilterValue(val); + setFilterValue(newVal); + }, [ ]); + + if (!isMounted || !shouldRender) { + return null; + } + + const actionBar = ( + + + + + + ); + + const filteredData = isPlaceholderData ? data?.items : data?.items.filter(i => filterValue ? getFromToValue(i, currentAddress) === filterValue : i); + + const content = ( + + + { filteredData?.map((item, i) => ( + + )) } + + + + + + + + + + + + + { filteredData?.map((item, i) => ( + + )) } + +
+ Age + + Action + + From/To +
+
+
+ ); + + return ( + + ); +}; + +export default AddressAccountHistory; diff --git a/ui/address/AddressAccountHistoryFilter.tsx b/ui/address/AddressAccountHistoryFilter.tsx new file mode 100644 index 0000000000..d66519d635 --- /dev/null +++ b/ui/address/AddressAccountHistoryFilter.tsx @@ -0,0 +1,55 @@ +import { + Menu, + MenuButton, + MenuList, + MenuOptionGroup, + MenuItemOption, + useDisclosure, +} from '@chakra-ui/react'; +import React from 'react'; + +import type { NovesHistoryFilterValue } from 'types/api/noves'; + +import useIsInitialLoading from 'lib/hooks/useIsInitialLoading'; +import FilterButton from 'ui/shared/filters/FilterButton'; + +interface Props { + isActive: boolean; + defaultFilter: NovesHistoryFilterValue; + onFilterChange: (nextValue: string | Array) => void; + isLoading?: boolean; +} + +const AccountHistoryFilter = ({ onFilterChange, defaultFilter, isActive, isLoading }: Props) => { + const { isOpen, onToggle } = useDisclosure(); + const isInitialLoading = useIsInitialLoading(isLoading); + + const onCloseMenu = React.useCallback(() => { + if (isOpen) { + onToggle(); + } + }, [ isOpen, onToggle ]); + + return ( + + + + + + + All + Received from + Sent to + + + + ); +}; + +export default React.memo(AccountHistoryFilter); diff --git a/ui/address/AddressBlocksValidated.tsx b/ui/address/AddressBlocksValidated.tsx index b72ef4776e..0c6f082281 100644 --- a/ui/address/AddressBlocksValidated.tsx +++ b/ui/address/AddressBlocksValidated.tsx @@ -8,6 +8,7 @@ import type { AddressBlocksValidatedResponse } from 'types/api/address'; import config from 'configs/app'; import { getResourceKey } from 'lib/api/useApiQuery'; +import useIsMounted from 'lib/hooks/useIsMounted'; import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketMessage from 'lib/socket/useSocketMessage'; import { currencyUnits } from 'lib/units'; @@ -25,12 +26,14 @@ import AddressBlocksValidatedTableItem from './blocksValidated/AddressBlocksVali interface Props { scrollRef?: React.RefObject; + shouldRender?: boolean; } -const AddressBlocksValidated = ({ scrollRef }: Props) => { +const AddressBlocksValidated = ({ scrollRef, shouldRender = true }: Props) => { const [ socketAlert, setSocketAlert ] = React.useState(false); const queryClient = useQueryClient(); const router = useRouter(); + const isMounted = useIsMounted(); const addressHash = String(router.query.hash); const query = useQueryWithPages({ @@ -84,6 +87,10 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => { handler: handleNewSocketMessage, }); + if (!isMounted || !shouldRender) { + return null; + } + const content = query.data?.items ? ( <> { socketAlert && } diff --git a/ui/address/AddressCoinBalance.tsx b/ui/address/AddressCoinBalance.tsx index 1f109cf366..d4b366530d 100644 --- a/ui/address/AddressCoinBalance.tsx +++ b/ui/address/AddressCoinBalance.tsx @@ -6,6 +6,7 @@ import type { SocketMessage } from 'lib/socket/types'; import type { AddressCoinBalanceHistoryResponse } from 'types/api/address'; import { getResourceKey } from 'lib/api/useApiQuery'; +import useIsMounted from 'lib/hooks/useIsMounted'; import getQueryParamString from 'lib/router/getQueryParamString'; import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketMessage from 'lib/socket/useSocketMessage'; @@ -17,10 +18,16 @@ import SocketAlert from 'ui/shared/SocketAlert'; import AddressCoinBalanceChart from './coinBalance/AddressCoinBalanceChart'; import AddressCoinBalanceHistory from './coinBalance/AddressCoinBalanceHistory'; -const AddressCoinBalance = () => { +type Props = { + shouldRender?: boolean; +} + +const AddressCoinBalance = ({ shouldRender = true }: Props) => { const [ socketAlert, setSocketAlert ] = React.useState(false); const queryClient = useQueryClient(); const router = useRouter(); + const isMounted = useIsMounted(); + const scrollRef = React.useRef(null); const addressHash = getQueryParamString(router.query.hash); @@ -78,6 +85,10 @@ const AddressCoinBalance = () => { handler: handleNewSocketMessage, }); + if (!isMounted || !shouldRender) { + return null; + } + return ( <> { socketAlert && } diff --git a/ui/address/AddressContract.pwstory.tsx b/ui/address/AddressContract.pwstory.tsx new file mode 100644 index 0000000000..c558dd9ca2 --- /dev/null +++ b/ui/address/AddressContract.pwstory.tsx @@ -0,0 +1,18 @@ +import { useRouter } from 'next/router'; +import React from 'react'; + +import useApiQuery from 'lib/api/useApiQuery'; +import useContractTabs from 'lib/hooks/useContractTabs'; +import getQueryParamString from 'lib/router/getQueryParamString'; + +import AddressContract from './AddressContract'; + +const AddressContractPwStory = () => { + const router = useRouter(); + const hash = getQueryParamString(router.query.hash); + const addressQuery = useApiQuery('address', { pathParams: { hash } }); + const { tabs } = useContractTabs(addressQuery.data, false); + return ; +}; + +export default AddressContractPwStory; diff --git a/ui/address/AddressContract.tsx b/ui/address/AddressContract.tsx index a2dd58db5f..a349532a7a 100644 --- a/ui/address/AddressContract.tsx +++ b/ui/address/AddressContract.tsx @@ -3,29 +3,24 @@ import React from 'react'; import type { RoutedSubTab } from 'ui/shared/Tabs/types'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; -import Web3ModalProvider from 'ui/shared/Web3ModalProvider'; interface Props { tabs: Array; - addressHash?: string; + isLoading: boolean; + shouldRender?: boolean; } const TAB_LIST_PROPS = { columnGap: 3, }; -const AddressContract = ({ tabs }: Props) => { - const fallback = React.useCallback(() => { - const noProviderTabs = tabs.filter(({ id }) => id === 'contact_code' || id.startsWith('read_')); - return ( - - ); - }, [ tabs ]); +const AddressContract = ({ tabs, isLoading, shouldRender }: Props) => { + if (!shouldRender) { + return null; + } return ( - - - + ); }; diff --git a/ui/address/AddressDetails.pw.tsx b/ui/address/AddressDetails.pw.tsx index 26920f92e1..310c175141 100644 --- a/ui/address/AddressDetails.pw.tsx +++ b/ui/address/AddressDetails.pw.tsx @@ -1,6 +1,7 @@ import { test, expect } from '@playwright/experimental-ct-react'; import React from 'react'; -import type { WindowProvider } from 'wagmi'; + +import type { WalletProvider } from 'types/web3'; import * as addressMock from 'mocks/address/address'; import * as countersMock from 'mocks/address/counters'; @@ -19,6 +20,7 @@ const API_URL_COUNTERS = buildApiUrl('address_counters', { hash: ADDRESS_HASH }) const API_URL_TOKENS_ERC20 = buildApiUrl('address_tokens', { hash: ADDRESS_HASH }) + '?type=ERC-20'; const API_URL_TOKENS_ERC721 = buildApiUrl('address_tokens', { hash: ADDRESS_HASH }) + '?type=ERC-721'; const API_URL_TOKENS_ER1155 = buildApiUrl('address_tokens', { hash: ADDRESS_HASH }) + '?type=ERC-1155'; +const API_URL_TOKENS_ERC404 = buildApiUrl('address_tokens', { hash: ADDRESS_HASH }) + '?type=ERC-404'; const hooksConfig = { router: { query: { hash: ADDRESS_HASH }, @@ -69,11 +71,15 @@ test('token', async({ mount, page }) => { status: 200, body: JSON.stringify(tokensMock.erc1155List), }), { times: 1 }); + await page.route(API_URL_TOKENS_ERC404, async(route) => route.fulfill({ + status: 200, + body: JSON.stringify(tokensMock.erc404List), + }), { times: 1 }); await page.evaluate(() => { window.ethereum = { providers: [ { isMetaMask: true, _events: {} } ], - }as WindowProvider; + } as WalletProvider; }); const component = await mount( diff --git a/ui/address/AddressDetails.tsx b/ui/address/AddressDetails.tsx index e9ee456b88..7e88acdfb5 100644 --- a/ui/address/AddressDetails.tsx +++ b/ui/address/AddressDetails.tsx @@ -3,9 +3,11 @@ import { useRouter } from 'next/router'; import React from 'react'; import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; +import useIsMounted from 'lib/hooks/useIsMounted'; 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 AddressEntity from 'ui/shared/entities/address/AddressEntity'; @@ -58,20 +60,23 @@ 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; + const isMounted = useIsMounted(); - 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; - if (!data) { + if (!data || !isMounted) { return null; } diff --git a/ui/address/AddressInternalTxs.tsx b/ui/address/AddressInternalTxs.tsx index d4d302e39b..ebaf11cbaf 100644 --- a/ui/address/AddressInternalTxs.tsx +++ b/ui/address/AddressInternalTxs.tsx @@ -6,6 +6,7 @@ import type { AddressFromToFilter } from 'types/api/address'; import { AddressFromToFilterValues } from 'types/api/address'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; +import useIsMounted from 'lib/hooks/useIsMounted'; import { apos } from 'lib/html-entities'; import getQueryParamString from 'lib/router/getQueryParamString'; import { INTERNAL_TX } from 'stubs/internalTx'; @@ -22,8 +23,14 @@ import AddressIntTxsList from './internals/AddressIntTxsList'; const getFilterValue = (getFilterValueFromQuery).bind(null, AddressFromToFilterValues); -const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject}) => { +type Props = { + scrollRef?: React.RefObject; + shouldRender?: boolean; +} +const AddressInternalTxs = ({ scrollRef, shouldRender = true }: Props) => { const router = useRouter(); + const isMounted = useIsMounted(); + const [ filterValue, setFilterValue ] = React.useState(getFilterValue(router.query.filter)); const hash = getQueryParamString(router.query.hash); @@ -55,6 +62,10 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject diff --git a/ui/address/AddressLogs.tsx b/ui/address/AddressLogs.tsx index 5de923c0fd..286a3c6fac 100644 --- a/ui/address/AddressLogs.tsx +++ b/ui/address/AddressLogs.tsx @@ -1,6 +1,7 @@ import { useRouter } from 'next/router'; import React from 'react'; +import useIsMounted from 'lib/hooks/useIsMounted'; import getQueryParamString from 'lib/router/getQueryParamString'; import { LOG } from 'stubs/log'; import { generateListStub } from 'stubs/utils'; @@ -12,8 +13,14 @@ import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import AddressCsvExportLink from './AddressCsvExportLink'; -const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject}) => { +type Props ={ + scrollRef?: React.RefObject; + shouldRender?: boolean; +} + +const AddressLogs = ({ scrollRef, shouldRender = true }: Props) => { const router = useRouter(); + const isMounted = useIsMounted(); const hash = getQueryParamString(router.query.hash); const { data, isPlaceholderData, isError, pagination } = useQueryWithPages({ @@ -41,6 +48,10 @@ const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject ); + if (!isMounted || !shouldRender) { + return null; + } + const content = data?.items ? data.items.map((item, index) => ) : null; return ( diff --git a/ui/address/AddressTokenTransfers.tsx b/ui/address/AddressTokenTransfers.tsx index 0ebba17f8e..a275478e12 100644 --- a/ui/address/AddressTokenTransfers.tsx +++ b/ui/address/AddressTokenTransfers.tsx @@ -13,6 +13,7 @@ import { getResourceKey } from 'lib/api/useApiQuery'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; import useIsMobile from 'lib/hooks/useIsMobile'; +import useIsMounted from 'lib/hooks/useIsMounted'; import { apos } from 'lib/html-entities'; import getQueryParamString from 'lib/router/getQueryParamString'; import useSocketChannel from 'lib/socket/useSocketChannel'; @@ -63,14 +64,16 @@ const matchFilters = (filters: Filters, tokenTransfer: TokenTransfer, address?: type Props = { scrollRef?: React.RefObject; + shouldRender?: boolean; // for tests only overloadCount?: number; } -const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => { +const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT, shouldRender = true }: Props) => { const router = useRouter(); const queryClient = useQueryClient(); const isMobile = useIsMobile(); + const isMounted = useIsMounted(); const currentAddress = getQueryParamString(router.query.hash); @@ -179,6 +182,18 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Pr handler: handleNewSocketMessage, }); + const tokenData = React.useMemo(() => ({ + address: tokenFilter || '', + name: '', + icon_url: '', + symbol: '', + type: 'ERC-20' as const, + }), [ tokenFilter ]); + + if (!isMounted || !shouldRender) { + return null; + } + const numActiveFilters = (filters.type?.length || 0) + (filters.filter ? 1 : 0); const isActionBarHidden = !tokenFilter && !numActiveFilters && !data?.items.length && !currentAddress; @@ -218,14 +233,6 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Pr ) : null; - const tokenData = React.useMemo(() => ({ - address: tokenFilter || '', - name: '', - icon_url: '', - symbol: '', - type: 'ERC-20' as const, - }), [ tokenFilter ]); - const tokenFilterComponent = tokenFilter && ( Filtered by token diff --git a/ui/address/AddressTokens.pw.tsx b/ui/address/AddressTokens.pw.tsx index 7ee5556986..514b3aa7ad 100644 --- a/ui/address/AddressTokens.pw.tsx +++ b/ui/address/AddressTokens.pw.tsx @@ -37,6 +37,10 @@ const test = base.extend({ items: [ tokensMock.erc1155a, tokensMock.erc1155b ], next_page_params: nextPageParams, }; + const response404 = { + items: [ tokensMock.erc404a, tokensMock.erc404b ], + next_page_params: nextPageParams, + }; await page.route(API_URL_ADDRESS, (route) => route.fulfill({ status: 200, @@ -54,6 +58,10 @@ const test = base.extend({ status: 200, body: JSON.stringify(response1155), })); + await page.route(API_URL_TOKENS + '?type=ERC-404', (route) => route.fulfill({ + status: 200, + body: JSON.stringify(response404), + })); await page.route(API_URL_NFT, (route) => route.fulfill({ status: 200, body: JSON.stringify(tokensMock.nfts), @@ -217,6 +225,10 @@ base.describe('update balances via socket', () => { items: [ tokensMock.erc1155a ], next_page_params: null, }; + const response404 = { + items: [ tokensMock.erc404a ], + next_page_params: null, + }; await page.route(API_URL_ADDRESS, (route) => route.fulfill({ status: 200, @@ -234,6 +246,10 @@ base.describe('update balances via socket', () => { status: 200, body: JSON.stringify(response1155), })); + await page.route(API_URL_TOKENS + '?type=ERC-404', (route) => route.fulfill({ + status: 200, + body: JSON.stringify(response404), + })); const component = await mount( @@ -248,6 +264,7 @@ base.describe('update balances via socket', () => { await page.waitForResponse(API_URL_TOKENS + '?type=ERC-20'); await page.waitForResponse(API_URL_TOKENS + '?type=ERC-721'); await page.waitForResponse(API_URL_TOKENS + '?type=ERC-1155'); + await page.waitForResponse(API_URL_TOKENS + '?type=ERC-404'); await expect(component).toHaveScreenshot(); diff --git a/ui/address/AddressTokens.tsx b/ui/address/AddressTokens.tsx index 0e27a73f7d..3f2c35a403 100644 --- a/ui/address/AddressTokens.tsx +++ b/ui/address/AddressTokens.tsx @@ -9,6 +9,7 @@ import { useAppContext } from 'lib/contexts/app'; import * as cookies from 'lib/cookies'; import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; import useIsMobile from 'lib/hooks/useIsMobile'; +import useIsMounted from 'lib/hooks/useIsMounted'; import getQueryParamString from 'lib/router/getQueryParamString'; import { NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes'; import { ADDRESS_TOKEN_BALANCE_ERC_20, ADDRESS_NFT_1155, ADDRESS_COLLECTION } from 'stubs/address'; @@ -28,7 +29,8 @@ import TokenBalances from './tokens/TokenBalances'; type TNftDisplayType = 'collection' | 'list'; const TAB_LIST_PROPS = { - my: 3, + mt: 1, + mb: { base: 6, lg: 1 }, py: 5, columnGap: 3, }; @@ -40,9 +42,14 @@ const TAB_LIST_PROPS_MOBILE = { const getTokenFilterValue = (getFilterValuesFromQuery).bind(null, NFT_TOKEN_TYPE_IDS); -const AddressTokens = () => { +type Props = { + shouldRender?: boolean; +} + +const AddressTokens = ({ shouldRender = true }: Props) => { const router = useRouter(); const isMobile = useIsMobile(); + const isMounted = useIsMounted(); const scrollRef = React.useRef(null); @@ -98,6 +105,10 @@ const AddressTokens = () => { setTokenTypes(value); }, [ nftsQuery, collectionsQuery ]); + if (!isMounted || !shouldRender) { + return null; + } + const nftTypeFilter = ( 0 } contentProps={{ w: '200px' }} appliedFiltersNum={ tokenTypes?.length }> nftOnly onChange={ handleTokenTypesChange } defaultValue={ tokenTypes }/> diff --git a/ui/address/AddressTxs.tsx b/ui/address/AddressTxs.tsx index cc21c355df..2bb002b8f7 100644 --- a/ui/address/AddressTxs.tsx +++ b/ui/address/AddressTxs.tsx @@ -10,6 +10,7 @@ import type { Transaction, TransactionsSortingField, TransactionsSortingValue, T import { getResourceKey } from 'lib/api/useApiQuery'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import useIsMobile from 'lib/hooks/useIsMobile'; +import useIsMounted from 'lib/hooks/useIsMounted'; import getQueryParamString from 'lib/router/getQueryParamString'; import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketMessage from 'lib/socket/useSocketMessage'; @@ -20,6 +21,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'; @@ -46,13 +48,15 @@ const matchFilter = (filterValue: AddressFromToFilter, transaction: Transaction, type Props = { scrollRef?: React.RefObject; + shouldRender?: boolean; // for tests only overloadCount?: number; } -const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => { +const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT, shouldRender = true }: Props) => { const router = useRouter(); const queryClient = useQueryClient(); + const isMounted = useIsMounted(); const [ socketAlert, setSocketAlert ] = React.useState(''); const [ newItemsCount, setNewItemsCount ] = React.useState(0); @@ -85,7 +89,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 +127,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.'); @@ -155,6 +159,10 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => { handler: handleNewSocketMessage, }); + if (!isMounted || !shouldRender) { + return null; + } + const filter = ( - + All Outgoing transactions Incoming transactions diff --git a/ui/address/AddressUserOps.tsx b/ui/address/AddressUserOps.tsx index 7c93c83b47..0cc121d2e8 100644 --- a/ui/address/AddressUserOps.tsx +++ b/ui/address/AddressUserOps.tsx @@ -1,6 +1,7 @@ import { useRouter } from 'next/router'; import React from 'react'; +import useIsMounted from 'lib/hooks/useIsMounted'; import getQueryParamString from 'lib/router/getQueryParamString'; import { USER_OPS_ITEM } from 'stubs/userOps'; import { generateListStub } from 'stubs/utils'; @@ -9,10 +10,12 @@ import UserOpsContent from 'ui/userOps/UserOpsContent'; type Props = { scrollRef?: React.RefObject; + shouldRender?: boolean; } -const AddressUserOps = ({ scrollRef }: Props) => { +const AddressUserOps = ({ scrollRef, shouldRender = true }: Props) => { const router = useRouter(); + const isMounted = useIsMounted(); const hash = getQueryParamString(router.query.hash); @@ -29,6 +32,10 @@ const AddressUserOps = ({ scrollRef }: Props) => { filters: { sender: hash }, }); + if (!isMounted || !shouldRender) { + return null; + } + return ; }; diff --git a/ui/address/AddressWithdrawals.tsx b/ui/address/AddressWithdrawals.tsx index bbc61e8730..ee8f9c4651 100644 --- a/ui/address/AddressWithdrawals.tsx +++ b/ui/address/AddressWithdrawals.tsx @@ -2,6 +2,7 @@ import { Show, Hide } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import React from 'react'; +import useIsMounted from 'lib/hooks/useIsMounted'; import getQueryParamString from 'lib/router/getQueryParamString'; import { generateListStub } from 'stubs/utils'; import { WITHDRAWAL } from 'stubs/withdrawals'; @@ -12,8 +13,13 @@ import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import BeaconChainWithdrawalsListItem from 'ui/withdrawals/beaconChain/BeaconChainWithdrawalsListItem'; import BeaconChainWithdrawalsTable from 'ui/withdrawals/beaconChain/BeaconChainWithdrawalsTable'; -const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject}) => { +type Props = { + scrollRef?: React.RefObject; + shouldRender?: boolean; +} +const AddressWithdrawals = ({ scrollRef, shouldRender = true }: Props) => { const router = useRouter(); + const isMounted = useIsMounted(); const hash = getQueryParamString(router.query.hash); @@ -28,6 +34,11 @@ const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject diff --git a/ui/address/SolidityscanReport.tsx b/ui/address/SolidityscanReport.tsx index d7bbdc9ca2..c3a7ae2669 100644 --- a/ui/address/SolidityscanReport.tsx +++ b/ui/address/SolidityscanReport.tsx @@ -1,72 +1,22 @@ -import { - Box, - Flex, - Text, - Grid, - Button, - chakra, - Popover, - PopoverTrigger, - PopoverBody, - PopoverContent, - useDisclosure, - Skeleton, - Center, - useColorModeValue, -} from '@chakra-ui/react'; +import { Box, Text, chakra, Icon, Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react'; import React from 'react'; -import { SolidityscanReport } 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']; - name: string; - color: string; -} - -const DISTRIBUTION_ITEMS: Array = [ - { id: 'critical', name: 'Critical', color: '#891F11' }, - { id: 'high', name: 'High', color: '#EC672C' }, - { id: 'medium', name: 'Medium', color: '#FBE74D' }, - { id: 'low', name: 'Low', color: '#68C88E' }, - { id: 'informational', name: 'Informational', color: '#A3AEBE' }, - { id: 'gas', name: 'Gas', color: '#A47585' }, -]; +import SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton'; +import SolidityscanReportDetails from 'ui/shared/solidityscanReport/SolidityscanReportDetails'; +import SolidityscanReportScore from 'ui/shared/solidityscanReport/SolidityscanReportScore'; interface Props { className?: string; hash: string; } -type ItemProps = { - item: DistributionItem; - vulnerabilities: SolidityscanReport['scan_report']['scan_summary']['issue_severity_distribution']; - vulnerabilitiesCount: number; -} - -const SolidityScanReportItem = ({ item, vulnerabilities, vulnerabilitiesCount }: ItemProps) => { - const bgBar = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); - const yetAnotherGrayColor = useColorModeValue('gray.400', 'gray.500'); - - return ( - <> - - - { item.name } - 0 ? 'text' : yetAnotherGrayColor }>{ vulnerabilities[item.id] } - - - - - - ); -}; - const SolidityscanReport = ({ className, hash }: Props) => { const { isOpen, onToggle, onClose } = useDisclosure(); @@ -80,31 +30,10 @@ const SolidityscanReport = ({ className, hash }: Props) => { const score = Number(data?.scan_report.scan_summary.score_v2); - const chartGrayColor = useColorModeValue('gray.100', 'gray.700'); - const yetAnotherGrayColor = useColorModeValue('gray.400', 'gray.500'); - const popoverBgColor = useColorModeValue('white', 'gray.900'); - - const greatScoreColor = useColorModeValue('green.600', 'green.400'); - const averageScoreColor = useColorModeValue('purple.600', 'purple.400'); - const lowScoreColor = useColorModeValue('red.600', 'red.400'); - if (isError || !score) { return null; } - let scoreColor; - let scoreLevel; - if (score >= 80) { - scoreColor = greatScoreColor; - scoreLevel = 'GREAT'; - } else if (score >= 30) { - scoreColor = averageScoreColor; - scoreLevel = 'AVERAGE'; - } else { - scoreColor = lowScoreColor; - scoreLevel = 'LOW'; - } - const vulnerabilities = data?.scan_report.scan_summary.issue_severity_distribution; const vulnerabilitiesCounts = vulnerabilities ? Object.values(vulnerabilities) : []; const vulnerabilitiesCount = vulnerabilitiesCounts.reduce((acc, val) => acc + val, 0); @@ -112,57 +41,25 @@ const SolidityscanReport = ({ className, hash }: Props) => { return ( - - - + - Contract analyzed for 140+ vulnerability patterns by SolidityScan - - -
- -
-
- - - { score } - / 100 - - Security score is { scoreLevel } - -
+ + Contract analyzed for 160+ vulnerability patterns by + + SolidityScan + + { vulnerabilities && vulnerabilitiesCount > 0 && ( Vulnerabilities distribution - - { DISTRIBUTION_ITEMS.map(item => ( - - )) } - + ) } View full report diff --git a/ui/address/__screenshots__/AddressDetails.pw.tsx_default_token-1.png b/ui/address/__screenshots__/AddressDetails.pw.tsx_default_token-1.png index 28d70e3dfe..a41b419d9b 100644 Binary files a/ui/address/__screenshots__/AddressDetails.pw.tsx_default_token-1.png and b/ui/address/__screenshots__/AddressDetails.pw.tsx_default_token-1.png differ diff --git a/ui/address/__screenshots__/AddressInternalTxs.pw.tsx_default_base-view-mobile-1.png b/ui/address/__screenshots__/AddressInternalTxs.pw.tsx_default_base-view-mobile-1.png index 4f76f75b60..6923895237 100644 Binary files a/ui/address/__screenshots__/AddressInternalTxs.pw.tsx_default_base-view-mobile-1.png and b/ui/address/__screenshots__/AddressInternalTxs.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/address/__screenshots__/AddressInternalTxs.pw.tsx_mobile_base-view-mobile-1.png b/ui/address/__screenshots__/AddressInternalTxs.pw.tsx_mobile_base-view-mobile-1.png index 46777342c1..b6f13d887f 100644 Binary files a/ui/address/__screenshots__/AddressInternalTxs.pw.tsx_mobile_base-view-mobile-1.png and b/ui/address/__screenshots__/AddressInternalTxs.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-no-pagination-1.png b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-no-pagination-1.png index 9a5fc4ea5d..4543e5121d 100644 Binary files a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-no-pagination-1.png and b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-no-pagination-1.png differ diff --git a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-pagination-1.png b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-pagination-1.png index bd3982620b..1bb1638dda 100644 Binary files a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-pagination-1.png and b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-pagination-1.png differ diff --git a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-no-pagination-1.png b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-no-pagination-1.png index dcdcb70660..e31c1c3b3e 100644 Binary files a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-no-pagination-1.png and b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-no-pagination-1.png differ diff --git a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-pagination-1.png b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-pagination-1.png index 0356e2f168..006ae00ba0 100644 Binary files a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-pagination-1.png and b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-pagination-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_collections-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_collections-dark-mode-1.png index 2cfbdaf3a7..4e2a2f0ded 100644 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_collections-dark-mode-1.png and b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_collections-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc1155-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc1155-dark-mode-1.png new file mode 100644 index 0000000000..c9105e429f Binary files /dev/null and b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc1155-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc20-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc20-dark-mode-1.png index 130d8e0355..a9bf89c441 100644 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc20-dark-mode-1.png and b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc20-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc721-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc721-dark-mode-1.png new file mode 100644 index 0000000000..4eca88970b Binary files /dev/null and b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc721-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_nfts-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_nfts-dark-mode-1.png index ff3f91748f..2b18eaea58 100644 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_nfts-dark-mode-1.png and b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_nfts-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_collections-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_collections-dark-mode-1.png index 185fa05a5c..bf64920931 100644 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_collections-dark-mode-1.png and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_collections-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_erc1155-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_erc1155-dark-mode-1.png new file mode 100644 index 0000000000..eae8460cae Binary files /dev/null and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_erc1155-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_erc20-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_erc20-dark-mode-1.png index ced1b9c977..f570e561c8 100644 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_erc20-dark-mode-1.png and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_erc20-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_erc721-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_erc721-dark-mode-1.png new file mode 100644 index 0000000000..ad6f89f6c1 Binary files /dev/null and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_erc721-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-collections-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-collections-1.png index d2aacbe810..fce88a821b 100644 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-collections-1.png and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-collections-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc1155-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc1155-1.png new file mode 100644 index 0000000000..c083aa4c05 Binary files /dev/null and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc1155-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc20-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc20-1.png index 9325e2a5b4..ac6aa9b71b 100644 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc20-1.png and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc20-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc721-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc721-1.png new file mode 100644 index 0000000000..9af951e2e9 Binary files /dev/null and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc721-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-nfts-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-nfts-1.png index cb01e73aa2..5075d144bd 100644 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-nfts-1.png and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-nfts-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_nfts-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_nfts-dark-mode-1.png index f4ca1946ec..2e9b458cd6 100644 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_nfts-dark-mode-1.png and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_nfts-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-1.png index 41cba95433..928a20abf3 100644 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-1.png and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-2.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-2.png index 0c131b0122..809e53fa3e 100644 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-2.png and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-2.png differ diff --git a/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-mobile-1.png b/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-mobile-1.png index 26e56490ac..77060ddcd5 100644 Binary files a/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-mobile-1.png and b/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-screen-xl-1.png b/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-screen-xl-1.png index 7432110133..40bd723d0d 100644 Binary files a/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-screen-xl-1.png and b/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-screen-xl-1.png differ diff --git a/ui/address/__screenshots__/AddressTxs.pw.tsx_mobile_base-view-mobile-1.png b/ui/address/__screenshots__/AddressTxs.pw.tsx_mobile_base-view-mobile-1.png index 03bc8552cd..4943e339c3 100644 Binary files a/ui/address/__screenshots__/AddressTxs.pw.tsx_mobile_base-view-mobile-1.png and b/ui/address/__screenshots__/AddressTxs.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/address/__screenshots__/SolidityscanReport.pw.tsx_dark-color-mode_average-report-dark-mode-mobile-2.png b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_dark-color-mode_average-report-dark-mode-mobile-2.png index 30011e3179..c25741b0da 100644 Binary files a/ui/address/__screenshots__/SolidityscanReport.pw.tsx_dark-color-mode_average-report-dark-mode-mobile-2.png and b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_dark-color-mode_average-report-dark-mode-mobile-2.png differ diff --git a/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_average-report-dark-mode-mobile-2.png b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_average-report-dark-mode-mobile-2.png index 64a8c4c7d4..fc6141ef12 100644 Binary files a/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_average-report-dark-mode-mobile-2.png and b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_average-report-dark-mode-mobile-2.png differ diff --git a/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_great-report-2.png b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_great-report-2.png index 7f849f73f4..c6b9450434 100644 Binary files a/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_great-report-2.png and b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_great-report-2.png differ diff --git a/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_low-report-2.png b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_low-report-2.png index a357f25316..af84b4459b 100644 Binary files a/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_low-report-2.png and b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_low-report-2.png differ diff --git a/ui/address/__screenshots__/SolidityscanReport.pw.tsx_mobile_average-report-dark-mode-mobile-2.png b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_mobile_average-report-dark-mode-mobile-2.png index 8f4b2b9099..eca7e9027e 100644 Binary files a/ui/address/__screenshots__/SolidityscanReport.pw.tsx_mobile_average-report-dark-mode-mobile-2.png and b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_mobile_average-report-dark-mode-mobile-2.png differ diff --git a/ui/address/accountHistory/AddressAccountHistoryListItem.tsx b/ui/address/accountHistory/AddressAccountHistoryListItem.tsx new file mode 100644 index 0000000000..46cf969932 --- /dev/null +++ b/ui/address/accountHistory/AddressAccountHistoryListItem.tsx @@ -0,0 +1,66 @@ +import { Box, Flex, Skeleton, Text } from '@chakra-ui/react'; +import React, { useMemo } from 'react'; + +import type { NovesResponseData } from 'types/api/noves'; + +import dayjs from 'lib/date/dayjs'; +import IconSvg from 'ui/shared/IconSvg'; +import LinkInternal from 'ui/shared/LinkInternal'; +import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; +import NovesFromTo from 'ui/shared/Noves/NovesFromTo'; + +type Props = { + isPlaceholderData: boolean; + tx: NovesResponseData; + currentAddress: string; +}; + +const AddressAccountHistoryListItem = (props: Props) => { + + const parsedDescription = useMemo(() => { + const description = props.tx.classificationData.description; + + return description.endsWith('.') ? description.substring(0, description.length - 1) : description; + }, [ props.tx.classificationData.description ]); + + return ( + + + + + + + + Action + + + + { dayjs(props.tx.rawTransactionData.timestamp * 1000).fromNow() } + + + + + + { parsedDescription } + + + + + + + + ); +}; + +export default React.memo(AddressAccountHistoryListItem); diff --git a/ui/address/accountHistory/AddressAccountHistoryTableItem.tsx b/ui/address/accountHistory/AddressAccountHistoryTableItem.tsx new file mode 100644 index 0000000000..c3aa61a283 --- /dev/null +++ b/ui/address/accountHistory/AddressAccountHistoryTableItem.tsx @@ -0,0 +1,66 @@ +import { Td, Tr, Skeleton, Text, Box } from '@chakra-ui/react'; +import React, { useMemo } from 'react'; + +import type { NovesResponseData } from 'types/api/noves'; + +import dayjs from 'lib/date/dayjs'; +import IconSvg from 'ui/shared/IconSvg'; +import LinkInternal from 'ui/shared/LinkInternal'; +import NovesFromTo from 'ui/shared/Noves/NovesFromTo'; + +type Props = { + isPlaceholderData: boolean; + tx: NovesResponseData; + currentAddress: string; +}; + +const AddressAccountHistoryTableItem = (props: Props) => { + + const parsedDescription = useMemo(() => { + const description = props.tx.classificationData.description; + + return description.endsWith('.') ? description.substring(0, description.length - 1) : description; + }, [ props.tx.classificationData.description ]); + + return ( + + + + + { dayjs(props.tx.rawTransactionData.timestamp * 1000).fromNow() } + + + + + + + + + + { parsedDescription } + + + + + + + + + + + ); +}; + +export default React.memo(AddressAccountHistoryTableItem); diff --git a/ui/address/coinBalance/AddressCoinBalanceChart.tsx b/ui/address/coinBalance/AddressCoinBalanceChart.tsx index 4e551f1e8b..15003058a4 100644 --- a/ui/address/coinBalance/AddressCoinBalanceChart.tsx +++ b/ui/address/coinBalance/AddressCoinBalanceChart.tsx @@ -15,10 +15,17 @@ const AddressCoinBalanceChart = ({ addressHash }: Props) => { pathParams: { hash: addressHash }, }); - const items = React.useMemo(() => data?.map(({ date, value }) => ({ - date: new Date(date), - value: BigNumber(value).div(10 ** config.chain.currency.decimals).toNumber(), - })), [ data ]); + const items = React.useMemo(() => { + if (!data) { + return undefined; + } + + const dataItems = 'items' in data ? data.items : data; + return dataItems.map(({ date, value }) => ({ + date: new Date(date), + value: BigNumber(value).div(10 ** config.chain.currency.decimals).toNumber(), + })); + }, [ data ]); return ( { isLoading={ isPending } h="300px" units={ currencyUnits.ether } + emptyText={ data && 'days' in data && `Insufficient data for the past ${ data.days } days` } /> ); }; diff --git a/ui/address/contract/ContractMethodsAccordion.tsx b/ui/address/contract/ABI/ContractAbi.tsx similarity index 58% rename from ui/address/contract/ContractMethodsAccordion.tsx rename to ui/address/contract/ABI/ContractAbi.tsx index b1e4c4bf6b..6b71c46c2d 100644 --- a/ui/address/contract/ContractMethodsAccordion.tsx +++ b/ui/address/contract/ABI/ContractAbi.tsx @@ -1,40 +1,27 @@ import { Accordion, Box, Flex, Link } from '@chakra-ui/react'; import _range from 'lodash/range'; import React from 'react'; -import { scroller } from 'react-scroll'; -import type { SmartContractMethod } from 'types/api/contract'; +import type { MethodType, ContractAbi as TContractAbi } from './types'; -import ContractMethodsAccordionItem from './ContractMethodsAccordionItem'; +import ContractAbiItem from './ContractAbiItem'; +import useFormSubmit from './useFormSubmit'; +import useScrollToMethod from './useScrollToMethod'; -interface Props { - data: Array; - addressHash?: string; - renderItemContent: (item: T, index: number, id: number) => React.ReactNode; +interface Props { + data: TContractAbi; + addressHash: string; tab: string; + methodType: MethodType; } -const ContractMethodsAccordion = ({ data, addressHash, renderItemContent, tab }: Props) => { +const ContractAbi = ({ data, addressHash, tab, methodType }: Props) => { const [ expandedSections, setExpandedSections ] = React.useState>(data.length === 1 ? [ 0 ] : []); const [ id, setId ] = React.useState(0); - React.useEffect(() => { - const hash = window.location.hash.replace('#', ''); + useScrollToMethod(data, setExpandedSections); - if (!hash) { - return; - } - - const index = data.findIndex((item) => 'method_id' in item && item.method_id === hash); - if (index > -1) { - scroller.scrollTo(`method_${ hash }`, { - duration: 500, - smooth: true, - offset: -100, - }); - setExpandedSections([ index ]); - } - }, [ data ]); + const handleFormSubmit = useFormSubmit({ addressHash, tab }); const handleAccordionStateChange = React.useCallback((newValue: Array) => { setExpandedSections(newValue); @@ -73,14 +60,15 @@ const ContractMethodsAccordion = ({ data, address
{ data.map((item, index) => ( - React.ReactNode } tab={ tab } + onSubmit={ handleFormSubmit } + methodType={ methodType } /> )) } @@ -88,4 +76,4 @@ const ContractMethodsAccordion = ({ data, address ); }; -export default React.memo(ContractMethodsAccordion) as typeof ContractMethodsAccordion; +export default React.memo(ContractAbi); diff --git a/ui/address/contract/ContractMethodsAccordionItem.tsx b/ui/address/contract/ABI/ContractAbiItem.tsx similarity index 64% rename from ui/address/contract/ContractMethodsAccordionItem.tsx rename to ui/address/contract/ABI/ContractAbiItem.tsx index b30c0d998b..9aa3fc6889 100644 --- a/ui/address/contract/ContractMethodsAccordionItem.tsx +++ b/ui/address/contract/ABI/ContractAbiItem.tsx @@ -1,25 +1,32 @@ -import { AccordionButton, AccordionIcon, AccordionItem, AccordionPanel, Box, Tooltip, useClipboard, useDisclosure } from '@chakra-ui/react'; +import { AccordionButton, AccordionIcon, AccordionItem, AccordionPanel, Alert, Box, Flex, Tooltip, useClipboard, useDisclosure } from '@chakra-ui/react'; import React from 'react'; import { Element } from 'react-scroll'; -import type { SmartContractMethod } from 'types/api/contract'; +import type { FormSubmitHandler, MethodType, ContractAbiItem as TContractAbiItem } from './types'; import { route } from 'nextjs-routes'; import config from 'configs/app'; +import Tag from 'ui/shared/chakra/Tag'; +import CopyToClipboard from 'ui/shared/CopyToClipboard'; import Hint from 'ui/shared/Hint'; import IconSvg from 'ui/shared/IconSvg'; -interface Props { - data: T; +import ContractAbiItemConstant from './ContractAbiItemConstant'; +import ContractMethodForm from './form/ContractMethodForm'; +import { getElementName } from './useScrollToMethod'; + +interface Props { + data: TContractAbiItem; index: number; id: number; - addressHash?: string; - renderContent: (item: T, index: number, id: number) => React.ReactNode; + addressHash: string; tab: string; + onSubmit: FormSubmitHandler; + methodType: MethodType; } -const ContractMethodsAccordionItem = ({ data, index, id, addressHash, renderContent, tab }: Props) => { +const ContractAbiItem = ({ data, index, id, addressHash, tab, onSubmit, methodType }: Props) => { const url = React.useMemo(() => { if (!('method_id' in data)) { return ''; @@ -43,11 +50,40 @@ const ContractMethodsAccordionItem = ({ data, ind onCopy(); }, [ onCopy ]); + const handleCopyMethodIdClick = React.useCallback((event: React.MouseEvent) => { + event.stopPropagation(); + }, []); + + const content = (() => { + if ('error' in data && data.error) { + return { data.error }; + } + + const hasConstantOutputs = 'outputs' in data && data.outputs.some(({ value }) => value !== undefined && value !== null); + + if (hasConstantOutputs) { + return ( + + { data.outputs.map((output, index) => ) } + + ); + } + + return ( + + ); + })(); + return ( { ({ isExpanded }) => ( <> - + { 'method_id' in data && ( @@ -85,11 +121,17 @@ const ContractMethodsAccordionItem = ({ data, ind the contract cannot receive Ether through regular transactions and throws an exception.` }/> ) } + { 'method_id' in data && ( + <> + { data.method_id } + + + ) } - { renderContent(data, index, id) } + { content } ) } @@ -97,4 +139,4 @@ const ContractMethodsAccordionItem = ({ data, ind ); }; -export default React.memo(ContractMethodsAccordionItem); +export default React.memo(ContractAbiItem); diff --git a/ui/address/contract/ContractMethodConstant.tsx b/ui/address/contract/ABI/ContractAbiItemConstant.tsx similarity index 83% rename from ui/address/contract/ContractMethodConstant.tsx rename to ui/address/contract/ABI/ContractAbiItemConstant.tsx index 016668e89b..1e2c3f213c 100644 --- a/ui/address/contract/ContractMethodConstant.tsx +++ b/ui/address/contract/ABI/ContractAbiItemConstant.tsx @@ -4,12 +4,14 @@ import type { ChangeEvent } from 'react'; import React from 'react'; import { getAddress } from 'viem'; -import type { SmartContractMethodOutput } from 'types/api/contract'; +import type { ContractAbiItemOutput } from './types'; import { WEI } from 'lib/consts'; import { currencyUnits } from 'lib/units'; import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import { matchInt } from './form/utils'; + function castValueToString(value: number | string | boolean | object | bigint | undefined): string { switch (typeof value) { case 'string': @@ -28,13 +30,15 @@ function castValueToString(value: number | string | boolean | object | bigint | } interface Props { - data: SmartContractMethodOutput; + data: ContractAbiItemOutput; } -const ContractMethodStatic = ({ data }: Props) => { +const ContractAbiItemConstant = ({ data }: Props) => { const [ value, setValue ] = React.useState(castValueToString(data.value)); const [ label, setLabel ] = React.useState(currencyUnits.wei.toUpperCase()); + const intMatch = matchInt(data.type); + const handleCheckboxChange = React.useCallback((event: ChangeEvent) => { const initialValue = castValueToString(data.value); @@ -63,9 +67,9 @@ const ContractMethodStatic = ({ data }: Props) => { return ( { content } - { (data.type.includes('int256') || data.type.includes('int128')) && { label } } + { Number(intMatch?.power) >= 128 && { label } } ); }; -export default ContractMethodStatic; +export default ContractAbiItemConstant; diff --git a/ui/address/contract/methodForm/ContractMethodArrayButton.tsx b/ui/address/contract/ABI/form/ContractMethodArrayButton.tsx similarity index 100% rename from ui/address/contract/methodForm/ContractMethodArrayButton.tsx rename to ui/address/contract/ABI/form/ContractMethodArrayButton.tsx diff --git a/ui/address/contract/methodForm/ContractMethodFieldAccordion.tsx b/ui/address/contract/ABI/form/ContractMethodFieldAccordion.tsx similarity index 100% rename from ui/address/contract/methodForm/ContractMethodFieldAccordion.tsx rename to ui/address/contract/ABI/form/ContractMethodFieldAccordion.tsx diff --git a/ui/address/contract/methodForm/ContractMethodFieldInput.tsx b/ui/address/contract/ABI/form/ContractMethodFieldInput.tsx similarity index 76% rename from ui/address/contract/methodForm/ContractMethodFieldInput.tsx rename to ui/address/contract/ABI/form/ContractMethodFieldInput.tsx index be6ac43ed9..cbe244b210 100644 --- a/ui/address/contract/methodForm/ContractMethodFieldInput.tsx +++ b/ui/address/contract/ABI/form/ContractMethodFieldInput.tsx @@ -3,17 +3,18 @@ import React from 'react'; import { useController, useFormContext } from 'react-hook-form'; import { NumericFormat } from 'react-number-format'; -import type { SmartContractMethodInput } from 'types/api/contract'; +import type { ContractAbiItemInput } from '../types'; import ClearButton from 'ui/shared/ClearButton'; import ContractMethodFieldLabel from './ContractMethodFieldLabel'; import ContractMethodMultiplyButton from './ContractMethodMultiplyButton'; -import useArgTypeMatchInt from './useArgTypeMatchInt'; +import useFormatFieldValue from './useFormatFieldValue'; import useValidateField from './useValidateField'; +import { matchInt } from './utils'; interface Props { - data: SmartContractMethodInput; + data: ContractAbiItemInput; hideLabel?: boolean; path: string; className?: string; @@ -27,17 +28,24 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi const isNativeCoin = data.fieldType === 'native_coin'; const isOptional = isNativeCoin; - const argTypeMatchInt = useArgTypeMatchInt({ argType: data.type }); + const argTypeMatchInt = React.useMemo(() => matchInt(data.type), [ data.type ]); const validate = useValidateField({ isOptional, argType: data.type, argTypeMatchInt }); + const format = useFormatFieldValue({ argType: data.type, argTypeMatchInt }); const { control, setValue, getValues } = useFormContext(); - const { field, fieldState } = useController({ control, name, rules: { validate, required: isOptional ? false : 'Field is required' } }); + const { field, fieldState } = useController({ control, name, rules: { validate } }); const inputBgColor = useColorModeValue('white', 'black'); const nativeCoinRowBgColor = useColorModeValue('gray.100', 'gray.700'); const hasMultiplyButton = argTypeMatchInt && Number(argTypeMatchInt.power) >= 64; + const handleChange = React.useCallback((event: React.ChangeEvent) => { + const formattedValue = format(event.target.value); + field.onChange(formattedValue); // data send back to hook form + setValue(name, formattedValue); // UI state + }, [ field, name, setValue, format ]); + const handleClear = React.useCallback(() => { setValue(name, ''); ref.current?.focus(); @@ -46,9 +54,9 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi const handleMultiplyButtonClick = React.useCallback((power: number) => { const zeroes = Array(power).fill('0').join(''); const value = getValues(name); - const newValue = value ? value + zeroes : '1' + zeroes; + const newValue = format(value ? value + zeroes : '1' + zeroes); setValue(name, newValue); - }, [ getValues, name, setValue ]); + }, [ format, getValues, name, setValue ]); const error = fieldState.error; @@ -76,6 +84,7 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi allowNegative: !argTypeMatchInt.isUnsigned, } : {}) } ref={ ref } + onChange={ handleChange } required={ !isOptional } isInvalid={ Boolean(error) } placeholder={ data.type } @@ -84,7 +93,7 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi paddingRight={ hasMultiplyButton ? '120px' : '40px' } /> - { typeof field.value === 'string' && field.value.replace('\n', '') && } + { field.value !== undefined && field.value !== '' && } { hasMultiplyButton && } diff --git a/ui/address/contract/methodForm/ContractMethodFieldInputArray.tsx b/ui/address/contract/ABI/form/ContractMethodFieldInputArray.tsx similarity index 55% rename from ui/address/contract/methodForm/ContractMethodFieldInputArray.tsx rename to ui/address/contract/ABI/form/ContractMethodFieldInputArray.tsx index 85a2611e90..add681e90b 100644 --- a/ui/address/contract/methodForm/ContractMethodFieldInputArray.tsx +++ b/ui/address/contract/ABI/form/ContractMethodFieldInputArray.tsx @@ -2,7 +2,7 @@ import { Flex } from '@chakra-ui/react'; import React from 'react'; import { useFormContext } from 'react-hook-form'; -import type { SmartContractMethodInput, SmartContractMethodArgType } from 'types/api/contract'; +import type { ContractAbiItemInput } from '../types'; import ContractMethodArrayButton from './ContractMethodArrayButton'; import type { Props as AccordionProps } from './ContractMethodFieldAccordion'; @@ -10,21 +10,35 @@ import ContractMethodFieldAccordion from './ContractMethodFieldAccordion'; import ContractMethodFieldInput from './ContractMethodFieldInput'; import ContractMethodFieldInputTuple from './ContractMethodFieldInputTuple'; import ContractMethodFieldLabel from './ContractMethodFieldLabel'; -import { getFieldLabel } from './utils'; +import { getFieldLabel, matchArray, transformDataForArrayItem } from './utils'; interface Props extends Pick { - data: SmartContractMethodInput; + data: ContractAbiItemInput; level: number; basePath: string; isDisabled: boolean; + isArrayElement?: boolean; + size?: number; } -const ContractMethodFieldInputArray = ({ data, level, basePath, onAddClick, onRemoveClick, index: parentIndex, isDisabled }: Props) => { +const ContractMethodFieldInputArray = ({ + data, + level, + basePath, + onAddClick, + onRemoveClick, + index: parentIndex, + isDisabled, + isArrayElement, +}: Props) => { const { formState: { errors } } = useFormContext(); const fieldsWithErrors = Object.keys(errors); const isInvalid = fieldsWithErrors.some((field) => field.startsWith(basePath)); - const [ registeredIndices, setRegisteredIndices ] = React.useState([ 0 ]); + const arrayMatch = matchArray(data.type); + const hasFixedSize = arrayMatch !== null && arrayMatch.size !== Infinity; + + const [ registeredIndices, setRegisteredIndices ] = React.useState(hasFixedSize ? Array(arrayMatch.size).fill(0).map((_, i) => i) : [ 0 ]); const handleAddButtonClick = React.useCallback((event: React.MouseEvent) => { event.preventDefault(); @@ -39,52 +53,69 @@ const ContractMethodFieldInputArray = ({ data, level, basePath, onAddClick, onRe } }, [ ]); - const getItemData = (index: number) => { - const childrenType = data.type.slice(0, -2) as SmartContractMethodArgType; - const childrenInternalType = data.internalType?.slice(0, parentIndex !== undefined ? -4 : -2).replaceAll('struct ', ''); - - const namePostfix = childrenInternalType ? ' ' + childrenInternalType : ''; - const nameParentIndex = parentIndex !== undefined ? `${ parentIndex + 1 }.` : ''; - const nameIndex = index + 1; + if (arrayMatch?.isNested) { + return ( + <> + { + registeredIndices.map((registeredIndex, index) => { + const itemData = transformDataForArrayItem(data, index); + const itemBasePath = `${ basePath }:${ registeredIndex }`; + const itemIsInvalid = fieldsWithErrors.some((field) => field.startsWith(itemBasePath)); + + return ( + 1 ? handleRemoveButtonClick : undefined } + index={ registeredIndex } + > + + + ); + }) + } + + ); + } - return { - ...data, - type: childrenType, - name: `#${ nameParentIndex + nameIndex }${ namePostfix }`, - }; - }; - const isNestedArray = data.type.includes('[][]'); + const isTupleArray = arrayMatch?.itemType.includes('tuple'); - if (isNestedArray) { - return ( - + if (isTupleArray) { + const content = ( + <> { registeredIndices.map((registeredIndex, index) => { - const itemData = getItemData(index); + const itemData = transformDataForArrayItem(data, index); return ( - 1 ? handleRemoveButtonClick : undefined } + onAddClick={ !hasFixedSize && index === registeredIndices.length - 1 ? handleAddButtonClick : undefined } + onRemoveClick={ !hasFixedSize && registeredIndices.length > 1 ? handleRemoveButtonClick : undefined } index={ registeredIndex } isDisabled={ isDisabled } /> ); }) } - + ); - } - const isTupleArray = data.type.includes('tuple'); + if (isArrayElement) { + return content; + } - if (isTupleArray) { return ( - { registeredIndices.map((registeredIndex, index) => { - const itemData = getItemData(index); - - return ( - 1 ? handleRemoveButtonClick : undefined } - index={ registeredIndex } - isDisabled={ isDisabled } - /> - ); - }) } + { content } ); } @@ -117,10 +133,10 @@ const ContractMethodFieldInputArray = ({ data, level, basePath, onAddClick, onRe // primitive value array return ( - + { !isArrayElement && } { registeredIndices.map((registeredIndex, index) => { - const itemData = getItemData(index); + const itemData = transformDataForArrayItem(data, index); return ( @@ -132,9 +148,9 @@ const ContractMethodFieldInputArray = ({ data, level, basePath, onAddClick, onRe px={ 0 } isDisabled={ isDisabled } /> - { registeredIndices.length > 1 && + { !hasFixedSize && registeredIndices.length > 1 && } - { index === registeredIndices.length - 1 && + { !hasFixedSize && index === registeredIndices.length - 1 && } ); diff --git a/ui/address/contract/methodForm/ContractMethodFieldInputTuple.tsx b/ui/address/contract/ABI/form/ContractMethodFieldInputTuple.tsx similarity index 83% rename from ui/address/contract/methodForm/ContractMethodFieldInputTuple.tsx rename to ui/address/contract/ABI/form/ContractMethodFieldInputTuple.tsx index 79adfb0d8a..8c0426e473 100644 --- a/ui/address/contract/methodForm/ContractMethodFieldInputTuple.tsx +++ b/ui/address/contract/ABI/form/ContractMethodFieldInputTuple.tsx @@ -1,16 +1,16 @@ import React from 'react'; import { useFormContext } from 'react-hook-form'; -import type { SmartContractMethodInput } from 'types/api/contract'; +import type { ContractAbiItemInput } from '../types'; import type { Props as AccordionProps } from './ContractMethodFieldAccordion'; import ContractMethodFieldAccordion from './ContractMethodFieldAccordion'; import ContractMethodFieldInput from './ContractMethodFieldInput'; import ContractMethodFieldInputArray from './ContractMethodFieldInputArray'; -import { ARRAY_REGEXP, getFieldLabel } from './utils'; +import { getFieldLabel, matchArray } from './utils'; interface Props extends Pick { - data: SmartContractMethodInput; + data: ContractAbiItemInput; basePath: string; level: number; isDisabled: boolean; @@ -21,6 +21,10 @@ const ContractMethodFieldInputTuple = ({ data, basePath, level, isDisabled, ...a const fieldsWithErrors = Object.keys(errors); const isInvalid = fieldsWithErrors.some((field) => field.startsWith(basePath)); + if (!('components' in data)) { + return null; + } + return ( { data.components?.map((component, index) => { - if (component.components && component.type === 'tuple') { + if ('components' in component && component.type === 'tuple') { return ( ); diff --git a/ui/address/contract/methodForm/ContractMethodFieldLabel.tsx b/ui/address/contract/ABI/form/ContractMethodFieldLabel.tsx similarity index 86% rename from ui/address/contract/methodForm/ContractMethodFieldLabel.tsx rename to ui/address/contract/ABI/form/ContractMethodFieldLabel.tsx index ea2b4be02c..305738fe12 100644 --- a/ui/address/contract/methodForm/ContractMethodFieldLabel.tsx +++ b/ui/address/contract/ABI/form/ContractMethodFieldLabel.tsx @@ -1,12 +1,12 @@ import { Box, useColorModeValue } from '@chakra-ui/react'; import React from 'react'; -import type { SmartContractMethodInput } from 'types/api/contract'; +import type { ContractAbiItemInput } from '../types'; import { getFieldLabel } from './utils'; interface Props { - data: SmartContractMethodInput; + data: ContractAbiItemInput; isOptional?: boolean; level: number; } diff --git a/ui/address/contract/methodForm/ContractMethodForm.pw.tsx b/ui/address/contract/ABI/form/ContractMethodForm.pw.tsx similarity index 85% rename from ui/address/contract/methodForm/ContractMethodForm.pw.tsx rename to ui/address/contract/ABI/form/ContractMethodForm.pw.tsx index 2fd5fbb589..8438c04d4d 100644 --- a/ui/address/contract/methodForm/ContractMethodForm.pw.tsx +++ b/ui/address/contract/ABI/form/ContractMethodForm.pw.tsx @@ -1,16 +1,15 @@ import { test, expect } from '@playwright/experimental-ct-react'; import React from 'react'; -import type { SmartContractWriteMethod } from 'types/api/contract'; +import type { ContractAbiItem } from '../types'; import TestApp from 'playwright/TestApp'; import ContractMethodForm from './ContractMethodForm'; -const onSubmit = () => Promise.resolve({ hash: '0x0000' as `0x${ string }` }); -const resultComponent = () => null; +const onSubmit = () => Promise.resolve({ source: 'wallet_client' as const, result: { hash: '0x0000' as `0x${ string }` } }); -const data: SmartContractWriteMethod = { +const data: ContractAbiItem = { inputs: [ // TUPLE { @@ -53,6 +52,13 @@ const data: SmartContractWriteMethod = { type: 'tuple[][]', }, + // TOP LEVEL NESTED ARRAY + { + internalType: 'int256[2][][3]', + name: 'ParentArray', + type: 'int256[2][][3]', + }, + // LITERALS { internalType: 'bytes32', name: 'fulfillerConduitKey', type: 'bytes32' }, { internalType: 'address', name: 'recipient', type: 'address' }, @@ -95,10 +101,9 @@ test('base view +@mobile +@dark-mode', async({ mount }) => { const component = await mount( - + , @@ -125,9 +130,13 @@ test('base view +@mobile +@dark-mode', async({ mount }) => { await component.getByText('struct FulfillmentComponent[][]').click(); await component.getByRole('button', { name: 'add' }).nth(1).click(); await component.getByText('#1 FulfillmentComponent[]').click(); - await component.getByText('#1.1 FulfillmentComponent').click(); + await component.getByLabel('#1 FulfillmentComponent[] (tuple[])').getByText('#1 FulfillmentComponent (tuple)').click(); await component.getByRole('button', { name: 'add' }).nth(1).click(); + await component.getByText('ParentArray (int256[2][][3])').click(); + await component.getByText('#1 int256[2][] (int256[2][])').click(); + await component.getByLabel('#1 int256[2][] (int256[2][])').getByText('#1 int256[2] (int256[2])').click(); + // submit form await component.getByRole('button', { name: 'Write' }).click(); diff --git a/ui/address/contract/ABI/form/ContractMethodForm.tsx b/ui/address/contract/ABI/form/ContractMethodForm.tsx new file mode 100644 index 0000000000..cab71b2320 --- /dev/null +++ b/ui/address/contract/ABI/form/ContractMethodForm.tsx @@ -0,0 +1,208 @@ +import { Box, Button, Flex, Tooltip, chakra } from '@chakra-ui/react'; +import _mapValues from 'lodash/mapValues'; +import React from 'react'; +import type { SubmitHandler } from 'react-hook-form'; +import { useForm, FormProvider } from 'react-hook-form'; +import type { AbiFunction } from 'viem'; + +import type { FormSubmitHandler, FormSubmitResult, MethodCallStrategy, MethodType, ContractAbiItem } from '../types'; + +import config from 'configs/app'; +import * as mixpanel from 'lib/mixpanel/index'; + +import ContractMethodFieldAccordion from './ContractMethodFieldAccordion'; +import ContractMethodFieldInput from './ContractMethodFieldInput'; +import ContractMethodFieldInputArray from './ContractMethodFieldInputArray'; +import ContractMethodFieldInputTuple from './ContractMethodFieldInputTuple'; +import ContractMethodOutputs from './ContractMethodOutputs'; +import ContractMethodResult from './ContractMethodResult'; +import { getFieldLabel, matchArray, transformFormDataToMethodArgs } from './utils'; +import type { ContractMethodFormFields } from './utils'; + +interface Props { + data: ContractAbiItem; + onSubmit: FormSubmitHandler; + methodType: MethodType; +} + +const ContractMethodForm = ({ data, onSubmit, methodType }: Props) => { + + const [ result, setResult ] = React.useState(); + const [ isLoading, setLoading ] = React.useState(false); + const [ callStrategy, setCallStrategy ] = React.useState(); + const callStrategyRef = React.useRef(callStrategy); + + const formApi = useForm({ + mode: 'all', + shouldUnregister: true, + }); + + const handleButtonClick = React.useCallback((event: React.MouseEvent) => { + const callStrategy = event?.currentTarget.getAttribute('data-call-strategy'); + setCallStrategy(callStrategy as MethodCallStrategy); + callStrategyRef.current = callStrategy as MethodCallStrategy; + }, []); + + const onFormSubmit: SubmitHandler = React.useCallback(async(formData) => { + // The API used for reading from contracts expects all values to be strings. + const formattedData = callStrategyRef.current === 'api' ? + _mapValues(formData, (value) => value !== undefined ? String(value) : undefined) : + formData; + const args = transformFormDataToMethodArgs(formattedData); + + setResult(undefined); + setLoading(true); + + onSubmit(data, args, callStrategyRef.current) + .then((result) => { + setResult(result); + }) + .catch((error) => { + setResult({ + source: callStrategyRef.current ?? 'wallet_client', + result: error?.error || error?.data || (error?.reason && { message: error.reason }) || error, + }); + setLoading(false); + }) + .finally(() => { + mixpanel.logEvent(mixpanel.EventTypes.CONTRACT_INTERACTION, { + 'Method type': methodType === 'write' ? 'Write' : 'Read', + 'Method name': 'name' in data ? data.name : 'Fallback', + }); + }); + }, [ data, methodType, onSubmit ]); + + const handleTxSettle = React.useCallback(() => { + setLoading(false); + }, []); + + const handleFormChange = React.useCallback(() => { + result && setResult(undefined); + }, [ result ]); + + const inputs: AbiFunction['inputs'] = React.useMemo(() => { + return [ + ...('inputs' in data && data.inputs ? 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 outputs = 'outputs' in data && data.outputs ? data.outputs : []; + + const callStrategies = (() => { + switch (methodType) { + case 'read': { + return { primary: 'api', secondary: undefined }; + } + + case 'write': { + return { + primary: config.features.blockchainInteraction.isEnabled ? 'wallet_client' : undefined, + secondary: 'outputs' in data && Boolean(data.outputs?.length) ? 'api' : undefined, + }; + } + + default: { + return { primary: undefined, secondary: undefined }; + } + } + })(); + + // eslint-disable-next-line max-len + const noWalletClientText = 'Blockchain interaction is not available at the moment since WalletConnect is not configured for this application. Please contact the service maintainer to make necessary changes in the service configuration.'; + + return ( + + + + + { inputs.map((input, index) => { + const props = { + data: input, + basePath: `${ index }`, + isDisabled: isLoading, + level: 0, + }; + + if ('components' in input && input.components && input.type === 'tuple') { + return ; + } + + const arrayMatch = matchArray(input.type); + if (arrayMatch) { + if (arrayMatch.isNested) { + const fieldsWithErrors = Object.keys(formApi.formState.errors); + const isInvalid = fieldsWithErrors.some((field) => field.startsWith(index + ':')); + + return ( + + + + ); + + } + + return ; + } + + return ; + }) } + + { callStrategies.secondary && ( + + ) } + + + + + + { 'outputs' in data && Boolean(data.outputs?.length) && } + { result && } + + ); +}; + +export default React.memo(ContractMethodForm) as typeof ContractMethodForm; diff --git a/ui/address/contract/methodForm/ContractMethodMultiplyButton.tsx b/ui/address/contract/ABI/form/ContractMethodMultiplyButton.tsx similarity index 100% rename from ui/address/contract/methodForm/ContractMethodMultiplyButton.tsx rename to ui/address/contract/ABI/form/ContractMethodMultiplyButton.tsx diff --git a/ui/address/contract/methodForm/ContractMethodFormOutputs.tsx b/ui/address/contract/ABI/form/ContractMethodOutputs.tsx similarity index 71% rename from ui/address/contract/methodForm/ContractMethodFormOutputs.tsx rename to ui/address/contract/ABI/form/ContractMethodOutputs.tsx index 17797bea0d..f145cd3f20 100644 --- a/ui/address/contract/methodForm/ContractMethodFormOutputs.tsx +++ b/ui/address/contract/ABI/form/ContractMethodOutputs.tsx @@ -1,15 +1,14 @@ import { Flex, chakra } from '@chakra-ui/react'; import React from 'react'; - -import type { SmartContractMethodOutput } from 'types/api/contract'; +import type { AbiFunction } from 'viem'; import IconSvg from 'ui/shared/IconSvg'; interface Props { - data: Array; + data: AbiFunction['outputs']; } -const ContractMethodFormOutputs = ({ data }: Props) => { +const ContractMethodOutputs = ({ data }: Props) => { if (data.length === 0) { return null; } @@ -20,11 +19,11 @@ const ContractMethodFormOutputs = ({ data }: Props) => {

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

@@ -32,4 +31,4 @@ const ContractMethodFormOutputs = ({ data }: Props) => { ); }; -export default React.memo(ContractMethodFormOutputs); +export default React.memo(ContractMethodOutputs); diff --git a/ui/address/contract/ABI/form/ContractMethodResult.tsx b/ui/address/contract/ABI/form/ContractMethodResult.tsx new file mode 100644 index 0000000000..654d7236c3 --- /dev/null +++ b/ui/address/contract/ABI/form/ContractMethodResult.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +import type { FormSubmitResult, ContractAbiItem } from '../types'; + +import ContractMethodResultApi from './ContractMethodResultApi'; +import ContractMethodResultWalletClient from './ContractMethodResultWalletClient'; + +interface Props { + abiItem: ContractAbiItem; + result: FormSubmitResult; + onSettle: () => void; +} + +const ContractMethodResult = ({ result, abiItem, onSettle }: Props) => { + + switch (result.source) { + case 'api': + return ; + + case 'wallet_client': + return ; + + default: { + return null; + } + } +}; + +export default React.memo(ContractMethodResult); diff --git a/ui/address/contract/ABI/form/ContractMethodResultApi.tsx b/ui/address/contract/ABI/form/ContractMethodResultApi.tsx new file mode 100644 index 0000000000..ba8e5fd928 --- /dev/null +++ b/ui/address/contract/ABI/form/ContractMethodResultApi.tsx @@ -0,0 +1,64 @@ +import { Box, chakra, useColorModeValue } from '@chakra-ui/react'; +import React from 'react'; + +import type { ContractAbiItem, FormSubmitResultApi } from '../types'; + +import hexToUtf8 from 'lib/hexToUtf8'; + +import ContractMethodResultApiError from './ContractMethodResultApiError'; +import ContractMethodResultApiItem from './ContractMethodResultApiItem'; + +interface Props { + item: ContractAbiItem; + result: FormSubmitResultApi['result']; + onSettle: () => void; +} + +const ContractMethodResultApi = ({ item, result, onSettle }: Props) => { + const resultBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); + + React.useEffect(() => { + onSettle(); + }, [ onSettle ]); + + if ('status' in result) { + return { result.statusText }; + } + + if (result instanceof Error) { + return { result.message }; + } + + if (result.is_error) { + if ('error' in result.result) { + return { result.result.error }; + } + + if ('message' in result.result) { + return [{ result.result.code }] { result.result.message }; + } + + if ('raw' in result.result) { + return { `Revert reason: ${ hexToUtf8(result.result.raw) }` }; + } + + if ('method_id' in result.result) { + return { JSON.stringify(result.result, undefined, 2) }; + } + + return Something went wrong.; + } + + return ( + +

+ [ { 'name' in item ? item.name : '' } method response ] +

+

[

+ { result.result.output.map((output, index) => ) } +

]

+
+ ); +}; + +export default React.memo(ContractMethodResultApi); diff --git a/ui/address/contract/ABI/form/ContractMethodResultApiError.tsx b/ui/address/contract/ABI/form/ContractMethodResultApiError.tsx new file mode 100644 index 0000000000..bbcfacf407 --- /dev/null +++ b/ui/address/contract/ABI/form/ContractMethodResultApiError.tsx @@ -0,0 +1,16 @@ +import { Alert } from '@chakra-ui/react'; +import React from 'react'; + +interface Props { + children: React.ReactNode; +} + +const ContractMethodResultApiError = ({ children }: Props) => { + return ( + + { children } + + ); +}; + +export default React.memo(ContractMethodResultApiError); diff --git a/ui/address/contract/ABI/form/ContractMethodResultApiItem.tsx b/ui/address/contract/ABI/form/ContractMethodResultApiItem.tsx new file mode 100644 index 0000000000..e1d0bacaf6 --- /dev/null +++ b/ui/address/contract/ABI/form/ContractMethodResultApiItem.tsx @@ -0,0 +1,45 @@ +import { chakra } from '@chakra-ui/react'; +import React from 'react'; + +import type { SmartContractQueryMethodSuccess } from 'types/api/contract'; + +const TUPLE_TYPE_REGEX = /\[(.+)\]/; + +interface Props { + output: SmartContractQueryMethodSuccess['result']['output'][0]; + name: SmartContractQueryMethodSuccess['result']['names'][0]; +} + +const ContractMethodResultApiItem = ({ output, name }: Props) => { + if (Array.isArray(name)) { + const [ structName, argNames ] = name; + const argTypes = output.type.match(TUPLE_TYPE_REGEX)?.[1].split(','); + + return ( + <> +

+ { structName } + ({ output.type }) : +

+ { argNames.map((argName, argIndex) => { + return ( +

+ { argName } + { argTypes?.[argIndex] ? ` (${ argTypes[argIndex] })` : '' } : { String(output.value[argIndex]) } +

+ ); + }) } + + ); + } + + return ( +

+ + { name && { name } } + ({ output.type }) : { String(output.value) } +

+ ); +}; + +export default React.memo(ContractMethodResultApiItem); diff --git a/ui/address/contract/ContractWriteResultDumb.tsx b/ui/address/contract/ABI/form/ContractMethodResultWalletClient.tsx similarity index 62% rename from ui/address/contract/ContractWriteResultDumb.tsx rename to ui/address/contract/ABI/form/ContractMethodResultWalletClient.tsx index 6e734f93a1..371b914205 100644 --- a/ui/address/contract/ContractWriteResultDumb.tsx +++ b/ui/address/contract/ABI/form/ContractMethodResultWalletClient.tsx @@ -1,26 +1,39 @@ -import { Box, chakra, Spinner } from '@chakra-ui/react'; +import { chakra, Spinner, Box } from '@chakra-ui/react'; import React from 'react'; +import type { UseWaitForTransactionReceiptReturnType } from 'wagmi'; +import { useWaitForTransactionReceipt } from 'wagmi'; -import type { ContractMethodWriteResult } from './types'; +import type { FormSubmitResultWalletClient } from '../types'; import { route } from 'nextjs-routes'; import LinkInternal from 'ui/shared/LinkInternal'; interface Props { - result: ContractMethodWriteResult; + result: FormSubmitResultWalletClient['result']; onSettle: () => void; - txInfo: { - status: 'loading' | 'success' | 'error' | 'idle'; - error: Error | null; - }; } -const ContractWriteResultDumb = ({ result, onSettle, txInfo }: Props) => { +const ContractMethodResultWalletClient = ({ result, onSettle }: Props) => { + const txHash = result && 'hash' in result ? result.hash as `0x${ string }` : undefined; + const txInfo = useWaitForTransactionReceipt({ + hash: txHash, + }); + + return ; +}; + +export interface PropsDumb { + result: FormSubmitResultWalletClient['result']; + onSettle: () => void; + txInfo: UseWaitForTransactionReceiptReturnType; +} + +export const ContractMethodResultWalletClientDumb = ({ result, onSettle, txInfo }: PropsDumb) => { const txHash = result && 'hash' in result ? result.hash : undefined; React.useEffect(() => { - if (txInfo.status !== 'loading') { + if (txInfo.status !== 'pending') { onSettle(); } }, [ onSettle, txInfo.status ]); @@ -55,7 +68,7 @@ const ContractWriteResultDumb = ({ result, onSettle, txInfo }: Props) => { ); } - case 'loading': { + case 'pending': { return ( <> @@ -93,4 +106,4 @@ const ContractWriteResultDumb = ({ result, onSettle, txInfo }: Props) => { ); }; -export default React.memo(ContractWriteResultDumb); +export default React.memo(ContractMethodResultWalletClient); diff --git a/ui/address/contract/ABI/form/__screenshots__/ContractMethodForm.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodForm.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..85a1ffa319 Binary files /dev/null and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodForm.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png differ diff --git a/ui/address/contract/ABI/form/__screenshots__/ContractMethodForm.pw.tsx_default_base-view-mobile-dark-mode-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodForm.pw.tsx_default_base-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..89230b3a16 Binary files /dev/null and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodForm.pw.tsx_default_base-view-mobile-dark-mode-1.png differ diff --git a/ui/address/contract/ABI/form/__screenshots__/ContractMethodForm.pw.tsx_mobile_base-view-mobile-dark-mode-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodForm.pw.tsx_mobile_base-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..8aba610638 Binary files /dev/null and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodForm.pw.tsx_mobile_base-view-mobile-dark-mode-1.png differ diff --git a/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_complex-error-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_complex-error-1.png new file mode 100644 index 0000000000..b96ceac2e2 Binary files /dev/null and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_complex-error-1.png differ diff --git a/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_complex-success-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_complex-success-1.png new file mode 100644 index 0000000000..d5902ddf0e Binary files /dev/null and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_complex-success-1.png differ diff --git a/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_default-error-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_default-error-1.png new file mode 100644 index 0000000000..cd23d7452d Binary files /dev/null and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_default-error-1.png differ diff --git a/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_error-with-code-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_error-with-code-1.png new file mode 100644 index 0000000000..e5d2fcf25c Binary files /dev/null and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_error-with-code-1.png differ diff --git a/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_raw-error-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_raw-error-1.png new file mode 100644 index 0000000000..241bb2f7e3 Binary files /dev/null and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_raw-error-1.png differ diff --git a/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_success-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_success-1.png new file mode 100644 index 0000000000..ab0f9ab157 Binary files /dev/null and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_success-1.png differ diff --git a/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_error-in-result-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_error-in-result-1.png new file mode 100644 index 0000000000..0d50122e57 Binary files /dev/null and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_error-in-result-1.png differ diff --git a/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_error-mobile-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_error-mobile-1.png new file mode 100644 index 0000000000..79e30aeaa0 Binary files /dev/null and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_error-mobile-1.png differ diff --git a/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_loading-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_loading-1.png new file mode 100644 index 0000000000..d80cc80e2f Binary files /dev/null and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_loading-1.png differ diff --git a/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_success-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_success-1.png new file mode 100644 index 0000000000..4bd45c7a0b Binary files /dev/null and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_success-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_mobile_error-mobile-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_mobile_error-mobile-1.png similarity index 100% rename from ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_mobile_error-mobile-1.png rename to ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_mobile_error-mobile-1.png diff --git a/ui/address/contract/ABI/form/useFormatFieldValue.tsx b/ui/address/contract/ABI/form/useFormatFieldValue.tsx new file mode 100644 index 0000000000..70bd8270fe --- /dev/null +++ b/ui/address/contract/ABI/form/useFormatFieldValue.tsx @@ -0,0 +1,44 @@ +import React from 'react'; + +import type { MatchInt } from './utils'; + +interface Params { + argType: string; + argTypeMatchInt: MatchInt | null; +} + +export default function useFormatFieldValue({ argType, argTypeMatchInt }: Params) { + + return React.useCallback((value: string | undefined) => { + if (!value) { + return; + } + + if (argTypeMatchInt) { + // we have to store all numbers as strings to avoid precision loss + // and we cannot store them as BigInt because the NumberFormat component will not work properly + // so we just remove all white spaces here otherwise the `viem` library will throw an error on attempt to write value to a contract + const formattedString = value.replace(/\s/g, ''); + return formattedString; + } + + if (argType === 'bool') { + const formattedValue = value.toLowerCase(); + + switch (formattedValue) { + case 'true': { + return true; + } + + case 'false':{ + return false; + } + + default: + return value; + } + } + + return value; + }, [ argType, argTypeMatchInt ]); +} diff --git a/ui/address/contract/methodForm/useValidateField.tsx b/ui/address/contract/ABI/form/useValidateField.tsx similarity index 69% rename from ui/address/contract/methodForm/useValidateField.tsx rename to ui/address/contract/ABI/form/useValidateField.tsx index 482ad73195..de9d506f31 100644 --- a/ui/address/contract/methodForm/useValidateField.tsx +++ b/ui/address/contract/ABI/form/useValidateField.tsx @@ -1,13 +1,11 @@ import React from 'react'; import { getAddress, isAddress, isHex } from 'viem'; -import type { SmartContractMethodArgType } from 'types/api/contract'; - -import type { MatchInt } from './useArgTypeMatchInt'; -import { BYTES_REGEXP, formatBooleanValue } from './utils'; +import type { MatchInt } from './utils'; +import { BYTES_REGEXP } from './utils'; interface Params { - argType: SmartContractMethodArgType; + argType: string; argTypeMatchInt: MatchInt | null; isOptional: boolean; } @@ -18,13 +16,15 @@ export default function useValidateField({ isOptional, argType, argTypeMatchInt return argType.match(BYTES_REGEXP); }, [ argType ]); - return React.useCallback((value: string | undefined) => { - if (!value) { + // some values are formatted before they are sent to the validator + // see ./useFormatFieldValue.tsx hook + return React.useCallback((value: string | boolean | undefined) => { + if (value === undefined || value === '') { return isOptional ? true : 'Field is required'; } if (argType === 'address') { - if (!isAddress(value)) { + if (typeof value !== 'string' || !isAddress(value)) { return 'Invalid address format'; } @@ -39,13 +39,19 @@ export default function useValidateField({ isOptional, argType, argTypeMatchInt } if (argTypeMatchInt) { - const formattedValue = Number(value.replace(/\s/g, '')); - - if (Object.is(formattedValue, NaN)) { + const valueBi = (() => { + try { + return BigInt(value); + } catch (error) { + return null; + } + })(); + + if (typeof value !== 'string' || valueBi === null) { return 'Invalid integer format'; } - if (formattedValue > argTypeMatchInt.max || formattedValue < argTypeMatchInt.min) { + if (valueBi > argTypeMatchInt.max || valueBi < argTypeMatchInt.min) { const lowerBoundary = argTypeMatchInt.isUnsigned ? '0' : `-1 * 2 ^ ${ Number(argTypeMatchInt.power) - 1 }`; const upperBoundary = argTypeMatchInt.isUnsigned ? `2 ^ ${ argTypeMatchInt.power } - 1` : `2 ^ ${ Number(argTypeMatchInt.power) - 1 } - 1`; return `Value should be in range from "${ lowerBoundary }" to "${ upperBoundary }" inclusively`; @@ -55,9 +61,8 @@ export default function useValidateField({ isOptional, argType, argTypeMatchInt } if (argType === 'bool') { - const formattedValue = formatBooleanValue(value); - if (formattedValue === undefined) { - return 'Invalid boolean format. Allowed values: 0, 1, true, false'; + if (typeof value !== 'boolean') { + return 'Invalid boolean format. Allowed values: true, false'; } } diff --git a/ui/address/contract/methodForm/utils.test.ts b/ui/address/contract/ABI/form/utils.test.ts similarity index 100% rename from ui/address/contract/methodForm/utils.test.ts rename to ui/address/contract/ABI/form/utils.test.ts diff --git a/ui/address/contract/ABI/form/utils.ts b/ui/address/contract/ABI/form/utils.ts new file mode 100644 index 0000000000..55abd78f1c --- /dev/null +++ b/ui/address/contract/ABI/form/utils.ts @@ -0,0 +1,103 @@ +import _set from 'lodash/set'; + +import type { ContractAbiItemInput } from '../types'; + +export type ContractMethodFormFields = Record; + +export const INT_REGEXP = /^(u)?int(\d+)?$/i; + +export const BYTES_REGEXP = /^bytes(\d+)?$/i; + +export const ARRAY_REGEXP = /^(.*)\[(\d*)\]$/; + +export interface MatchArray { + itemType: string; + size: number; + isNested: boolean; +} + +export const matchArray = (argType: string): MatchArray | null => { + const match = argType.match(ARRAY_REGEXP); + if (!match) { + return null; + } + + const [ , itemType, size ] = match; + const isNested = Boolean(matchArray(itemType)); + + return { + itemType, + size: size ? Number(size) : Infinity, + isNested, + }; +}; + +export interface MatchInt { + isUnsigned: boolean; + power: string; + min: bigint; + max: bigint; +} + +export const matchInt = (argType: string): MatchInt | null => { + 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: Boolean(isUnsigned), power, min, max }; +}; + +export const transformDataForArrayItem = (data: ContractAbiItemInput, index: number): ContractAbiItemInput => { + const arrayMatchType = matchArray(data.type); + const arrayMatchInternalType = data.internalType ? matchArray(data.internalType) : null; + const childrenInternalType = arrayMatchInternalType?.itemType.replaceAll('struct ', ''); + + const postfix = childrenInternalType ? ' ' + childrenInternalType : ''; + + return { + ...data, + type: arrayMatchType?.itemType || data.type, + internalType: childrenInternalType, + name: `#${ index + 1 }${ postfix }`, + }; +}; + +export const getIntBoundaries = (power: number, isUnsigned: boolean) => { + const maxUnsigned = BigInt(2 ** power); + const max = isUnsigned ? maxUnsigned - BigInt(1) : maxUnsigned / BigInt(2) - BigInt(1); + const min = isUnsigned ? BigInt(0) : -maxUnsigned / BigInt(2); + return [ min, max ]; +}; + +export function transformFormDataToMethodArgs(formData: ContractMethodFormFields) { + const result: Array = []; + + for (const field in formData) { + const value = formData[field]; + if (value !== undefined) { + _set(result, field.replaceAll(':', '.'), value); + } + } + + return filterOurEmptyItems(result); +} + +function filterOurEmptyItems(array: Array): Array { + // The undefined value may occur in two cases: + // 1. When an optional form field is left blank by the user. + // The only optional field is the native coin value, which is safely handled in the form submit handler. + // 2. When the user adds and removes items from a field array. + // In this scenario, empty items need to be filtered out to maintain the correct sequence of arguments. + return array + .map((item) => Array.isArray(item) ? filterOurEmptyItems(item) : item) + .filter((item) => item !== undefined); +} + +export function getFieldLabel(input: ContractAbiItemInput, isRequired?: boolean) { + const name = input.name || input.internalType || ''; + return `${ name } (${ input.type })${ isRequired ? '*' : '' }`; +} diff --git a/ui/address/contract/ABI/types.ts b/ui/address/contract/ABI/types.ts new file mode 100644 index 0000000000..7268edee30 --- /dev/null +++ b/ui/address/contract/ABI/types.ts @@ -0,0 +1,25 @@ +import type { AbiFunction } from 'abitype'; + +import type { SmartContractMethod, SmartContractMethodOutput, SmartContractQueryMethod } from 'types/api/contract'; + +import type { ResourceError } from 'lib/api/resources'; + +export type ContractAbiItemInput = AbiFunction['inputs'][number] & { fieldType?: 'native_coin' }; +export type ContractAbiItemOutput = SmartContractMethodOutput; +export type ContractAbiItem = SmartContractMethod; +export type ContractAbi = Array; + +export type MethodType = 'read' | 'write'; +export type MethodCallStrategy = 'api' | 'wallet_client'; + +export interface FormSubmitResultApi { + source: 'api'; + result: SmartContractQueryMethod | ResourceError | Error; +} +export interface FormSubmitResultWalletClient { + source: 'wallet_client'; + result: Error | { hash: `0x${ string }` | undefined } | undefined; +} +export type FormSubmitResult = FormSubmitResultApi | FormSubmitResultWalletClient; + +export type FormSubmitHandler = (item: ContractAbiItem, args: Array, submitType: MethodCallStrategy | undefined) => Promise; diff --git a/ui/address/contract/ABI/useCallMethodApi.ts b/ui/address/contract/ABI/useCallMethodApi.ts new file mode 100644 index 0000000000..80345a222d --- /dev/null +++ b/ui/address/contract/ABI/useCallMethodApi.ts @@ -0,0 +1,51 @@ +import React from 'react'; + +import type { FormSubmitResult } from './types'; +import type { SmartContractQueryMethod } from 'types/api/contract'; + +import type { ResourceError } from 'lib/api/resources'; +import useApiFetch from 'lib/api/useApiFetch'; +import useAccount from 'lib/web3/useAccount'; + +interface Params { + methodId: string; + args: Array; + isProxy: boolean; + isCustomAbi: boolean; + addressHash: string; +} + +export default function useCallMethodApi(): (params: Params) => Promise { + const apiFetch = useApiFetch(); + const { address } = useAccount(); + + return React.useCallback(async({ addressHash, isCustomAbi, isProxy, args, methodId }) => { + try { + const response = await apiFetch<'contract_method_query', SmartContractQueryMethod>('contract_method_query', { + pathParams: { hash: addressHash }, + queryParams: { + is_custom_abi: isCustomAbi ? 'true' : 'false', + }, + fetchParams: { + method: 'POST', + body: { + args, + method_id: methodId, + contract_type: isProxy ? 'proxy' : 'regular', + from: address, + }, + }, + }); + + return { + source: 'api', + result: response, + }; + } catch (error) { + return { + source: 'api', + result: error as (Error | ResourceError), + }; + } + }, [ address, apiFetch ]); +} diff --git a/ui/address/contract/ABI/useCallMethodWalletClient.ts b/ui/address/contract/ABI/useCallMethodWalletClient.ts new file mode 100644 index 0000000000..1ec49b0d32 --- /dev/null +++ b/ui/address/contract/ABI/useCallMethodWalletClient.ts @@ -0,0 +1,70 @@ +import React from 'react'; +import type { Abi } from 'viem'; +import { useAccount, useWalletClient, useSwitchChain } from 'wagmi'; + +import type { ContractAbiItem, FormSubmitResult } from './types'; + +import config from 'configs/app'; + +import { getNativeCoinValue } from './utils'; + +interface Params { + item: ContractAbiItem; + args: Array; + addressHash: string; +} + +export default function useCallMethodWalletClient(): (params: Params) => Promise { + const { data: walletClient } = useWalletClient(); + const { isConnected, chainId } = useAccount(); + const { switchChainAsync } = useSwitchChain(); + + return React.useCallback(async({ args, item, addressHash }) => { + if (!isConnected) { + throw new Error('Wallet is not connected'); + } + + if (!walletClient) { + throw new Error('Wallet Client is not defined'); + } + + if (chainId && String(chainId) !== config.chain.id) { + await switchChainAsync?.({ chainId: Number(config.chain.id) }); + } + + if (item.type === 'receive' || item.type === 'fallback') { + const value = getNativeCoinValue(args[0]); + const hash = await walletClient.sendTransaction({ + to: addressHash as `0x${ string }` | undefined, + value, + }); + return { source: 'wallet_client', result: { hash } }; + } + + 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 hash = await walletClient.writeContract({ + args: _args, + // Here we provide the ABI as an array containing only one item from the submitted form. + // This is a workaround for the issue with the "viem" library. + // It lacks a "method_id" field to uniquely identify the correct method and instead attempts to find a method based on its name. + // But the name is not unique in the contract ABI and this behavior in the "viem" could result in calling the wrong method. + // See related issues: + // - https://github.com/blockscout/frontend/issues/1032, + // - https://github.com/blockscout/frontend/issues/1327 + abi: [ item ] as Abi, + functionName: methodName, + address: addressHash as `0x${ string }`, + value, + }); + + return { source: 'wallet_client', result: { hash } }; + }, [ chainId, isConnected, switchChainAsync, walletClient ]); +} diff --git a/ui/address/contract/ABI/useFormSubmit.ts b/ui/address/contract/ABI/useFormSubmit.ts new file mode 100644 index 0000000000..a401955658 --- /dev/null +++ b/ui/address/contract/ABI/useFormSubmit.ts @@ -0,0 +1,72 @@ +import React from 'react'; + +import type { FormSubmitHandler } from './types'; + +import config from 'configs/app'; + +import useCallMethodApi from './useCallMethodApi'; +import useCallMethodWalletClient from './useCallMethodWalletClient'; + +interface Params { + tab: string; + addressHash: string; +} + +function useFormSubmit({ tab, addressHash }: Params): FormSubmitHandler { + const callMethodApi = useCallMethodApi(); + const callMethodWalletClient = useCallMethodWalletClient(); + + return React.useCallback(async(item, args, strategy) => { + switch (strategy) { + case 'api': { + if (!('method_id' in item)) { + throw new Error('Method ID is not defined'); + } + return callMethodApi({ + args, + methodId: item.method_id, + addressHash, + isCustomAbi: tab === 'read_custom_methods' || tab === 'write_custom_methods', + isProxy: tab === 'read_proxy' || tab === 'write_proxy', + }); + } + case 'wallet_client': { + return callMethodWalletClient({ args, item, addressHash }); + } + + default: { + throw new Error(`Unknown call strategy "${ strategy }"`); + } + } + }, [ addressHash, callMethodApi, callMethodWalletClient, tab ]); +} + +function useFormSubmitFallback({ tab, addressHash }: Params): FormSubmitHandler { + const callMethodApi = useCallMethodApi(); + + return React.useCallback(async(item, args, strategy) => { + switch (strategy) { + case 'api': { + if (!('method_id' in item)) { + throw new Error('Method ID is not defined'); + } + return callMethodApi({ + args, + methodId: item.method_id, + addressHash, + isCustomAbi: tab === 'read_custom_methods' || tab === 'write_custom_methods', + isProxy: tab === 'read_proxy' || tab === 'write_proxy', + }); + } + + default: { + throw new Error(`Unknown call strategy "${ strategy }"`); + } + } + + }, [ addressHash, callMethodApi, tab ]); +} + +const hook = config.features.blockchainInteraction.isEnabled ? useFormSubmit : useFormSubmitFallback; + +export default hook; diff --git a/ui/address/contract/ABI/useScrollToMethod.ts b/ui/address/contract/ABI/useScrollToMethod.ts new file mode 100644 index 0000000000..ddeac03d5d --- /dev/null +++ b/ui/address/contract/ABI/useScrollToMethod.ts @@ -0,0 +1,26 @@ +import React from 'react'; +import { scroller } from 'react-scroll'; + +import type { ContractAbi } from './types'; + +export const getElementName = (id: string) => `method_${ id }`; + +export default function useScrollToMethod(data: ContractAbi, onScroll: (indices: Array) => void) { + React.useEffect(() => { + const id = window.location.hash.replace('#', ''); + + if (!id) { + return; + } + + const index = data.findIndex((item) => 'method_id' in item && item.method_id === id); + if (index > -1) { + scroller.scrollTo(getElementName(id), { + duration: 500, + smooth: true, + offset: -100, + }); + onScroll([ index ]); + } + }, [ data, onScroll ]); +} diff --git a/ui/address/contract/ABI/utils.ts b/ui/address/contract/ABI/utils.ts new file mode 100644 index 0000000000..46c9fb2c8a --- /dev/null +++ b/ui/address/contract/ABI/utils.ts @@ -0,0 +1,7 @@ +export const getNativeCoinValue = (value: unknown) => { + if (typeof value !== 'string') { + return BigInt(0); + } + + return BigInt(value); +}; diff --git a/ui/address/contract/ContractCode.pw.tsx b/ui/address/contract/ContractCode.pw.tsx deleted file mode 100644 index cbd660a941..0000000000 --- a/ui/address/contract/ContractCode.pw.tsx +++ /dev/null @@ -1,286 +0,0 @@ -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 }, - }, -}; - -const test = base.extend({ - createSocket: socketServer.createSocket, -}); - -// FIXME -// test cases which use socket cannot run in parallel since the socket server always run on the same port -test.describe.configure({ mode: 'serial' }); - -test('full view +@mobile +@dark-mode', async({ mount, page }) => { - await page.route('https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/**', (route) => route.abort()); - await page.route(CONTRACT_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(contractMock.withChangedByteCode), - })); - - const ADDRESS_API_URL = buildApiUrl('address', { hash: addressHash }); - await page.route(ADDRESS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(addressMock.contract), - })); - - const PROXY_CONTRACT_API_URL = buildApiUrl('contract', { hash: addressMock.contract.implementation_address as string }); - await page.route(PROXY_CONTRACT_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(contractMock.withChangedByteCode), - })); - - const component = await mount( - - - - - , - { hooksConfig }, - ); - - await expect(component).toHaveScreenshot(); -}); - -test('verified with changed byte code socket', async({ mount, page, createSocket }) => { - await page.route(CONTRACT_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(contractMock.verified), - })); - await page.route('https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/**', (route) => route.abort()); - - const component = await mount( - - - , - { hooksConfig }, - ); - - const socket = await createSocket(); - const channel = await socketServer.joinChannel(socket, 'addresses:' + addressHash.toLowerCase()); - socketServer.sendMessage(socket, channel, 'changed_bytecode', {}); - - await expect(component).toHaveScreenshot(); -}); - -test('verified via lookup in eth_bytecode_db', async({ mount, page, createSocket }) => { - await page.route(CONTRACT_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(contractMock.nonVerified), - })); - await page.route('https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/**', (route) => route.abort()); - - await mount( - - - , - { hooksConfig }, - ); - - const socket = await createSocket(); - const channel = await socketServer.joinChannel(socket, 'addresses:' + addressHash.toLowerCase()); - - await page.waitForResponse(CONTRACT_API_URL); - socketServer.sendMessage(socket, channel, 'smart_contract_was_verified', {}); - - const request = await page.waitForRequest(CONTRACT_API_URL); - - expect(request).toBeTruthy(); -}); - -test('verified with multiple sources', async({ mount, page }) => { - await page.route(CONTRACT_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(contractMock.withMultiplePaths), - })); - await page.route('https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/**', (route) => route.abort()); - - await mount( - - - , - { hooksConfig }, - ); - - const section = page.locator('section', { hasText: 'Contract source code' }); - await expect(section).toHaveScreenshot(); - - await page.getByRole('button', { name: 'View external libraries' }).click(); - await expect(section).toHaveScreenshot(); - - await page.getByRole('button', { name: 'Open source code in IDE' }).click(); - await expect(section).toHaveScreenshot(); -}); - -test('verified via sourcify', async({ mount, page }) => { - await page.route(CONTRACT_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(contractMock.verifiedViaSourcify), - })); - await page.route('https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/**', (route) => route.abort()); - - await mount( - - - , - { hooksConfig }, - ); - - await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 110 } }); -}); - -test('verified via eth bytecode db', async({ mount, page }) => { - await page.route(CONTRACT_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(contractMock.verifiedViaEthBytecodeDb), - })); - await page.route('https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/**', (route) => route.abort()); - - await mount( - - - , - { hooksConfig }, - ); - - await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 110 } }); -}); - -test('self destructed', async({ mount, page }) => { - await page.route(CONTRACT_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(contractMock.selfDestructed), - })); - await page.route('https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/**', (route) => route.abort()); - - await mount( - - - , - { hooksConfig }, - ); - - const section = page.locator('section', { hasText: 'Contract creation code' }); - await expect(section).toHaveScreenshot(); -}); - -test('with twin address alert +@mobile', async({ mount, page }) => { - await page.route(CONTRACT_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(contractMock.withTwinAddress), - })); - await page.route('https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/**', (route) => route.abort()); - - const component = await mount( - - - , - { hooksConfig }, - ); - - await expect(component.getByRole('alert')).toHaveScreenshot(); -}); - -test('with proxy address alert +@mobile', async({ mount, page }) => { - await page.route(CONTRACT_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(contractMock.withProxyAddress), - })); - await page.route('https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/**', (route) => route.abort()); - - const component = await mount( - - - , - { hooksConfig }, - ); - - await expect(component.getByRole('alert')).toHaveScreenshot(); -}); - -test('non verified', async({ mount, page }) => { - await page.route(CONTRACT_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(contractMock.nonVerified), - })); - 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(); -}); - -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 9b915ddb21..fddb9c8fd8 100644 --- a/ui/address/contract/ContractCode.tsx +++ b/ui/address/contract/ContractCode.tsx @@ -1,20 +1,24 @@ -import { Flex, Skeleton, Button, Grid, GridItem, Alert, Link, chakra, Box } from '@chakra-ui/react'; +import { Flex, Skeleton, Button, Grid, GridItem, Alert, Link, chakra, Box, useColorModeValue } from '@chakra-ui/react'; +import type { UseQueryResult } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query'; +import type { Channel } from 'phoenix'; import React from 'react'; import type { SocketMessage } from 'lib/socket/types'; import type { Address as AddressInfo } from 'types/api/address'; +import type { SmartContract } from 'types/api/contract'; import { route } from 'nextjs-routes'; import config from 'configs/app'; -import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; +import type { ResourceError } from 'lib/api/resources'; +import { 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'; -import * as stubs from 'stubs/contract'; import DataFetchAlert from 'ui/shared/DataFetchAlert'; import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import Hint from 'ui/shared/Hint'; import LinkExternal from 'ui/shared/LinkExternal'; import LinkInternal from 'ui/shared/LinkInternal'; import RawDataSnippet from 'ui/shared/RawDataSnippet'; @@ -24,8 +28,8 @@ import ContractSourceCode from './ContractSourceCode'; type Props = { addressHash?: string; - // prop for pw tests only - noSocket?: boolean; + contractQuery: UseQueryResult>; + channel: Channel | undefined; } type InfoItemProps = { @@ -33,30 +37,35 @@ type InfoItemProps = { content: string | React.ReactNode; className?: string; isLoading: boolean; + hint?: string; } -const InfoItem = chakra(({ label, content, className, isLoading }: InfoItemProps) => ( +const InfoItem = chakra(({ label, content, hint, className, isLoading }: InfoItemProps) => ( - { label } + + + { label } + { hint && ( + + ) } + + { content } )); -const ContractCode = ({ addressHash, noSocket }: Props) => { - const [ isQueryEnabled, setIsQueryEnabled ] = React.useState(false); +const ContractCode = ({ addressHash, contractQuery, channel }: Props) => { const [ isChangedBytecodeSocket, setIsChangedBytecodeSocket ] = React.useState(); const queryClient = useQueryClient(); const addressInfo = queryClient.getQueryData(getResourceKey('address', { pathParams: { hash: addressHash } })); - const { data, isPlaceholderData, isError } = useApiQuery('contract', { - pathParams: { hash: addressHash }, - queryOptions: { - enabled: Boolean(addressHash) && (noSocket || isQueryEnabled), - refetchOnMount: false, - placeholderData: addressInfo?.is_verified ? stubs.CONTRACT_CODE_VERIFIED : stubs.CONTRACT_CODE_UNVERIFIED, - }, - }); + const { data, isPlaceholderData, isError } = contractQuery; const handleChangedBytecodeMessage: SocketMessage.AddressChangedBytecode['handler'] = React.useCallback(() => { setIsChangedBytecodeSocket(true); @@ -71,14 +80,6 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { }); }, [ addressHash, queryClient ]); - const enableQuery = React.useCallback(() => setIsQueryEnabled(true), []); - - const channel = useSocketChannel({ - topic: `addresses:${ addressHash?.toLowerCase() }`, - isDisabled: !addressHash, - onJoin: enableQuery, - onSocketError: enableQuery, - }); useSocketMessage({ channel, event: 'changed_bytecode', @@ -118,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; @@ -233,6 +251,14 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { { data.name && } { data.compiler_version && } { data.evm_version && } + { licenseLink && ( + + ) } { typeof data.optimization_enabled === 'boolean' && } { data.optimization_runs && } diff --git a/ui/address/contract/ContractMethodCallable.tsx b/ui/address/contract/ContractMethodCallable.tsx new file mode 100644 index 0000000000..b2b91afaeb --- /dev/null +++ b/ui/address/contract/ContractMethodCallable.tsx @@ -0,0 +1,162 @@ +import { Box, Button, chakra, Flex, Icon, Text } from '@chakra-ui/react'; +import _fromPairs from 'lodash/fromPairs'; +import React from 'react'; +import type { SubmitHandler } from 'react-hook-form'; +import { useForm } from 'react-hook-form'; + +import type { MethodFormFields, ContractMethodCallResult } from './types'; +import type { SmartContractMethodInput, SmartContractMethod } from 'types/api/contract'; + +import arrowIcon from 'icons/arrows/down-right.svg'; + +import ContractMethodField from './ContractMethodField'; + +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; +} + +const getFieldName = (name: string | undefined, index: number): string => name || String(index); + +const sortFields = (data: Array) => ([ a ]: [string, string], [ b ]: [string, string]): 1 | -1 | 0 => { + const fieldNames = data.map(({ name }, index) => getFieldName(name, index)); + const indexA = fieldNames.indexOf(a); + const indexB = fieldNames.indexOf(b); + + if (indexA > indexB) { + return 1; + } + + if (indexA < indexB) { + return -1; + } + + return 0; +}; + +const castFieldValue = (data: Array) => ([ key, value ]: [ string, string ], index: number) => { + if (data[index].type.includes('[')) { + return [ key, parseArrayValue(value) ]; + } + return [ key, value ]; +}; + +const parseArrayValue = (value: string) => { + try { + const parsedResult = JSON.parse(value); + if (Array.isArray(parsedResult)) { + return parsedResult; + } + throw new Error('Not an array'); + } catch (error) { + return ''; + } +}; + +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: 'value', + type: 'uint256' as const, + internalType: 'uint256' as const, + } ] : []), + ]; + }, [ data ]); + + const { control, handleSubmit, setValue, getValues } = useForm({ + defaultValues: _fromPairs(inputs.map(({ name }, index) => [ getFieldName(name, index), '' ])), + }); + + const handleTxSettle = React.useCallback(() => { + setLoading(false); + }, []); + + const handleFormChange = React.useCallback(() => { + result && setResult(undefined); + }, [ result ]); + + const onFormSubmit: SubmitHandler = React.useCallback(async(formData) => { + const args = Object.entries(formData) + .sort(sortFields(inputs)) + .map(castFieldValue(inputs)) + .map(([ , value ]) => value); + + 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); + }); + }, [ onSubmit, data, inputs ]); + + return ( + + + { inputs.map(({ type, name }, index) => { + const fieldName = getFieldName(name, index); + return ( + + ); + }) } + + + { 'outputs' in data && !isWrite && data.outputs.length > 0 && ( + + + { data.outputs.map(({ type }) => type).join(', ') } + + ) } + { result && } + + ); +}; + +export default React.memo(ContractMethodCallable) as typeof ContractMethodCallable; diff --git a/ui/address/contract/ContractMethodField.tsx b/ui/address/contract/ContractMethodField.tsx new file mode 100644 index 0000000000..d85deeaebb --- /dev/null +++ b/ui/address/contract/ContractMethodField.tsx @@ -0,0 +1,84 @@ +import { + FormControl, + Input, + InputGroup, + InputRightElement, +} from '@chakra-ui/react'; +import React from 'react'; +import type { Control, ControllerRenderProps, UseFormGetValues, UseFormSetValue } from 'react-hook-form'; +import { Controller } from 'react-hook-form'; + +import type { MethodFormFields } from './types'; +import type { SmartContractMethodArgType } from 'types/api/contract'; + +import ClearButton from 'ui/shared/ClearButton'; + +import ContractMethodFieldZeroes from './ContractMethodFieldZeroes'; +import { addZeroesAllowed } from './utils'; + +interface Props { + control: Control; + setValue: UseFormSetValue; + getValues: UseFormGetValues; + placeholder: string; + name: string; + valueType: SmartContractMethodArgType; + isDisabled: boolean; + onChange: () => void; +} + +const ContractMethodField = ({ control, name, valueType, placeholder, setValue, getValues, isDisabled, onChange }: Props) => { + const ref = React.useRef(null); + + const handleClear = React.useCallback(() => { + setValue(name, ''); + onChange(); + ref.current?.focus(); + }, [ name, onChange, setValue ]); + + const handleAddZeroesClick = React.useCallback((power: number) => { + const value = getValues()[name]; + const zeroes = Array(power).fill('0').join(''); + const newValue = value ? value + zeroes : '1' + zeroes; + setValue(name, newValue); + onChange(); + }, [ getValues, name, onChange, setValue ]); + + const hasZerosControl = addZeroesAllowed(valueType); + + const renderInput = React.useCallback(({ field }: { field: ControllerRenderProps }) => { + return ( + + + + + { field.value && } + { hasZerosControl && } + + + + ); + }, [ name, isDisabled, placeholder, hasZerosControl, handleClear, handleAddZeroesClick ]); + + return ( + + + ); +}; + +export default React.memo(ContractMethodField); diff --git a/ui/address/contract/ContractMethodFieldZeroes.tsx b/ui/address/contract/ContractMethodFieldZeroes.tsx new file mode 100644 index 0000000000..bf078260f8 --- /dev/null +++ b/ui/address/contract/ContractMethodFieldZeroes.tsx @@ -0,0 +1,131 @@ +import { + chakra, + Popover, + PopoverBody, + PopoverContent, + PopoverTrigger, + Portal, + Button, + List, + ListItem, + Icon, + useDisclosure, + Input, +} from '@chakra-ui/react'; +import React from 'react'; + +import iconEastMini from 'icons/arrows/east-mini.svg'; +import iconCheck from 'icons/check.svg'; +import { times } from 'lib/html-entities'; + +interface Props { + onClick: (power: number) => void; + isDisabled?: boolean; +} + +const ContractMethodFieldZeroes = ({ onClick, isDisabled }: Props) => { + const [ selectedOption, setSelectedOption ] = React.useState(18); + const [ customValue, setCustomValue ] = React.useState(); + const { isOpen, onToggle, onClose } = useDisclosure(); + + const handleOptionClick = React.useCallback((event: React.MouseEvent) => { + const id = Number((event.currentTarget as HTMLDivElement).getAttribute('data-id')); + if (!Object.is(id, NaN)) { + setSelectedOption((prev) => prev === id ? undefined : id); + setCustomValue(undefined); + onClose(); + } + }, [ onClose ]); + + const handleInputChange = React.useCallback((event: React.ChangeEvent) => { + setCustomValue(Number(event.target.value)); + setSelectedOption(undefined); + }, []); + + const value = selectedOption || customValue; + + const handleButtonClick = React.useCallback(() => { + value && onClick(value); + }, [ onClick, value ]); + + return ( + <> + { Boolean(value) && ( + + ) } + + + + + + + + + { [ 8, 12, 16, 18, 20 ].map((id) => ( + + 10*{ id } + { selectedOption === id && } + + )) } + + 10* + + + + + + + + + ); +}; + +export default React.memo(ContractMethodFieldZeroes); diff --git a/ui/address/contract/ContractRead.tsx b/ui/address/contract/ContractRead.tsx index 7a2fd94ded..9bd819025c 100644 --- a/ui/address/contract/ContractRead.tsx +++ b/ui/address/contract/ContractRead.tsx @@ -1,27 +1,24 @@ -import { Alert, Flex } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import React from 'react'; -import type { SmartContractReadMethod, SmartContractQueryMethodRead } from 'types/api/contract'; - -import useApiFetch from 'lib/api/useApiFetch'; +import config from 'configs/app'; import useApiQuery from 'lib/api/useApiQuery'; import getQueryParamString from 'lib/router/getQueryParamString'; -import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion'; +import useAccount from 'lib/web3/useAccount'; import ContentLoader from 'ui/shared/ContentLoader'; import DataFetchAlert from 'ui/shared/DataFetchAlert'; +import ContractAbi from './ABI/ContractAbi'; import ContractConnectWallet from './ContractConnectWallet'; import ContractCustomAbiAlert from './ContractCustomAbiAlert'; import ContractImplementationAddress from './ContractImplementationAddress'; -import ContractMethodConstant from './ContractMethodConstant'; -import ContractReadResult from './ContractReadResult'; -import ContractMethodForm from './methodForm/ContractMethodForm'; -import useWatchAccount from './useWatchAccount'; -const ContractRead = () => { - const apiFetch = useApiFetch(); - const account = useWatchAccount(); +interface Props { + isLoading?: boolean; +} + +const ContractRead = ({ isLoading }: Props) => { + const { address } = useAccount(); const router = useRouter(); const tab = getQueryParamString(router.query.tab); @@ -33,55 +30,13 @@ const ContractRead = () => { pathParams: { hash: addressHash }, queryParams: { is_custom_abi: isCustomAbi ? 'true' : 'false', - from: account?.address, + from: address, }, queryOptions: { - enabled: Boolean(addressHash), + enabled: !isLoading, }, }); - const handleMethodFormSubmit = React.useCallback(async(item: SmartContractReadMethod, args: Array) => { - return apiFetch<'contract_method_query', SmartContractQueryMethodRead>('contract_method_query', { - pathParams: { hash: addressHash }, - queryParams: { - is_custom_abi: isCustomAbi ? 'true' : 'false', - }, - fetchParams: { - method: 'POST', - body: { - args, - method_id: item.method_id, - contract_type: isProxy ? 'proxy' : 'regular', - from: account?.address, - }, - }, - }); - }, [ account?.address, addressHash, apiFetch, isCustomAbi, isProxy ]); - - const renderItemContent = React.useCallback((item: SmartContractReadMethod, index: number, id: number) => { - if (item.error) { - return { item.error }; - } - - if (item.outputs?.some(({ value }) => value !== undefined && value !== null)) { - return ( - - { item.outputs.map((output, index) => ) } - - ); - } - - return ( - - ); - }, [ handleMethodFormSubmit ]); - if (isError) { return ; } @@ -97,9 +52,9 @@ const ContractRead = () => { return ( <> { isCustomAbi && } - { account && } + { config.features.blockchainInteraction.isEnabled && } { isProxy && } - + ); }; diff --git a/ui/address/contract/ContractReadResult.pw.tsx b/ui/address/contract/ContractReadResult.pw.tsx deleted file mode 100644 index 35b4991a0c..0000000000 --- a/ui/address/contract/ContractReadResult.pw.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { test, expect } from '@playwright/experimental-ct-react'; -import React from 'react'; - -import type { ContractMethodReadResult } from './types'; - -import * as contractMethodsMock from 'mocks/contract/methods'; -import TestApp from 'playwright/TestApp'; - -import ContractReadResult from './ContractReadResult'; - -const item = contractMethodsMock.read[0]; -const onSettle = () => Promise.resolve(); - -test.use({ viewport: { width: 500, height: 500 } }); - -test('default error', async({ mount }) => { - const result: ContractMethodReadResult = { - is_error: true, - result: { - error: 'I am an error', - }, - }; - const component = await mount( - - - , - ); - - await expect(component).toHaveScreenshot(); -}); - -test('error with code', async({ mount }) => { - const result: ContractMethodReadResult = { - is_error: true, - result: { - message: 'I am an error', - code: -32017, - }, - }; - const component = await mount( - - - , - ); - - await expect(component).toHaveScreenshot(); -}); - -test('raw error', async({ mount }) => { - const result: ContractMethodReadResult = { - is_error: true, - result: { - raw: '49276d20616c7761797320726576657274696e67207769746820616e206572726f72', - }, - }; - const component = await mount( - - - , - ); - - await expect(component).toHaveScreenshot(); -}); - -test('complex error', async({ mount }) => { - const result: ContractMethodReadResult = { - is_error: true, - result: { - method_call: 'SomeCustomError(address addr, uint256 balance)', - method_id: '50289a9f', - parameters: [ - { name: 'addr', type: 'address', value: '0x850e73b42f48e91ebaedf8f00a74f6147e485c5a' }, - { name: 'balance', type: 'uint256', value: '14' }, - ], - }, - }; - const component = await mount( - - - , - ); - - await expect(component).toHaveScreenshot(); -}); - -test('success', async({ mount }) => { - const result: ContractMethodReadResult = { - is_error: false, - result: { - names: [ 'address' ], - output: [ { type: 'address', value: '0x0000000000000000000000000000000000000000' } ], - }, - }; - const component = await mount( - - - , - ); - - await expect(component).toHaveScreenshot(); -}); - -test('complex success', async({ mount }) => { - const result: ContractMethodReadResult = { - is_error: false, - result: { - names: [ - [ - 'data', - [ 'totalSupply', 'owner', 'symbol' ], - ], - 'supports721', - 'page', - ], - output: [ - { - type: 'tuple[uint256,address,string]', - value: [ 1000, '0xe150519ae293922cfe6217feba3add4726f5e851', 'AOC_INCUBATORS' ], - }, - { type: 'bool', value: 'true' }, - { type: 'uint256[]', value: [ 1, 2, 3, 4, 5 ] }, - ], - }, - }; - const component = await mount( - - - , - ); - - await expect(component).toHaveScreenshot(); -}); diff --git a/ui/address/contract/ContractReadResult.tsx b/ui/address/contract/ContractReadResult.tsx deleted file mode 100644 index 28f701eb9b..0000000000 --- a/ui/address/contract/ContractReadResult.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { Alert, Box, chakra, useColorModeValue } from '@chakra-ui/react'; -import React from 'react'; - -import type { ContractMethodReadResult } from './types'; -import type { SmartContractQueryMethodReadSuccess, SmartContractReadMethod } from 'types/api/contract'; - -import hexToUtf8 from 'lib/hexToUtf8'; - -const TUPLE_TYPE_REGEX = /\[(.+)\]/; - -const ContractReadResultError = ({ children }: {children: React.ReactNode}) => { - return ( - - { children } - - ); -}; - -interface ItemProps { - output: SmartContractQueryMethodReadSuccess['result']['output'][0]; - name: SmartContractQueryMethodReadSuccess['result']['names'][0]; -} - -const ContractReadResultItem = ({ output, name }: ItemProps) => { - if (Array.isArray(name)) { - const [ structName, argNames ] = name; - const argTypes = output.type.match(TUPLE_TYPE_REGEX)?.[1].split(','); - - return ( - <> -

- { structName } - ({ output.type }) : -

- { argNames.map((argName, argIndex) => { - return ( -

- { argName } - { argTypes?.[argIndex] ? ` (${ argTypes[argIndex] })` : '' } : { String(output.value[argIndex]) } -

- ); - }) } - - ); - } - - return ( -

- - { name && { name } } - ({ output.type }) : { String(output.value) } -

- ); -}; - -interface Props { - item: SmartContractReadMethod; - result: ContractMethodReadResult; - onSettle: () => void; -} - -const ContractReadResult = ({ item, result, onSettle }: Props) => { - const resultBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); - - React.useEffect(() => { - onSettle(); - }, [ onSettle ]); - - if ('status' in result) { - return { result.statusText }; - } - - if (result.is_error) { - if ('error' in result.result) { - return { result.result.error }; - } - - if ('message' in result.result) { - return [{ result.result.code }] { result.result.message }; - } - - if ('raw' in result.result) { - return { `Revert reason: ${ hexToUtf8(result.result.raw) }` }; - } - - if ('method_id' in result.result) { - return { JSON.stringify(result.result, undefined, 2) }; - } - - return Something went wrong.; - } - - return ( - -

- [ { 'name' in item ? item.name : '' } method response ] -

-

[

- { result.result.output.map((output, index) => ) } -

]

-
- ); -}; - -export default React.memo(ContractReadResult); 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 3e68421ed2..b79d8e0a02 100644 --- a/ui/address/contract/ContractWrite.tsx +++ b/ui/address/contract/ContractWrite.tsx @@ -1,30 +1,22 @@ import { useRouter } from 'next/router'; import React from 'react'; -import { useAccount, useWalletClient, useNetwork, useSwitchNetwork } from 'wagmi'; - -import type { SmartContractWriteMethod } from 'types/api/contract'; import config from 'configs/app'; import useApiQuery from 'lib/api/useApiQuery'; import getQueryParamString from 'lib/router/getQueryParamString'; -import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion'; import ContentLoader from 'ui/shared/ContentLoader'; import DataFetchAlert from 'ui/shared/DataFetchAlert'; +import ContractAbi from './ABI/ContractAbi'; import ContractConnectWallet from './ContractConnectWallet'; import ContractCustomAbiAlert from './ContractCustomAbiAlert'; import ContractImplementationAddress from './ContractImplementationAddress'; -import ContractWriteResult from './ContractWriteResult'; -import ContractMethodForm from './methodForm/ContractMethodForm'; -import useContractAbi from './useContractAbi'; -import { getNativeCoinValue, prepareAbi } from './utils'; -const ContractWrite = () => { - const { data: walletClient } = useWalletClient(); - const { isConnected } = useAccount(); - const { chain } = useNetwork(); - const { switchNetworkAsync } = useSwitchNetwork(); +interface Props { + isLoading?: boolean; +} +const ContractWrite = ({ isLoading }: Props) => { const router = useRouter(); const tab = getQueryParamString(router.query.tab); @@ -38,69 +30,11 @@ const ContractWrite = () => { is_custom_abi: isCustomAbi ? 'true' : 'false', }, queryOptions: { - enabled: Boolean(addressHash), + enabled: !isLoading, refetchOnMount: false, }, }); - const contractAbi = useContractAbi({ addressHash, isProxy, isCustomAbi }); - - // 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'); - } - - if (chain?.id && String(chain.id) !== config.chain.id) { - await switchNetworkAsync?.(Number(config.chain.id)); - } - - if (!contractAbi) { - throw new Error('Something went wrong. Try again later.'); - } - - if (item.type === 'receive' || item.type === 'fallback') { - const value = getNativeCoinValue(args[0]); - const hash = await walletClient?.sendTransaction({ - to: addressHash as `0x${ string }` | undefined, - value, - }); - return { hash }; - } - - 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, - }); - - return { hash }; - }, [ isConnected, chain, contractAbi, walletClient, addressHash, switchNetworkAsync ]); - - const renderItemContent = React.useCallback((item: SmartContractWriteMethod, index: number, id: number) => { - return ( - - ); - }, [ handleMethodFormSubmit ]); - if (isError) { return ; } @@ -116,9 +50,9 @@ const ContractWrite = () => { return ( <> { isCustomAbi && } - + { config.features.blockchainInteraction.isEnabled && } { isProxy && } - + ); }; diff --git a/ui/address/contract/ContractWriteResult.tsx b/ui/address/contract/ContractWriteResult.tsx deleted file mode 100644 index 266f64dd03..0000000000 --- a/ui/address/contract/ContractWriteResult.tsx +++ /dev/null @@ -1,19 +0,0 @@ -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'; - -const ContractWriteResult = ({ result, onSettle }: ResultComponentProps) => { - const txHash = result && 'hash' in result ? result.hash as `0x${ string }` : undefined; - const txInfo = useWaitForTransaction({ - hash: txHash, - }); - - return ; -}; - -export default React.memo(ContractWriteResult) as typeof ContractWriteResult; diff --git a/ui/address/contract/ContractWriteResultDumb.pw.tsx b/ui/address/contract/ContractWriteResultDumb.pw.tsx deleted file mode 100644 index e082542ff0..0000000000 --- a/ui/address/contract/ContractWriteResultDumb.pw.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { test, expect } from '@playwright/experimental-ct-react'; -import React from 'react'; - -import TestApp from 'playwright/TestApp'; - -import ContractWriteResultDumb from './ContractWriteResultDumb'; - -test('loading', async({ mount }) => { - const props = { - txInfo: { - status: 'loading' as const, - error: null, - }, - result: { - hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29' as `0x${ string }`, - }, - onSettle: () => {}, - }; - - const component = await mount( - - - , - ); - - await expect(component).toHaveScreenshot(); -}); - -test('success', async({ mount }) => { - const props = { - txInfo: { - status: 'success' as const, - error: null, - }, - result: { - hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29' as `0x${ string }`, - }, - onSettle: () => {}, - }; - - const component = await mount( - - - , - ); - - await expect(component).toHaveScreenshot(); -}); - -test('error +@mobile', async({ mount }) => { - const props = { - txInfo: { - status: 'error' as const, - error: { - // eslint-disable-next-line max-len - message: 'missing revert data in call exception; Transaction reverted without a reason string [ See: https://links.ethers.org/v5-errors-CALL_EXCEPTION ]', - } as Error, - }, - result: { - hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29' as `0x${ string }`, - }, - onSettle: () => {}, - }; - - const component = await mount( - - - , - ); - - await expect(component).toHaveScreenshot(); -}); - -test('error in result', async({ mount }) => { - const props = { - txInfo: { - status: 'idle' as const, - error: null, - }, - result: { - message: 'wallet is not connected', - } as Error, - onSettle: () => {}, - }; - - const component = await mount( - - - , - ); - - await expect(component).toHaveScreenshot(); -}); 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..05c6e15e3a 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..719afffe43 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..fc9a51d73d 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 index e8500aa473..a3c93ab6fe 100644 Binary files a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-audits-feature-has-audits-1.png 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 index c696f2d596..bdbd0774b3 100644 Binary files a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-audits-feature-no-audits-1.png 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..16444113bf 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 f19ba84553..9ca2b3c467 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 9402cd11fc..3fb74dce71 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 82900c2f57..fe2ffcfcba 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 635406e6f5..171aa0aad1 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_default_error-result-1.png b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_default_error-result-1.png new file mode 100644 index 0000000000..66552cc739 Binary files /dev/null and b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_default_error-result-1.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 930ca02a9e..3d31997d9d 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 b5d534a0d0..77c8176a15 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__/ContractReadResult.pw.tsx_default_complex-error-1.png b/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_complex-error-1.png deleted file mode 100644 index 80741a58e9..0000000000 Binary files a/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_complex-error-1.png and /dev/null differ diff --git a/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_complex-success-1.png b/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_complex-success-1.png deleted file mode 100644 index 695b1df2c6..0000000000 Binary files a/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_complex-success-1.png and /dev/null differ diff --git a/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_default-error-1.png b/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_default-error-1.png deleted file mode 100644 index 31c2a142c1..0000000000 Binary files a/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_default-error-1.png and /dev/null differ diff --git a/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_error-with-code-1.png b/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_error-with-code-1.png deleted file mode 100644 index 680fbde860..0000000000 Binary files a/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_error-with-code-1.png and /dev/null differ diff --git a/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_raw-error-1.png b/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_raw-error-1.png deleted file mode 100644 index dfbf8524ab..0000000000 Binary files a/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_raw-error-1.png and /dev/null differ diff --git a/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_success-1.png b/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_success-1.png deleted file mode 100644 index 0010723b55..0000000000 Binary files a/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_success-1.png and /dev/null 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 e58e9c4755..19741ab173 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 c9a99a0bfb..20cf83a691 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/__screenshots__/ContractWriteResultDumb.pw.tsx_default_error-in-result-1.png b/ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_default_error-in-result-1.png deleted file mode 100644 index 30a20b5510..0000000000 Binary files a/ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_default_error-in-result-1.png and /dev/null differ diff --git a/ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_default_error-mobile-1.png b/ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_default_error-mobile-1.png deleted file mode 100644 index e9ec6c1578..0000000000 Binary files a/ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_default_error-mobile-1.png and /dev/null differ diff --git a/ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_default_loading-1.png b/ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_default_loading-1.png deleted file mode 100644 index 986269100c..0000000000 Binary files a/ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_default_loading-1.png and /dev/null differ diff --git a/ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_default_success-1.png b/ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_default_success-1.png deleted file mode 100644 index 49697a5f42..0000000000 Binary files a/ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_default_success-1.png and /dev/null differ diff --git a/ui/address/contract/methodForm/ContractMethodForm.tsx b/ui/address/contract/methodForm/ContractMethodForm.tsx deleted file mode 100644 index 5bcd5e3af6..0000000000 --- a/ui/address/contract/methodForm/ContractMethodForm.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { Box, Button, Flex, chakra } from '@chakra-ui/react'; -import React from 'react'; -import type { SubmitHandler } from 'react-hook-form'; -import { useForm, FormProvider } from 'react-hook-form'; - -import type { ContractMethodCallResult } from '../types'; -import type { ResultComponentProps } from './types'; -import type { SmartContractMethod, SmartContractMethodInput } from 'types/api/contract'; - -import config from 'configs/app'; -import * as mixpanel from 'lib/mixpanel/index'; - -import ContractMethodFieldInput from './ContractMethodFieldInput'; -import ContractMethodFieldInputArray from './ContractMethodFieldInputArray'; -import ContractMethodFieldInputTuple from './ContractMethodFieldInputTuple'; -import ContractMethodFormOutputs from './ContractMethodFormOutputs'; -import { ARRAY_REGEXP, transformFormDataToMethodArgs } from './utils'; -import type { ContractMethodFormFields } from './utils'; - -interface Props { - data: T; - onSubmit: (data: T, args: Array) => Promise>; - resultComponent: (props: ResultComponentProps) => JSX.Element | null; - methodType: 'read' | 'write'; -} - -const ContractMethodForm = ({ data, onSubmit, resultComponent: ResultComponent, methodType }: Props) => { - - const [ result, setResult ] = React.useState>(); - const [ isLoading, setLoading ] = React.useState(false); - - const formApi = useForm({ - mode: 'all', - shouldUnregister: true, - }); - - const onFormSubmit: SubmitHandler = React.useCallback(async(formData) => { - const args = transformFormDataToMethodArgs(formData); - - 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': methodType === 'write' ? 'Write' : 'Read', - 'Method name': 'name' in data ? data.name : 'Fallback', - }); - }); - }, [ data, methodType, onSubmit ]); - - const handleTxSettle = React.useCallback(() => { - setLoading(false); - }, []); - - const handleFormChange = React.useCallback(() => { - result && setResult(undefined); - }, [ result ]); - - 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 outputs = 'outputs' in data && data.outputs ? data.outputs : []; - - return ( - - - - - { inputs.map((input, index) => { - if (input.components && input.type === 'tuple') { - return ; - } - - const arrayMatch = input.type.match(ARRAY_REGEXP); - if (arrayMatch) { - return ; - } - - return ; - }) } - - - - - { methodType === 'read' && } - { result && } - - ); -}; - -export default React.memo(ContractMethodForm) as typeof ContractMethodForm; diff --git a/ui/address/contract/methodForm/__screenshots__/ContractMethodForm.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png b/ui/address/contract/methodForm/__screenshots__/ContractMethodForm.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png deleted file mode 100644 index 0efe77db56..0000000000 Binary files a/ui/address/contract/methodForm/__screenshots__/ContractMethodForm.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png and /dev/null differ diff --git a/ui/address/contract/methodForm/__screenshots__/ContractMethodForm.pw.tsx_default_base-view-mobile-dark-mode-1.png b/ui/address/contract/methodForm/__screenshots__/ContractMethodForm.pw.tsx_default_base-view-mobile-dark-mode-1.png deleted file mode 100644 index c5bc0169c9..0000000000 Binary files a/ui/address/contract/methodForm/__screenshots__/ContractMethodForm.pw.tsx_default_base-view-mobile-dark-mode-1.png and /dev/null differ diff --git a/ui/address/contract/methodForm/__screenshots__/ContractMethodForm.pw.tsx_mobile_base-view-mobile-dark-mode-1.png b/ui/address/contract/methodForm/__screenshots__/ContractMethodForm.pw.tsx_mobile_base-view-mobile-dark-mode-1.png deleted file mode 100644 index c386ad9a29..0000000000 Binary files a/ui/address/contract/methodForm/__screenshots__/ContractMethodForm.pw.tsx_mobile_base-view-mobile-dark-mode-1.png and /dev/null differ diff --git a/ui/address/contract/methodForm/types.ts b/ui/address/contract/methodForm/types.ts deleted file mode 100644 index 845d6d3621..0000000000 --- a/ui/address/contract/methodForm/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { ContractMethodCallResult } from '../types'; -import type { SmartContractMethod } from 'types/api/contract'; - -export interface ResultComponentProps { - item: T; - result: ContractMethodCallResult; - onSettle: () => void; -} diff --git a/ui/address/contract/methodForm/useArgTypeMatchInt.tsx b/ui/address/contract/methodForm/useArgTypeMatchInt.tsx deleted file mode 100644 index bb3f375e7c..0000000000 --- a/ui/address/contract/methodForm/useArgTypeMatchInt.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import type { SmartContractMethodArgType } from 'types/api/contract'; - -import { INT_REGEXP, getIntBoundaries } from './utils'; - -interface Params { - argType: SmartContractMethodArgType; -} - -export interface MatchInt { - isUnsigned: boolean; - power: string; - min: number; - max: number; -} - -export default function useArgTypeMatchInt({ argType }: Params): MatchInt | null { - 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: Boolean(isUnsigned), power, min, max }; -} diff --git a/ui/address/contract/methodForm/utils.ts b/ui/address/contract/methodForm/utils.ts deleted file mode 100644 index be8edb5324..0000000000 --- a/ui/address/contract/methodForm/utils.ts +++ /dev/null @@ -1,66 +0,0 @@ -import _set from 'lodash/set'; - -import type { SmartContractMethodInput } from 'types/api/contract'; - -export type ContractMethodFormFields = Record; - -export const INT_REGEXP = /^(u)?int(\d+)?$/i; - -export const BYTES_REGEXP = /^bytes(\d+)?$/i; - -export const ARRAY_REGEXP = /^(.*)\[(\d*)\]$/; - -export const getIntBoundaries = (power: number, isUnsigned: boolean) => { - const maxUnsigned = 2 ** power; - const max = isUnsigned ? maxUnsigned - 1 : maxUnsigned / 2 - 1; - const min = isUnsigned ? 0 : -maxUnsigned / 2; - return [ min, max ]; -}; - -export const formatBooleanValue = (value: string) => { - const formattedValue = value.toLowerCase(); - - switch (formattedValue) { - case 'true': - case '1': { - return 'true'; - } - - case 'false': - case '0': { - return 'false'; - } - - default: - return; - } -}; - -export function transformFormDataToMethodArgs(formData: ContractMethodFormFields) { - const result: Array = []; - - for (const field in formData) { - const value = formData[field]; - if (value !== undefined) { - _set(result, field.replaceAll(':', '.'), value); - } - } - - return filterOurEmptyItems(result); -} - -function filterOurEmptyItems(array: Array): Array { - // The undefined value may occur in two cases: - // 1. When an optional form field is left blank by the user. - // The only optional field is the native coin value, which is safely handled in the form submit handler. - // 2. When the user adds and removes items from a field array. - // In this scenario, empty items need to be filtered out to maintain the correct sequence of arguments. - return array - .map((item) => Array.isArray(item) ? filterOurEmptyItems(item) : item) - .filter((item) => item !== undefined); -} - -export function getFieldLabel(input: SmartContractMethodInput, isRequired?: boolean) { - const name = input.name || input.internalType || ''; - return `${ name } (${ input.type })${ isRequired ? '*' : '' }`; -} diff --git a/ui/address/contract/specs/ContractCode.tsx b/ui/address/contract/specs/ContractCode.tsx new file mode 100644 index 0000000000..ac2ee1305e --- /dev/null +++ b/ui/address/contract/specs/ContractCode.tsx @@ -0,0 +1,16 @@ +import { useRouter } from 'next/router'; + +import useApiQuery from 'lib/api/useApiQuery'; +import useContractTabs from 'lib/hooks/useContractTabs'; +import getQueryParamString from 'lib/router/getQueryParamString'; + +const ContractCode = () => { + const router = useRouter(); + const hash = getQueryParamString(router.query.hash); + const addressQuery = useApiQuery('address', { pathParams: { hash } }); + const { tabs } = useContractTabs(addressQuery.data, false); + const content = tabs.find(({ id }) => id === 'contract_code')?.component; + return content ?? null; +}; + +export default ContractCode; diff --git a/ui/address/contract/types.ts b/ui/address/contract/types.ts deleted file mode 100644 index 26753dcbf1..0000000000 --- a/ui/address/contract/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { SmartContractQueryMethodRead, SmartContractMethod, SmartContractReadMethod } from 'types/api/contract'; - -import type { ResourceError } from 'lib/api/resources'; - -export type MethodFormFields = Record>; -export type MethodFormFieldsFormatted = Record; - -export type MethodArgType = string | boolean | Array; - -export type ContractMethodReadResult = SmartContractQueryMethodRead | ResourceError; - -export type ContractMethodWriteResult = Error | { hash: `0x${ string }` | undefined } | undefined; - -export type ContractMethodCallResult = - T extends SmartContractReadMethod ? ContractMethodReadResult : ContractMethodWriteResult; diff --git a/ui/address/contract/useContractAbi.tsx b/ui/address/contract/useContractAbi.tsx deleted file mode 100644 index d2bd337951..0000000000 --- a/ui/address/contract/useContractAbi.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useQueryClient } from '@tanstack/react-query'; -import type { Abi } from 'abitype'; -import React from 'react'; - -import type { Address } from 'types/api/address'; - -import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; - -interface Params { - addressHash?: string; - isProxy?: boolean; - isCustomAbi?: boolean; -} - -export default function useContractAbi({ addressHash, isProxy, isCustomAbi }: Params): Abi | undefined { - const queryClient = useQueryClient(); - - const { data: contractInfo } = useApiQuery('contract', { - pathParams: { hash: addressHash }, - queryOptions: { - enabled: Boolean(addressHash), - refetchOnMount: false, - }, - }); - - const addressInfo = queryClient.getQueryData
(getResourceKey('address', { - pathParams: { hash: addressHash }, - })); - - const { data: proxyInfo } = useApiQuery('contract', { - pathParams: { hash: addressInfo?.implementation_address || '' }, - queryOptions: { - enabled: Boolean(addressInfo?.implementation_address), - refetchOnMount: false, - }, - }); - - const { data: customInfo } = useApiQuery('contract_methods_write', { - pathParams: { hash: addressHash }, - queryParams: { is_custom_abi: 'true' }, - queryOptions: { - enabled: Boolean(addressInfo?.has_custom_methods_write), - refetchOnMount: false, - }, - }); - - return React.useMemo(() => { - if (isProxy) { - return proxyInfo?.abi ?? undefined; - } - - if (isCustomAbi) { - return customInfo as Abi; - } - - return contractInfo?.abi ?? undefined; - }, [ contractInfo?.abi, customInfo, isCustomAbi, isProxy, proxyInfo?.abi ]); -} diff --git a/ui/address/contract/useWatchAccount.tsx b/ui/address/contract/useWatchAccount.tsx deleted file mode 100644 index d4035e6ce5..0000000000 --- a/ui/address/contract/useWatchAccount.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { watchAccount, getAccount } from '@wagmi/core'; -import React from 'react'; - -export function getWalletAccount() { - try { - return getAccount(); - } catch (error) { - return null; - } -} - -export default function useWatchAccount() { - const [ account, setAccount ] = React.useState(getWalletAccount()); - - React.useEffect(() => { - if (!account) { - return; - } - - return watchAccount(setAccount); - }, [ account ]); - - return account; -} diff --git a/ui/address/contract/utils.test.ts b/ui/address/contract/utils.test.ts deleted file mode 100644 index 47e12d3b67..0000000000 --- a/ui/address/contract/utils.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { prepareAbi } from './utils'; - -describe('function prepareAbi()', () => { - const commonAbi = [ - { - inputs: [ - { internalType: 'address', name: '_pool', type: 'address' }, - { internalType: 'address', name: '_token', type: 'address' }, - { internalType: 'uint256', name: '_denominator', type: 'uint256' }, - ], - stateMutability: 'nonpayable' as const, - type: 'constructor' as const, - }, - { - anonymous: false, - inputs: [ - { indexed: false, internalType: 'uint256[]', name: 'indices', type: 'uint256[]' }, - ], - name: 'CompleteDirectDepositBatch', - type: 'event' as const, - }, - { - inputs: [ - { internalType: 'address', name: '_fallbackUser', type: 'address' }, - { internalType: 'string', name: '_zkAddress', type: 'string' }, - ], - name: 'directNativeDeposit', - outputs: [ - { internalType: 'uint256', name: '', type: 'uint256' }, - ], - stateMutability: 'payable' as const, - type: 'function' as const, - }, - ]; - - const method = { - inputs: [ - { internalType: 'address' as const, name: '_fallbackUser', type: 'address' as const }, - { internalType: 'string' as const, name: '_zkAddress', type: 'string' as const }, - ], - name: 'directNativeDeposit', - outputs: [ - { internalType: 'uint256' as const, name: '', type: 'uint256' as const }, - ], - stateMutability: 'payable' as const, - type: 'function' as const, - constant: false, - payable: true, - method_id: '0x2e0e2d3e', - }; - - it('if there is only one method with provided name, does nothing', () => { - const abi = prepareAbi(commonAbi, method); - expect(abi).toHaveLength(commonAbi.length); - }); - - it('if there are two or more methods with the same name and inputs length, filters out those which input types are not matched', () => { - const abi = prepareAbi([ - ...commonAbi, - { - inputs: [ - { internalType: 'address', name: '_fallbackUser', type: 'address' }, - { internalType: 'bytes', name: '_rawZkAddress', type: 'bytes' }, - ], - name: 'directNativeDeposit', - outputs: [ - { internalType: 'uint256', name: '', type: 'uint256' }, - ], - stateMutability: 'payable', - type: 'function', - }, - ], method); - - expect(abi).toHaveLength(commonAbi.length); - - const item = abi.find((item) => 'name' in item ? item.name === method.name : false); - expect(item).toEqual(commonAbi[2]); - }); - - it('if there are two or more methods with the same name and different inputs length, filters out those which inputs are not matched', () => { - const abi = prepareAbi([ - ...commonAbi, - { - inputs: [ - { internalType: 'address', name: '_fallbackUser', type: 'address' }, - ], - name: 'directNativeDeposit', - outputs: [ - { internalType: 'uint256', name: '', type: 'uint256' }, - ], - stateMutability: 'payable', - type: 'function', - }, - ], method); - - expect(abi).toHaveLength(commonAbi.length); - - const item = abi.find((item) => 'name' in item ? item.name === method.name : false); - expect(item).toEqual(commonAbi[2]); - }); -}); diff --git a/ui/address/contract/utils.ts b/ui/address/contract/utils.ts deleted file mode 100644 index 8fa04e839c..0000000000 --- a/ui/address/contract/utils.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { Abi } from 'abitype'; - -import type { SmartContractWriteMethod } from 'types/api/contract'; - -export const getNativeCoinValue = (value: unknown) => { - if (typeof value !== 'string') { - return BigInt(0); - } - - return BigInt(value); -}; - -export function prepareAbi(abi: Abi, item: SmartContractWriteMethod): Abi { - if ('name' in item) { - const hasMethodsWithSameName = abi.filter((abiItem) => 'name' in abiItem ? abiItem.name === item.name : false).length > 1; - - if (hasMethodsWithSameName) { - return abi.filter((abiItem) => { - if (!('name' in abiItem)) { - return true; - } - - if (abiItem.name !== item.name) { - return true; - } - - if (abiItem.inputs.length !== item.inputs.length) { - return false; - } - - return abiItem.inputs.every(({ name, type }) => { - const itemInput = item.inputs.find((input) => input.name === name); - return Boolean(itemInput) && itemInput?.type === type; - }); - }); - } - } - - return abi; -} diff --git a/ui/address/ensDomains/AddressEnsDomains.tsx b/ui/address/ensDomains/AddressEnsDomains.tsx index c7099ee0d1..a67612535d 100644 --- a/ui/address/ensDomains/AddressEnsDomains.tsx +++ b/ui/address/ensDomains/AddressEnsDomains.tsx @@ -1,4 +1,18 @@ -import { Button, chakra, Flex, Grid, Hide, Popover, PopoverBody, PopoverContent, PopoverTrigger, Show, Skeleton, useDisclosure } from '@chakra-ui/react'; +import { + Box, + Button, + Flex, + Grid, + Hide, + Popover, + PopoverBody, + PopoverContent, + PopoverTrigger, + Show, + Skeleton, + useDisclosure, + chakra, +} from '@chakra-ui/react'; import _clamp from 'lodash/clamp'; import React from 'react'; @@ -12,6 +26,7 @@ import dayjs from 'lib/date/dayjs'; import EnsEntity from 'ui/shared/entities/ens/EnsEntity'; import IconSvg from 'ui/shared/IconSvg'; import LinkInternal from 'ui/shared/LinkInternal'; +import PopoverTriggerTooltip from 'ui/shared/PopoverTriggerTooltip'; interface Props { addressHash: string; @@ -90,37 +105,39 @@ const AddressEnsDomains = ({ addressHash, mainDomainName }: Props) => { return ( - + + + { mainDomain && ( -
+ Primary* { mainDomain.expiry_date && (expires { dayjs(mainDomain.expiry_date).fromNow() }) } -
+ ) } { ownedDomains.length > 0 && (
diff --git a/ui/address/tokenSelect/TokenSelect.pw.tsx b/ui/address/tokenSelect/TokenSelect.pw.tsx index d2a810c981..e58975e540 100644 --- a/ui/address/tokenSelect/TokenSelect.pw.tsx +++ b/ui/address/tokenSelect/TokenSelect.pw.tsx @@ -14,6 +14,7 @@ const ASSET_URL = tokenInfoERC20a.icon_url as string; const TOKENS_ERC20_API_URL = buildApiUrl('address_tokens', { hash: '1' }) + '?type=ERC-20'; const TOKENS_ERC721_API_URL = buildApiUrl('address_tokens', { hash: '1' }) + '?type=ERC-721'; const TOKENS_ER1155_API_URL = buildApiUrl('address_tokens', { hash: '1' }) + '?type=ERC-1155'; +const TOKENS_ER404_API_URL = buildApiUrl('address_tokens', { hash: '1' }) + '?type=ERC-404'; const ADDRESS_API_URL = buildApiUrl('address', { hash: '1' }); const hooksConfig = { router: { @@ -46,6 +47,10 @@ const test = base.extend({ status: 200, body: JSON.stringify(tokensMock.erc1155List), }), { times: 1 }); + await page.route(TOKENS_ER404_API_URL, async(route) => route.fulfill({ + status: 200, + body: JSON.stringify(tokensMock.erc404List), + }), { times: 1 }); use(page); }, @@ -148,7 +153,7 @@ base('long values', async({ mount, page }) => { }), { times: 1 }); await page.route(TOKENS_ERC20_API_URL, async(route) => route.fulfill({ status: 200, - body: JSON.stringify({ items: [ tokensMock.erc20LongSymbol ] }), + body: JSON.stringify({ items: [ tokensMock.erc20LongSymbol, tokensMock.erc20BigAmount ] }), }), { times: 1 }); await page.route(TOKENS_ERC721_API_URL, async(route) => route.fulfill({ status: 200, @@ -158,6 +163,10 @@ base('long values', async({ mount, page }) => { status: 200, body: JSON.stringify({ items: [ tokensMock.erc1155LongId ] }), }), { times: 1 }); + await page.route(TOKENS_ER404_API_URL, async(route) => route.fulfill({ + status: 200, + body: JSON.stringify(tokensMock.erc404List), + }), { times: 1 }); await mount( diff --git a/ui/address/tokenSelect/TokenSelectButton.tsx b/ui/address/tokenSelect/TokenSelectButton.tsx index 67fb5b58f8..773c06569c 100644 --- a/ui/address/tokenSelect/TokenSelectButton.tsx +++ b/ui/address/tokenSelect/TokenSelectButton.tsx @@ -3,6 +3,7 @@ import React from 'react'; import type { FormattedData } from './types'; +import { space } from 'lib/html-entities'; import * as mixpanel from 'lib/mixpanel/index'; import IconSvg from 'ui/shared/IconSvg'; @@ -42,7 +43,16 @@ const TokenSelectButton = ({ isOpen, isLoading, onClick, data }: Props, ref: Rea > { prefix }{ num } - ({ prefix }${ usd.toFormat(2) }) + + { space }({ prefix }${ usd.toFormat(2) }) + { isLoading && !isOpen && } diff --git a/ui/address/tokenSelect/TokenSelectDesktop.tsx b/ui/address/tokenSelect/TokenSelectDesktop.tsx index 629fa5fda3..858bb5a443 100644 --- a/ui/address/tokenSelect/TokenSelectDesktop.tsx +++ b/ui/address/tokenSelect/TokenSelectDesktop.tsx @@ -1,4 +1,4 @@ -import { useColorModeValue, Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react'; +import { Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react'; import React from 'react'; import type { FormattedData } from './types'; @@ -15,8 +15,6 @@ interface Props { const TokenSelectDesktop = ({ data, isLoading }: Props) => { const { isOpen, onToggle, onClose } = useDisclosure(); - const bgColor = useColorModeValue('white', 'gray.900'); - const result = useTokenSelect(data); return ( @@ -25,7 +23,7 @@ const TokenSelectDesktop = ({ data, isLoading }: Props) => { - + diff --git a/ui/address/tokenSelect/TokenSelectItem.tsx b/ui/address/tokenSelect/TokenSelectItem.tsx index bd9b9ea007..fc7d6cde27 100644 --- a/ui/address/tokenSelect/TokenSelectItem.tsx +++ b/ui/address/tokenSelect/TokenSelectItem.tsx @@ -1,9 +1,10 @@ -import { chakra, Flex, Text, useColorModeValue } from '@chakra-ui/react'; +import { chakra, Flex, useColorModeValue } from '@chakra-ui/react'; import BigNumber from 'bignumber.js'; import React from 'react'; import { route } from 'nextjs-routes'; +import getCurrencyValue from 'lib/getCurrencyValue'; import TokenEntity from 'ui/shared/entities/token/TokenEntity'; import LinkInternal from 'ui/shared/LinkInternal'; import TruncatedValue from 'ui/shared/TruncatedValue'; @@ -45,6 +46,25 @@ const TokenSelectItem = ({ data }: Props) => { ); } + case 'ERC-404': { + return ( + <> + { data.token_id !== null && ( + + #{ data.token_id || 0 } + + ) } + { data.value !== null && ( + + { data.token.decimals ? + getCurrencyValue({ value: data.value, decimals: data.token.decimals, accuracy: 2 }).valueStr : + BigNumber(data.value).toFormat() + } + + ) } + + ); + } } })(); @@ -62,7 +82,7 @@ const TokenSelectItem = ({ data }: Props) => { _hover={{ bgColor: useColorModeValue('blue.50', 'gray.800'), }} - color="initial" + color="unset" fontSize="sm" href={ url } > @@ -73,8 +93,11 @@ const TokenSelectItem = ({ data }: Props) => { noCopy noLink fontWeight={ 700 } + mr={ 2 } /> - { data.usd && ${ data.usd.toFormat(2) } } + { data.usd && ( + + ) } { secondRow } diff --git a/ui/address/tokenSelect/TokenSelectMenu.tsx b/ui/address/tokenSelect/TokenSelectMenu.tsx index d247a0e3cd..22f87950ed 100644 --- a/ui/address/tokenSelect/TokenSelectMenu.tsx +++ b/ui/address/tokenSelect/TokenSelectMenu.tsx @@ -16,12 +16,13 @@ interface Props { searchTerm: string; erc20sort: Sort; erc1155sort: Sort; + erc404sort: Sort; filteredData: FormattedData; onInputChange: (event: ChangeEvent) => void; onSortClick: (event: React.SyntheticEvent) => void; } -const TokenSelectMenu = ({ erc20sort, erc1155sort, filteredData, onInputChange, onSortClick, searchTerm }: Props) => { +const TokenSelectMenu = ({ erc20sort, erc1155sort, erc404sort, filteredData, onInputChange, onSortClick, searchTerm }: Props) => { const searchIconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600'); const inputBorderColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.200'); @@ -43,15 +44,17 @@ const TokenSelectMenu = ({ erc20sort, erc1155sort, filteredData, onInputChange, { Object.entries(filteredData).sort(sortTokenGroups).map(([ tokenType, tokenInfo ]) => { - if (tokenInfo.items.length === 0) { return null; } const type = tokenType as TokenType; - const arrowTransform = (type === 'ERC-1155' && erc1155sort === 'desc') || (type === 'ERC-20' && erc20sort === 'desc') ? - 'rotate(90deg)' : - 'rotate(-90deg)'; + const arrowTransform = + (type === 'ERC-1155' && erc1155sort === 'desc') || + (type === 'ERC-404' && erc404sort === 'desc') || + (type === 'ERC-20' && erc20sort === 'desc') ? + 'rotate(90deg)' : + 'rotate(-90deg)'; const sortDirection: Sort = (() => { switch (type) { case 'ERC-1155': @@ -62,7 +65,10 @@ const TokenSelectMenu = ({ erc20sort, erc1155sort, filteredData, onInputChange, return 'desc'; } })(); - const hasSort = type === 'ERC-1155' || (type === 'ERC-20' && tokenInfo.items.some(({ usd }) => usd)); + const hasSort = + (type === 'ERC-404' && tokenInfo.items.some(item => item.value)) || + type === 'ERC-1155' || + (type === 'ERC-20' && tokenInfo.items.some(({ usd }) => usd)); const numPrefix = tokenInfo.isOverflow ? '>' : ''; return ( diff --git a/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_dark-color-mode_base-view-dark-mode-1.png b/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_dark-color-mode_base-view-dark-mode-1.png index 9dba7e43bb..0f13fc0097 100644 Binary files a/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_dark-color-mode_base-view-dark-mode-1.png and b/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_dark-color-mode_base-view-dark-mode-1.png differ diff --git a/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_dark-color-mode_base-view-dark-mode-2.png b/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_dark-color-mode_base-view-dark-mode-2.png index 0079d7d3db..dc1db04181 100644 Binary files a/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_dark-color-mode_base-view-dark-mode-2.png and b/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_dark-color-mode_base-view-dark-mode-2.png differ diff --git a/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_base-view-dark-mode-1.png b/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_base-view-dark-mode-1.png index 6b7fa96751..90b6524154 100644 Binary files a/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_base-view-dark-mode-1.png and b/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_base-view-dark-mode-1.png differ diff --git a/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_base-view-dark-mode-2.png b/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_base-view-dark-mode-2.png index 464e028a1c..df33a35e69 100644 Binary files a/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_base-view-dark-mode-2.png and b/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_base-view-dark-mode-2.png differ diff --git a/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_filter-1.png b/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_filter-1.png index f2151d30c1..97f8b096bc 100644 Binary files a/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_filter-1.png and b/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_filter-1.png differ diff --git a/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_long-values-1.png b/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_long-values-1.png index 523b23617f..1f59eeb768 100644 Binary files a/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_long-values-1.png and b/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_long-values-1.png differ diff --git a/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_mobile-base-view-1.png b/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_mobile-base-view-1.png index 98ab4ea3a3..924dca0642 100644 Binary files a/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_mobile-base-view-1.png and b/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_sort-1.png b/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_sort-1.png index c276436451..56f4a24113 100644 Binary files a/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_sort-1.png and b/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_sort-1.png differ diff --git a/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_sort-2.png b/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_sort-2.png index 10c6465fa7..0638c45f9e 100644 Binary files a/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_sort-2.png and b/ui/address/tokenSelect/__screenshots__/TokenSelect.pw.tsx_default_sort-2.png differ diff --git a/ui/address/tokenSelect/useTokenSelect.ts b/ui/address/tokenSelect/useTokenSelect.ts index da268d248e..95346e25c3 100644 --- a/ui/address/tokenSelect/useTokenSelect.ts +++ b/ui/address/tokenSelect/useTokenSelect.ts @@ -10,6 +10,7 @@ import { filterTokens } from '../utils/tokenUtils'; export default function useTokenSelect(data: FormattedData) { const [ searchTerm, setSearchTerm ] = React.useState(''); const [ erc1155sort, setErc1155Sort ] = React.useState('desc'); + const [ erc404sort, setErc404Sort ] = React.useState('desc'); const [ erc20sort, setErc20Sort ] = React.useState('desc'); const onInputChange = React.useCallback((event: ChangeEvent) => { @@ -21,6 +22,9 @@ export default function useTokenSelect(data: FormattedData) { if (tokenType === 'ERC-1155') { setErc1155Sort((prevValue) => prevValue === 'desc' ? 'asc' : 'desc'); } + if (tokenType === 'ERC-404') { + setErc404Sort((prevValue) => prevValue === 'desc' ? 'asc' : 'desc'); + } if (tokenType === 'ERC-20') { setErc20Sort((prevValue) => prevValue === 'desc' ? 'asc' : 'desc'); } @@ -37,6 +41,7 @@ export default function useTokenSelect(data: FormattedData) { searchTerm, erc20sort, erc1155sort, + erc404sort, onInputChange, onSortClick, data, diff --git a/ui/address/tokens/AddressCollections.tsx b/ui/address/tokens/AddressCollections.tsx index 2be76c5c87..caf185a30c 100644 --- a/ui/address/tokens/AddressCollections.tsx +++ b/ui/address/tokens/AddressCollections.tsx @@ -64,7 +64,7 @@ const AddressCollections = ({ collectionsQuery, address, hasActiveFilters }: Pro ; +} + +const ERC1155Tokens = ({ tokensQuery }: Props) => { + const isMobile = useIsMobile(); + + const { isError, isPlaceholderData, data, pagination } = tokensQuery; + + const actionBar = isMobile && pagination.isVisible && ( + + + + ); + + const content = data?.items ? ( + + { data.items.map((item, index) => { + const key = item.token.address + '_' + (item.token_instance?.id && !isPlaceholderData ? `id_${ item.token_instance?.id }` : `index_${ index }`); + + return ( + + ); + }) } + + ) : null; + + return ( + + ); +}; + +export default ERC1155Tokens; diff --git a/ui/address/tokens/ERC20TokensListItem.tsx b/ui/address/tokens/ERC20TokensListItem.tsx index 3f5c79893c..6f9e9845af 100644 --- a/ui/address/tokens/ERC20TokensListItem.tsx +++ b/ui/address/tokens/ERC20TokensListItem.tsx @@ -46,17 +46,17 @@ const ERC20TokensListItem = ({ token, value, isLoading }: Props) => { ) } - + Quantity - + { tokenQuantity } { tokenValue !== undefined && ( - + Value - - { tokenValue } + + ${ tokenValue } ) } diff --git a/ui/address/tokens/ERC721Tokens.tsx b/ui/address/tokens/ERC721Tokens.tsx new file mode 100644 index 0000000000..8877703844 --- /dev/null +++ b/ui/address/tokens/ERC721Tokens.tsx @@ -0,0 +1,52 @@ +import { Show, Hide } from '@chakra-ui/react'; +import React from 'react'; + +import useIsMobile from 'lib/hooks/useIsMobile'; +import ActionBar from 'ui/shared/ActionBar'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import Pagination from 'ui/shared/pagination/Pagination'; +import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages'; + +import ERC721TokensListItem from './ERC721TokensListItem'; +import ERC721TokensTable from './ERC721TokensTable'; + +type Props = { + tokensQuery: QueryWithPagesResult<'address_tokens'>; +} + +const ERC721Tokens = ({ tokensQuery }: Props) => { + const isMobile = useIsMobile(); + + const { isError, isPlaceholderData, data, pagination } = tokensQuery; + + const actionBar = isMobile && pagination.isVisible && ( + + + + ); + + const content = data?.items ? ( + <> + + { data.items.map((item, index) => ( + + )) } + ) : null; + + return ( + + ); + +}; + +export default ERC721Tokens; diff --git a/ui/address/tokens/ERC721TokensListItem.tsx b/ui/address/tokens/ERC721TokensListItem.tsx new file mode 100644 index 0000000000..66cf9e3911 --- /dev/null +++ b/ui/address/tokens/ERC721TokensListItem.tsx @@ -0,0 +1,48 @@ +import { Flex, HStack, Skeleton } from '@chakra-ui/react'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { AddressTokenBalance } from 'types/api/address'; + +import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet'; +import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import TokenEntityWithAddressFilter from 'ui/shared/entities/token/TokenEntityWithAddressFilter'; +import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; + +type Props = AddressTokenBalance & { isLoading: boolean}; + +const ERC721TokensListItem = ({ token, value, isLoading }: Props) => { + const router = useRouter(); + + const hash = router.query.hash?.toString() || ''; + + return ( + + + + + + + + Quantity + + { value } + + + + ); +}; + +export default ERC721TokensListItem; diff --git a/ui/address/tokens/ERC721TokensTable.tsx b/ui/address/tokens/ERC721TokensTable.tsx new file mode 100644 index 0000000000..9490e2cb58 --- /dev/null +++ b/ui/address/tokens/ERC721TokensTable.tsx @@ -0,0 +1,35 @@ +import { Table, Tbody, Tr, Th } from '@chakra-ui/react'; +import React from 'react'; + +import type { AddressTokenBalance } from 'types/api/address'; + +import { default as Thead } from 'ui/shared/TheadSticky'; + +import ERC721TokensTableItem from './ERC721TokensTableItem'; + +interface Props { + data: Array; + top: number; + isLoading: boolean; +} + +const ERC721TokensTable = ({ data, top, isLoading }: Props) => { + return ( + + + + + + + + + + { data.map((item, index) => ( + + )) } + +
AssetContract addressQuantity
+ ); +}; + +export default ERC721TokensTable; diff --git a/ui/address/tokens/ERC721TokensTableItem.tsx b/ui/address/tokens/ERC721TokensTableItem.tsx new file mode 100644 index 0000000000..fdb8c04611 --- /dev/null +++ b/ui/address/tokens/ERC721TokensTableItem.tsx @@ -0,0 +1,53 @@ +import { Tr, Td, Flex, Skeleton } from '@chakra-ui/react'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { AddressTokenBalance } from 'types/api/address'; + +import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet'; +import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import TokenEntityWithAddressFilter from 'ui/shared/entities/token/TokenEntityWithAddressFilter'; + +type Props = AddressTokenBalance & { isLoading: boolean}; + +const ERC721TokensTableItem = ({ + token, + value, + isLoading, +}: Props) => { + const router = useRouter(); + + const hash = router.query.hash?.toString() || ''; + + return ( + + + + + + + + + + + + + { value } + + + + ); +}; + +export default React.memo(ERC721TokensTableItem); diff --git a/ui/address/tokens/NFTItem.tsx b/ui/address/tokens/NFTItem.tsx index 50a33ab73b..9ed44b2248 100644 --- a/ui/address/tokens/NFTItem.tsx +++ b/ui/address/tokens/NFTItem.tsx @@ -5,6 +5,7 @@ import type { AddressNFT } from 'types/api/address'; import { route } from 'nextjs-routes'; +import getCurrencyValue from 'lib/getCurrencyValue'; import NftEntity from 'ui/shared/entities/nft/NftEntity'; import TokenEntity from 'ui/shared/entities/token/TokenEntity'; import NftMedia from 'ui/shared/nft/NftMedia'; @@ -14,6 +15,7 @@ import NFTItemContainer from './NFTItemContainer'; type Props = AddressNFT & { isLoading: boolean; withTokenLink?: boolean }; const NFTItem = ({ token, value, isLoading, withTokenLink, ...tokenInstance }: Props) => { + const valueResult = token.decimals && value ? getCurrencyValue({ value, decimals: token.decimals, accuracy: 2 }).valueStr : value; const tokenInstanceLink = tokenInstance.id ? route({ pathname: '/token/[hash]/instance/[id]', query: { hash: token.address, id: tokenInstance.id } }) : undefined; @@ -26,17 +28,23 @@ const NFTItem = ({ token, value, isLoading, withTokenLink, ...tokenInstance }: P - + ID# - - { Number(value) > 1 && Qty { value } } + + { valueResult && ( + + Qty + { valueResult } + + ) } { withTokenLink && ( diff --git a/ui/address/tokens/TokenBalancesItem.tsx b/ui/address/tokens/TokenBalancesItem.tsx index 7bf61d1860..fcd3c08752 100644 --- a/ui/address/tokens/TokenBalancesItem.tsx +++ b/ui/address/tokens/TokenBalancesItem.tsx @@ -9,10 +9,10 @@ const TokenBalancesItem = ({ name, value, isLoading }: {name: string; value: str return ( - + { name } - { value } + { value } ); diff --git a/ui/address/utils/tokenUtils.ts b/ui/address/utils/tokenUtils.ts index 67f2e52c2e..b204166a18 100644 --- a/ui/address/utils/tokenUtils.ts +++ b/ui/address/utils/tokenUtils.ts @@ -22,13 +22,13 @@ export interface TokenSelectDataItem { type TokenGroup = [string, TokenSelectDataItem]; -const TOKEN_GROUPS_ORDER: Array = [ 'ERC-20', 'ERC-721', 'ERC-1155' ]; +const TOKEN_GROUPS_ORDER: Array = [ 'ERC-20', 'ERC-721', 'ERC-1155', 'ERC-404' ]; export const sortTokenGroups = (groupA: TokenGroup, groupB: TokenGroup) => { return TOKEN_GROUPS_ORDER.indexOf(groupA[0] as TokenType) > TOKEN_GROUPS_ORDER.indexOf(groupB[0] as TokenType) ? 1 : -1; }; -const sortErc1155Tokens = (sort: Sort) => (dataA: AddressTokenBalance, dataB: AddressTokenBalance) => { +const sortErc1155or404Tokens = (sort: Sort) => (dataA: AddressTokenBalance, dataB: AddressTokenBalance) => { if (dataA.value === dataB.value) { return 0; } @@ -38,6 +38,7 @@ const sortErc1155Tokens = (sort: Sort) => (dataA: AddressTokenBalance, dataB: Ad return Number(dataA.value) > Number(dataB.value) ? 1 : -1; }; + const sortErc20Tokens = (sort: Sort) => (dataA: TokenEnhancedData, dataB: TokenEnhancedData) => { if (!dataA.usd && !dataB.usd) { return 0; @@ -63,7 +64,8 @@ const sortErc721Tokens = () => () => 0; export const sortingFns = { 'ERC-20': sortErc20Tokens, 'ERC-721': sortErc721Tokens, - 'ERC-1155': sortErc1155Tokens, + 'ERC-1155': sortErc1155or404Tokens, + 'ERC-404': sortErc1155or404Tokens, }; export const filterTokens = (searchTerm: string) => ({ token }: AddressTokenBalance) => { diff --git a/ui/address/utils/useAddressQuery.ts b/ui/address/utils/useAddressQuery.ts index 141ce56d72..e9569e7330 100644 --- a/ui/address/utils/useAddressQuery.ts +++ b/ui/address/utils/useAddressQuery.ts @@ -74,14 +74,8 @@ export default function useAddressQuery({ hash }: Params): AddressQuery { creation_tx_hash: null, exchange_rate: null, ens_domain_name: null, - has_custom_methods_read: false, - has_custom_methods_write: false, has_decompiled_code: false, has_logs: false, - has_methods_read: false, - has_methods_read_proxy: false, - has_methods_write: false, - has_methods_write_proxy: false, has_token_transfers: false, has_tokens: false, has_validated_blocks: false, diff --git a/ui/address/utils/useFetchTokens.ts b/ui/address/utils/useFetchTokens.ts index 9b5bcda583..53055d9ced 100644 --- a/ui/address/utils/useFetchTokens.ts +++ b/ui/address/utils/useFetchTokens.ts @@ -36,6 +36,11 @@ export default function useFetchTokens({ hash }: Props) { queryParams: { type: 'ERC-1155' }, queryOptions: { enabled: Boolean(hash), refetchOnMount: false }, }); + const erc404query = useApiQuery('address_tokens', { + pathParams: { hash }, + queryParams: { type: 'ERC-404' }, + queryOptions: { enabled: Boolean(hash), refetchOnMount: false }, + }); const queryClient = useQueryClient(); @@ -78,6 +83,10 @@ export default function useFetchTokens({ hash }: Props) { updateTokensData('ERC-1155', payload); }, [ updateTokensData ]); + const handleTokenBalancesErc404Message: SocketMessage.AddressTokenBalancesErc1155['handler'] = React.useCallback((payload) => { + updateTokensData('ERC-404', payload); + }, [ updateTokensData ]); + const channel = useSocketChannel({ topic: `addresses:${ hash?.toLowerCase() }`, isDisabled: Boolean(hash) && (erc20query.isPlaceholderData || erc721query.isPlaceholderData || erc1155query.isPlaceholderData), @@ -98,6 +107,11 @@ export default function useFetchTokens({ hash }: Props) { event: 'updated_token_balances_erc_1155', handler: handleTokenBalancesErc1155Message, }); + useSocketMessage({ + channel, + event: 'updated_token_balances_erc_404', + handler: handleTokenBalancesErc404Message, + }); const data = React.useMemo(() => { return { @@ -113,12 +127,16 @@ export default function useFetchTokens({ hash }: Props) { items: erc1155query.data?.items.map(calculateUsdValue) || [], isOverflow: Boolean(erc1155query.data?.next_page_params), }, + 'ERC-404': { + items: erc404query.data?.items.map(calculateUsdValue) || [], + isOverflow: Boolean(erc1155query.data?.next_page_params), + }, }; - }, [ erc1155query.data, erc20query.data, erc721query.data ]); + }, [ erc1155query.data, erc20query.data, erc721query.data, erc404query.data ]); return { - isPending: erc20query.isPending || erc721query.isPending || erc1155query.isPending, - isError: erc20query.isError || erc721query.isError || erc1155query.isError, + isPending: erc20query.isPending || erc721query.isPending || erc1155query.isPending || erc404query.isPending, + isError: erc20query.isError || erc721query.isError || erc1155query.isError || erc404query.isError, data, }; } diff --git a/ui/addressVerification/AddressVerificationModal.tsx b/ui/addressVerification/AddressVerificationModal.tsx index b70d472dc7..c81cb10e6c 100644 --- a/ui/addressVerification/AddressVerificationModal.tsx +++ b/ui/addressVerification/AddressVerificationModal.tsx @@ -100,7 +100,7 @@ const AddressVerificationModal = ({ defaultAddress, isOpen, onClose, onSubmit, o { stepIndex !== 0 && ( - + ) } { step.title } diff --git a/ui/addressVerification/steps/AddressVerificationStepSignature.tsx b/ui/addressVerification/steps/AddressVerificationStepSignature.tsx index 22a80fef28..9db7f2dd97 100644 --- a/ui/addressVerification/steps/AddressVerificationStepSignature.tsx +++ b/ui/addressVerification/steps/AddressVerificationStepSignature.tsx @@ -80,15 +80,7 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre const onSubmit = handleSubmit(onFormSubmit); - const { signMessage, isLoading: isSigning } = useSignMessage({ - onSuccess: (data) => { - setValue('signature', data); - onSubmit(); - }, - onError: (error) => { - return setError('root', { type: 'SIGNING_FAIL', message: (error as Error)?.message || 'Oops! Something went wrong' }); - }, - }); + const { signMessage, isPending: isSigning } = useSignMessage(); const handleSignMethodChange = React.useCallback((value: typeof signMethod) => { setSignMethod(value); @@ -108,8 +100,16 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre } const message = getValues('message'); - signMessage({ message }); - }, [ clearErrors, isConnected, getValues, signMessage, setError ]); + signMessage({ message }, { + onSuccess: (data) => { + setValue('signature', data); + onSubmit(); + }, + onError: (error) => { + return setError('root', { type: 'SIGNING_FAIL', message: (error as Error)?.message || 'Oops! Something went wrong' }); + }, + }); + }, [ clearErrors, isConnected, getValues, signMessage, setError, setValue, onSubmit ]); const handleManualSignClick = React.useCallback(() => { clearErrors('root'); @@ -219,7 +219,7 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre { !noWeb3Provider && ( Sign via Web3 wallet - Sign manually + Sign manually ) } { signMethod === 'manual' && } diff --git a/ui/blob/BlobData.pw.tsx b/ui/blob/BlobData.pw.tsx new file mode 100644 index 0000000000..487ad0e69c --- /dev/null +++ b/ui/blob/BlobData.pw.tsx @@ -0,0 +1,53 @@ +import { test, expect } from '@playwright/experimental-ct-react'; +import React from 'react'; + +import TestApp from 'playwright/TestApp'; + +import BlobData from './BlobData'; +import imageBlobWithZeroesBytes from './image_with_zeroes.blob'; + +test.use({ viewport: { width: 500, height: 300 } }); + +test('text', async({ mount }) => { + // eslint-disable-next-line max-len + const data = '0xE2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A280E2A3A4E2A1B6E2A0BFE2A0BFE2A0B7E2A3B6E2A384E2A080E2A080E2A080E2A080E2A0800AE2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A3B0E2A1BFE2A081E2A080E2A080E2A280E2A380E2A180E2A099E2A3B7E2A180E2A080E2A080E2A0800AE2A080E2A080E2A080E2A180E2A080E2A080E2A080E2A080E2A080E2A2A0E2A3BFE2A081E2A080E2A080E2A080E2A098E2A0BFE2A083E2A080E2A2B8E2A3BFE2A3BFE2A3BFE2A3BF0AE2A080E2A3A0E2A1BFE2A09BE2A2B7E2A3A6E2A180E2A080E2A080E2A088E2A3BFE2A184E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A3B8E2A3BFE2A3BFE2A3BFE2A09F0AE2A2B0E2A1BFE2A081E2A080E2A080E2A099E2A2BFE2A3A6E2A3A4E2A3A4E2A3BCE2A3BFE2A384E2A080E2A080E2A080E2A080E2A080E2A2B4E2A19FE2A09BE2A08BE2A081E2A0800AE2A3BFE2A087E2A080E2A080E2A080E2A080E2A080E2A089E2A089E2A089E2A089E2A089E2A081E2A080E2A080E2A080E2A080E2A080E2A088E2A3BFE2A180E2A080E2A080E2A0800AE2A3BFE2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A2B9E2A187E2A080E2A080E2A0800AE2A3BFE2A186E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A3BCE2A187E2A080E2A080E2A0800AE2A0B8E2A3B7E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A2A0E2A1BFE2A080E2A080E2A080E2A0800AE2A080E2A0B9E2A3B7E2A3A4E2A380E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A080E2A380E2A3B0E2A1BFE2A081E2A080E2A080E2A080E2A0800AE2A080E2A080E2A080E2A089E2A099E2A09BE2A0BFE2A0B6E2A3B6E2A3B6E2A3B6E2A3B6E2A3B6E2A0B6E2A0BFE2A09FE2A09BE2A089E2A080E2A080E2A080E2A080E2A080E2A080'; + + const component = await mount( + + + , + ); + + await expect(component).toHaveScreenshot(); + + await component.locator('select').selectOption('UTF-8'); + + await expect(component).toHaveScreenshot(); +}); + +test('image', async({ mount }) => { + // eslint-disable-next-line max-len + const data = '0x89504E470D0A1A0A0000000D494844520000003C0000003C0403000000C8D2C4410000000467414D410000B18F0BFC6105000000017352474200AECE1CE900000027504C54454C69712B6CB02A6CB02B6CB02B6CB02B6CB02B6CB02B6CB02B6CB02B6CB02B6CB02B6CB02B6CB0F4205A540000000C74524E5300ED2F788CD91B99475C09B969CFA99D0000004F7A5458745261772070726F66696C65207479706520697074630000789CE3CA2C2849E6520003230B2E630B1323134B9314031320448034C3640323B35420CBD8D4C8C4CCC41CC407CB8048A04A2E0028950EE32A226D1F0000000970485973000084DF000084DF0195C81C33000000F24944415438CB636000018E983367CE482780D90CDA40F6991D0C4820152472A60ACCE6DA03629F4E40929E03961602B39964C09C0624691B24690E88F48461215D03160903B3D962C01C07842C2758C341A80643B0B40484C3646C6C5C78E6E016171723A8E215262EEE31670E161B1B7731304C05AB155EC08002C0D172E6F80206884DBB50651938CF4003FE0CBA4390E3C56064482F53525252C329CD562A2828283A0197340B22AAB0494332C311FCD2C747A547A58996C69998D8F12745B68DA0846C85331B2CEAE8E8681A81D91F8B348C4605D0527B02A4283FA88026CD05163EAAC0900ED21EC9800EC0C2110C002BBA9FE999B920330000000049454E44AE426082'; + + const component = await mount( + + + , + ); + + await expect(component).toHaveScreenshot(); + + await component.locator('select').selectOption('Base64'); + + await expect(component).toHaveScreenshot(); +}); + +test('image blob with zeroes bytes', async({ mount }) => { + const component = await mount( + + + , + ); + + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/blob/BlobData.tsx b/ui/blob/BlobData.tsx new file mode 100644 index 0000000000..da7980c84c --- /dev/null +++ b/ui/blob/BlobData.tsx @@ -0,0 +1,135 @@ +import { Flex, GridItem, Select, Skeleton, Button } from '@chakra-ui/react'; +import React from 'react'; + +import * as blobUtils from 'lib/blob'; +import removeNonSignificantZeroBytes from 'lib/blob/removeNonSignificantZeroBytes'; +import bytesToBase64 from 'lib/bytesToBase64'; +import downloadBlob from 'lib/downloadBlob'; +import hexToBase64 from 'lib/hexToBase64'; +import hexToBytes from 'lib/hexToBytes'; +import hexToUtf8 from 'lib/hexToUtf8'; +import CopyToClipboard from 'ui/shared/CopyToClipboard'; +import RawDataSnippet from 'ui/shared/RawDataSnippet'; + +import BlobDataImage from './BlobDataImage'; + +const FORMATS = [ 'Image', 'Raw', 'UTF-8', 'Base64' ] as const; + +type Format = typeof FORMATS[number]; + +interface Props { + data: string; + hash: string; + isLoading?: boolean; +} + +const BlobData = ({ data, isLoading, hash }: Props) => { + const [ format, setFormat ] = React.useState('Raw'); + + const guessedType = React.useMemo(() => { + if (isLoading) { + return; + } + return blobUtils.guessDataType(data); + }, [ data, isLoading ]); + + const isImage = guessedType?.mime?.startsWith('image/'); + const formats = isImage ? FORMATS : FORMATS.filter((format) => format !== 'Image'); + + React.useEffect(() => { + if (isImage) { + setFormat('Image'); + } + }, [ isImage ]); + + const handleSelectChange = React.useCallback((event: React.ChangeEvent) => { + setFormat(event.target.value as Format); + }, []); + + const handleDownloadButtonClick = React.useCallback(() => { + const fileBlob = (() => { + switch (format) { + case 'Image': { + const bytes = new Uint8Array(hexToBytes(data)); + const filteredBytes = removeNonSignificantZeroBytes(bytes); + return new Blob([ filteredBytes ], { type: guessedType?.mime }); + } + case 'UTF-8': { + return new Blob([ hexToUtf8(data) ], { type: guessedType?.mime ?? 'text/plain' }); + } + case 'Base64': { + return new Blob([ hexToBase64(data) ], { type: 'application/octet-stream' }); + } + case 'Raw': { + return new Blob([ data ], { type: 'application/octet-stream' }); + } + } + })(); + const fileName = `blob_${ hash }`; + + downloadBlob(fileBlob, fileName); + }, [ data, format, guessedType, hash ]); + + const content = (() => { + switch (format) { + case 'Image': { + if (!guessedType?.mime?.startsWith('image/')) { + return ; + } + + const bytes = new Uint8Array(hexToBytes(data)); + const filteredBytes = removeNonSignificantZeroBytes(bytes); + const base64 = bytesToBase64(filteredBytes); + + const imgSrc = `data:${ guessedType.mime };base64,${ base64 }`; + + return ; + } + case 'UTF-8': + return ; + case 'Base64': + return ; + case 'Raw': + return ; + default: + return ; + } + })(); + + return ( + + + + Blob data + + + + + + + + + + { content } + + ); +}; + +export default React.memo(BlobData); diff --git a/ui/blob/BlobDataImage.tsx b/ui/blob/BlobDataImage.tsx new file mode 100644 index 0000000000..8cc8e54323 --- /dev/null +++ b/ui/blob/BlobDataImage.tsx @@ -0,0 +1,31 @@ +import { Image, Center, useColorModeValue } from '@chakra-ui/react'; +import React from 'react'; + +interface Props { + src: string; +} + +const BlobDataImage = ({ src }: Props) => { + const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); + + return ( +
+ Blob image representation +
+ ); +}; + +export default React.memo(BlobDataImage); diff --git a/ui/blob/BlobInfo.tsx b/ui/blob/BlobInfo.tsx new file mode 100644 index 0000000000..4f93cf3756 --- /dev/null +++ b/ui/blob/BlobInfo.tsx @@ -0,0 +1,92 @@ +import { Alert, Grid, GridItem, Skeleton } from '@chakra-ui/react'; +import React from 'react'; + +import type { Blob } from 'types/api/blobs'; + +import CopyToClipboard from 'ui/shared/CopyToClipboard'; +import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; +import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider'; +import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem'; +import TxEntity from 'ui/shared/entities/tx/TxEntity'; + +import BlobData from './BlobData'; + +interface Props { + data: Blob; + isLoading?: boolean; +} + +const BlobInfo = ({ data, isLoading }: Props) => { + return ( + + { !data.blob_data && ( + + + This blob is not yet indexed + + + ) } + { data.kzg_proof && ( + + + { data.kzg_proof } + + + + ) } + { data.kzg_commitment && ( + + + { data.kzg_commitment } + + + + ) } + { data.blob_data && ( + + + { (data.blob_data.replace('0x', '').length / 2).toLocaleString() } + + + ) } + + { data.blob_data && } + + { data.transaction_hashes[0] && ( + + + + ) } + + + { data.blob_data && ( + <> + + + + ) } + + ); +}; + +export default React.memo(BlobInfo); diff --git a/ui/blob/__screenshots__/BlobData.pw.tsx_default_image-1.png b/ui/blob/__screenshots__/BlobData.pw.tsx_default_image-1.png new file mode 100644 index 0000000000..b5bc4b859e Binary files /dev/null and b/ui/blob/__screenshots__/BlobData.pw.tsx_default_image-1.png differ diff --git a/ui/blob/__screenshots__/BlobData.pw.tsx_default_image-2.png b/ui/blob/__screenshots__/BlobData.pw.tsx_default_image-2.png new file mode 100644 index 0000000000..47262e6684 Binary files /dev/null and b/ui/blob/__screenshots__/BlobData.pw.tsx_default_image-2.png differ diff --git a/ui/blob/__screenshots__/BlobData.pw.tsx_default_image-blob-with-zeroes-bytes-1.png b/ui/blob/__screenshots__/BlobData.pw.tsx_default_image-blob-with-zeroes-bytes-1.png new file mode 100644 index 0000000000..653421e4cb Binary files /dev/null and b/ui/blob/__screenshots__/BlobData.pw.tsx_default_image-blob-with-zeroes-bytes-1.png differ diff --git a/ui/blob/__screenshots__/BlobData.pw.tsx_default_text-1.png b/ui/blob/__screenshots__/BlobData.pw.tsx_default_text-1.png new file mode 100644 index 0000000000..42f8f62146 Binary files /dev/null and b/ui/blob/__screenshots__/BlobData.pw.tsx_default_text-1.png differ diff --git a/ui/blob/__screenshots__/BlobData.pw.tsx_default_text-2.png b/ui/blob/__screenshots__/BlobData.pw.tsx_default_text-2.png new file mode 100644 index 0000000000..888fdc8cf7 Binary files /dev/null and b/ui/blob/__screenshots__/BlobData.pw.tsx_default_text-2.png differ diff --git a/ui/blob/image_with_zeroes.blob.ts b/ui/blob/image_with_zeroes.blob.ts new file mode 100644 index 0000000000..7c9eb4ed8a --- /dev/null +++ b/ui/blob/image_with_zeroes.blob.ts @@ -0,0 +1,3 @@ +// eslint-disable-next-line max-len +const data = '0x00ffd8ffe000104a46494600010100000100010000ffe201d84943435f50524f0046494c45000101000001c800000000043000006d6e74725247422058595a200007e0000100010000000000006163737000000000000000000000000000000000000000000000000000000000010000f6d6000100000000d32d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000964657363000000f0000000247258595a0000000114000000146758595a00000128000000146258595a0000013c000000001477747074000001500000001472545243000001640000002867545243000000016400000028625452430000016400000028637072740000018c0000003c6d006c756300000000000000010000000c656e5553000000080000001c00730052000047004258595a200000000000006fa2000038f50000039058595a200000000000000062990000b785000018da58595a2000000000000024a000000f84000000b6cf58595a20000000000000f6d6000100000000d32d70617261000000000000040000000266660000f2a700000d59000013d000000a5b0000000000000000006d6c756300000000000000010000000c656e5553000000200000001c004700006f006f0067006c006500200049006e0063002e00200032003000310036ffdb00004300010101010101010101010101010101010101010101010101010101010001010101010101010101010101010101010101010101010101010101010101000101010101ffdb004301010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101010101010100010101010101010101010101ffc0001108019001900301110002110103110100ffc4001f000100010402030100000000000000000000010708090a02060305000b04ffc4005d100001020404040306030603020809080b0102030405112100000607310841516112718109131491a1f0b1c1d10a151622e1f117233218422400333952627278b619373843778293b7c225264853565792a22735444754668300a3a6b3d3ffc4001d01010001040301000000000000000000000001020304050006070809ffc4005911000102040305060404030600080b06070102110003040021053141061251617107138191a1f022b1c1d1081432e11542f1162333526200722443455382a2b2c2252634353654739293b3d2094463647583171827658400a4b4ffda000c03010002110311003f00f9ff00e1086108610861086108610800610861086108610861086108610861086108610861089009200049240000a90024d80005c926c00df084558cbba17ab39a12f392cc913965860b617113a6d900cbacb85c5bed8f855cf5d9718ff76ec33adc47c0089f8473dda62bdc975af100f68ecc762bdaa6d7bab04d88c6d720212b15988c8460943312b240322b71a9009414d544149de4534d9cb40214b4a52a04e42296a267e892beaa1b83c0aca4001f0262ba48b832cd1125a5664ce52293b6e309794893c0c74fe2185b90c8750010afa229597d80fb710a5c2c5a9889898768b6a7e15e8e6d480aefdc03f05500b715a8913768f69f67b0044d4854c91452ab31daea6743f773656ee1744b9a0085ba2677188ce921b7a5cf999466230b9a5b7e6211c4005645bfe887d0ddb90098ac328e1074ca022988999cc3354f10cbcd38a80899841414be29b4a00758008bfddd2d8799fbb75cf12d2a829a413cda3c2d871641717dcf83fe0b3b3ca3005489b8d6d16d4e32b96a42a648a799876174751ba0efa264b45155d62252cd00da4e212a6a47c226b82a8c94e172436fae6288cd8a520f8312c7929f9c54d9003e81e8fc8e3188e82c8727887e1d4e2da44e171f9860c8792a496df96cfe3200672e8b4a02c86be2a15f5364256921d4058ed2c33f0d7d896173a4cf93b0f400953364a8a93fc4f12c6b1294b371fdf52d76273e8a7062fb9369d6870e120b0018c84d0d2a588940917f894a5798258f421a3bbc3e47c9708971a82c9d94e0009a796a5b8c40e5b92c14329c340a57c3c2c0b4cd7c206cd800505a829d834900d9bf675429228b60762e912e1ff2fb2f824a2a2d62a32e892a592c03a892460065b2bc24491949943a4b40f908ecd05596c3220e5c7e020db484370b0558580066d09144a10c31eeda4a40164a52001602d8e4b47856154084caa2c330fa2900681ba89749474d4c8400186e2644a9694a4337c2001c85a2e04a40609481c0000005f3b08e44a9649528a89a9a935249352493526db9a81ccf218cf76b0006004e437239002d7f10fd44c1254829285149dea0d0835352082286b7fa0c0a8100cc039f4661c5f33d0daed7844468332865c1cc7fe1f06e27c0e42c6ffc2a19006850f0a92b61ef78d292454292a41143420d71afabc2f0aaf96b955d8661f500d2d60854baba2a6a996a49674a913a5ad2a0cee0821b94414a55fa9292f9b80005fcfa08eaf1191f2545a5b6e33276548e6597038db11d96e4b1d0c8580521007f0f1502f326809176c8a1db738e3757d9bf6775c9ddaed81d8caa4dca454600cbe09354330e83328894a80243a485072c43806d99124e72651eb2d07e623a0044e340f47a7919111d1b90e4ec3f12a694e264eb8ecbd069f749424261e5b20008b96cba110b4a007be1211953854b714af7ab2b3d7d897e1afb13c526ce9f003b61a969a74e502bfe1b8963585ca410024095494388c8a392180f864d3a1200a3bca52092a2ab2aa1a55124ca009ff2a9691e49501e914d271c20699c7c5300d132c986699121f79c7132f8698c146cbe15a536434c41fef296c44cfddb6e0000b52e36691aeb892a6fc68252b4756631f82deceeb173a6e0db43b55832a60015aa5c99d370ec528e41206ea25cb99454b58a96955c89d884e98a0e0cc4820008c7561720beeae625f4252a03901ba0f9a89e714767bc1966886f7aacb79c0064538436ca9d4a26f031f21887968875b8b8665308acc10fef9d884a2161170011150d0ce9703d14fc0b614074ced07e0ab6e28933e66ce6d3ecf63e89482b009522b65d66055d52420932a54a2313a144e52c044bfcc6252649077d73a58b004632f0b9a1f726216da17413d07c41faa80e7142f316866ace574b4e4cf24400e1f65e2b098892a19cc4c3650a61bff852e42f4c7e03de3912d370ff001c2100be2dcf78985f7c5977c1d05b4dd8a76a9b22cac6f6231b4482852cd661d4e900c6e865a52529267d6e0b32be9a9492a1ba8aa9b256b0e509525248c3994b51002bf5ca5b66e91be9039946f01e244527208241041048208a10458820dc106c0041db1d5d18f11842184218421842184218421842184218421842184218421800421842184218421842184218421842184218421842184218421842184218420018421842184218422a8e46d19d45d42532ee5fcbd12995b8eb2854fa69596c0095b6dd70a16fb7171090b98221802e453528666318d202690ca5baca1cecbd0087ec83b44ed0d6856ccecd56d450929dfc62b0270ec1a5a7bc12d6b4e235a6004c8aa54924aa6d3d09aaab4a4129a75581bf2a9a7ceff0e5923fcc7e147fef001b1e81cf28bbaca1c1ac860fe162f3be658c9d4436e30fbf2991342592b516005d5a9d827665121e99c74245a12d256f4343c8631a429f6d95a1d2d44b7ebf00d8cfc1661b23b9a9dbdda99d5d304c94b5e15b352ff29464215bcb953716af0092baaa8953834b589141874f969dfeeea1331485cbd94ac2d22f3a613fe996001878a9572fc920f3e173f9534e7246486d94655caf27943ac174a63da854bd003757be429b703b3a8b311367d25b5bad21b7a356d36dbcf36ca1b6dd712bf50056c9f655d9cec3a65ff66764b06a09f294568c46653ff10c592a50209fe2d80081aac482589025a6a84b46f10842428bec65d3c894dddcb4a48fe66757fef100757ac77420f5efb5458d76a57ccd6b5fa760ef8d3ede3c1b2fb5a2f44786b6002079803a9a53cc7dd6c6adf19be8c731a7b2dab5816684026d7adabe95fa9b0079dcf3e51bde570efc3d2f9dc8c9f83a005bd48af3a1f3048b1dbd79d70de00033d7e5e0e0e87a65704420523bf7f4a505297e74dfd70df1a7cf4624f21a8200097b4214a509bd0d4501277b6d4b0006f5e83960142e35676e99872cc46bfd0059003a5efb11b0e7bec7e408248186f747b105ede0dcdf4b36908537dc53e4007f1b75df7aee5586f86f3cbf766f9bf1b420050dab737f974a5ae79f216dee00de19e9e6d9b1719bf2763e88786fccf3352695e56e7b6c49b6e45aadf1ccdd00b2f5e9fbf030852e398a937ad473e95f43bf4b571215c8837b3176fa73bb710021e10f08a7cbb5fa80297bf7f0df6008c415b3ddfc7319f4e77373e04a1e1100b8b9008bec6c45ff000f4eb86f0e77f3e56e0743e6d08809edf303a1d87a8a0083e86a2b892af1e02f93b70391eb08e999af4e72367869d466acaf299bbcf1006caa60ec3866703dc36969b4b539832c4d58425b6da6cb4d46a1a750cb2dbc008710d2129ebddadecabb39db94ccfed36c8e0f88544c5a662b10974e70fc5800903741fe2d872a931254b01b7a52aa8c956ea77e5ab752d66653c89dfe24a400a8bbef36eaadfea4b29b93b45b066fe0da431822a2b246658e93c42dd79e6200553e6d3329580eba92dc23531864b13281858541752dbd130f3d8b790961b70096a73dec52fcabb65f82bc36777d53b07b555143354b98b97856d2ca157463007d5bc9912f15a0928aba7952412997df61f88cf5a0244d9ca9815317af9b8500a4b99330a4e899971d378070068e951e2758b47cf5a33a89a76a79dcc397e200152a69e79a44fe567f79491d436e06db7d7170e92b97b7141497211a9bb12e008c750540c2a5c65f6daf1f6dc7643da1f678a5af69b66eb29e80294118c51e00ee23832d226f752d6ac428ccd934a67a8a4c9a7aff00ca55a92a4934e92e0600b27534f91fe22081a2832927c52ecfa0531e20452dc75ac5886108610861080061086108610861086108610861086108610861086108610861086108610861000861086108610861086108610861086108aefa71c3bea16a1a588f103fc3590075e4a5d44f67cd3d0e98c654965c4392897f8531b336de69f0e43c6210cca900e2d3ed099a5f694d63bb7b38ec07b43ed20c9aba1c37f82e013082ada1c7130036928a64b281302e824141acc4c2d25a5cda490ba3ef3e09d572194a4e5c8a0039d3d8a53b88ff003aec3fe88cd5d406e2445f4e43e1a74cf241868b8897af00364ee1d4a70cd331a1a7e19b794c065460e469aca98690a0a89833188994c20012297ef9a9915b30aa67de5d9efe17bb36d8a34f5b89d21db3c72429333f3b008eca42b0e933bba32d4693024a974225ef133650c4bf89cf9338226caa842e005cb29dbc9c3e44a62a1deab37581ba2da2326d7e2de6e36117009484a42520002529484a52004a5200a2401b0012289000a580d857d1d2e5cb952d12a5211200a54b4265ca972d2944b972d09094225a100250842404a52901294800000011009d6d2c341c044d3b76a52b526fca9d7a93d8e2b7f4f1f9f2844529fa539f3100f9fdd312fc4e8de571c3d5d8741089a01bfe7d6f5e7cb954f7df0249279e7e00bec741c042229dff00bde9bd0f2fadef6c1f316d3d3dddfac2269dc6e3e5d70095afcc8bef887cba742dcf5b65ca1116b7cbbfe5b9e9d3b60f9dfc1f837c8700d210a57f2dbf3fbfc707f5b7be8de9c21134e5fd6b5dbafa52d7e94c4fdbe800dc3df18447a74e5f2fd7bef8876f979da1134d8017fd493eb5a8a7c872c4bf004361f26b787df363080036ee7f1279f9fc86c4d2a77e19365c1bcf21cba42100e1efc8f317de94a50d3b8bd77e78876e1a79f8e673f330852dc8f3e55bf2f9001f4ea0d712fc2dd1e10e676a5c585bcc5b726a45f7e831048d6d6d4fbf01c000723087cb61f8f9d2b7ef6de94b1f4b7d7eff0048429d08f3eb5f3bd07a6de8006497f1cf2cf8d80605b5b78c214d8fade879d3d7f1bdebb987f5fdbdf0f4840029ebdfcae7efe83077e1cbd4b71d5e110529502950052a052a49150524514000a4820822a083635a6c0e295a11350b953108992e62148992d690b42d0a052b0042d0a052a42924a54950208241041845bfe7de1a74d33b9898c625eaca53b8008525cfde99750d310cb752c161262e44a0995bed38af0c4c59836a5b1f191400dfbe7664971e8a53fe71ed07f0bdd9b6dafe62b70ca456c6e3938999f9ec06005a13874d9c25f769357812948a032f7809b3bf877f0ba89f3b7a6cea952e6400c2bc19d87c89ae520ca59bba3f493cd06cdfeddd3ce2c5b51f877d42d3b4bf001ea81fe24cbaca54eae7b2169e884423294bee2dc9b4bfc263a58db2d305c80088c5b6f4a992e32d19917dd4b58f06768fd807689d9bf7f595d86ff1ad9f940049fed0e0699957452a5841595e214fb82b70c12c27766ceab908a3135a5caa00c9c5492ad44fa29f21c94efa07f3a2e07fb87ea4f3246ebd81314231d25189000c210c210c210c210c210c210c210c210c210c210c210c210c210c210c210c00210c210c210c210c210c210c210c211dfb21699e71d49997eefcad2a5c4b6d00a9023a6912a30b2796b6a5b485391b1eb49405212ea5ef83864c4cc5f652e200e160a23dda80e6fb0bd9d6d7768d8a0c2b657099b5cb4141acae987b8c330d0094b501df57d72c77525206f2d3253de55d4250b4d2d34f989dc8bd2644d9ea00dd9697e2a364a47151d3a5c9d018c89698f0cd91b22b70f309e30c673cca8200dbdf1d34851fba65ce9654871a96c95c75f857bdd38e92dcc26498b8bf7b0f000f1d02d4a5e0594fd12ecbbf0b9b19b1229b13da8453ed96d22372685d6c8200701c3e6eeac2934385cede456291de7c3598a227abbc9526aa92970f9c186e00a9f0f95299533fbd999dc7c00f24eadc54fc40498b912a2a254a254a51254a0026aa249a9249a9249b93b9bdef8f502592024240003000300320c136602c18006561666d844624a89f26d7c795f5843adbefe74f9e1bc4677b6afc723d73bb00bda10bd3977ec2bdbbf23dbb6054486b31e0fc8eb7e5c336843ec77fe95b7600be0f717b0e835ebc9ef67e59a1ebf5fa7af4e780511cf3f0e9e86f99d1ef08007ddbe9b73ebfae04b12737caedcf205db5be79c217dbeed7f973fae009cb2100a70e418d9bea5c92d0876f5dbece0f66ebc067e2186ad7bdb2b42183b3316b003d80377235cade275844ff007f3fed7fad30737caec5ecf62e2c35e3eb6844007f6b52fe7d7cfb76c0a9f5b58f89b9b1b10ef9fca10c4ef3e8340eecd99b3900b007e5a3864307b5b9e8c2e6fab6bad830e3087dfe34fba76b571049367b7400b30d45ba9cacd6c83203e5bfdfdec7e786f5c72bdef7fab641f23ab421fd3b006df7ebb9c495177e447b7bb6b95f22ed643eff00bf4fbf41258646dc3a785900b4b5afac227e5cbbfe54f3af3eb887cdc8cb2e37c8f9977cfc4184462778f10016d18807cbe45817bf0843d3d0fe36f9ee3f2c46f173cf3176cafe79f1e79b00a1b721f3a8f9f7edb72277c378e7619e9ef33c2ed9bb0843efefef972c09cd00bc5b8b8d75c9df89cf8a2429492149252a49052a048208350411420837046c006fbe0485050500a4a92c52458836208208219ed931391308b6dd4ee19b23e700a4444c644c31933332fde3bf192b854fee898bc1969286e67256dc62199f7800b67fcc984ac42457be898a8f8e6a6eff008593e5ded47f0b9b17b6a2a314d90084c8d8dda45efcd52a8a47fe01c46694cb0135b85cb29451ad465deaf0b1200085ce9f53574988cf50035f5187ca9aea96d2a66761f02baa74ea96cc921463001db9f74d338e9b4c952ecd32a721db538b4414d61bc5132699a12a584bb0130000842165684178c244261e62c36a418b83865a8231f3bb6e3b3bdafeceb13500617b55844fa1515a852572019d85e2484dfbec3f10427b8a849494ad729d150054fbc11554f226ef4b1a59d226c856ecc491c159a55d1591e998d408e858e100116618421842184218421842184218421842184218421842184218421842180042184218421842184218422f1f4778579a66210d98751d11b21929703b0d9600c05424f66a841a83302b4fbd92c03ab1eed4d1409bc4b497bdd26581c838f7007d75d8e7e17318dac14bb43b7a2af67b6714513a9f08dc548c7718940b8335003313bd84504e6b4e9c835d5129d54f2244a9b4f5c7654b87ae632e73a25e6100392d5e047c29e66e7401de320b2590c9b2dcb61e4f209640c9e570894221e000a0219b86601f7686cbcb4b63c4fc4ba96d0a898b7d4ec5c5b80bd14f3afa9400b57d12d9fd9ec0b6530ba6c1b6770aa2c1f0ca6404cba4a1928948de4a1085004e9ea03bca9aa9a10933eaea5736a6a260ef274d993145477884225a42109000948c800c3af327526e758f6d4a5e97e5cc56e2d6a5ebb52b620914a1dd3f3100c4f103833bf8f46046554401534e7b7979fd6df65be1ba65cf561f5ebce11300437a5ec2b7aee474f2f4bf998df1c75f4e76f972e7088a1aedd36e5f5a6c39009dae7ae2778367c45ece40d6df219f9420013b7f5b5edd7ebf861bc398bfcf008fcdf5d09bc227c3ced4a57e401a7af9f5d861be39e6df31ab5985f83b422700c34eb7b5b6d8837bdc9f2f31ca37bea799c8f260dafcf547102bd6fd2db79600fc89e74b6c052a71c47be3931cc5f5f284284f2f3e5f2ed4a7a9ee311bc1d800eaede1993ae6e385bc610a540a72153defbdf71b0f9d39e01573a5f741fa7c00f93422684f2fa1b1b589f2b8ad7eb86f07cfd783e433f367b33ea852bd079000a8bda9607f123a50e1bc06a4b747d5ed6f938ccdaf0801dab5a7236ed51e7700a50d7eade1c78f8b7c8db5d39e4814902b4b7a7afd76ded4c02c7cf2072cdf00cb3e7ca1029e9e63a915b5bd79796f6c37b8e791e0ed7be5e679e57843c37e005e7cbaf9d295bf63d090df0df4d7edeb9422294dfbf314f5bef636ded41be100bc3dbbf935ee34fa5d120579f5deb6a0af23e7ca9cf7b06f676fb91c6e074600cc9b73840279f202febccf3b75b8a75a530de1f3ced97bc8907ce11045397e003f4a81f7f3c0287bd0359ff6d783b422424da97d89af2a72343b76373436c000a85efe5e998cfd39c21423702d5ef5b806b4db7ed4e57386f0e3f4617c9c5e00e39de1114b9a50d3e5e55fd4df00a0dfd09eb6f50d6d435e1134aedd4f5edd00b615e77a6e49c37c0e393fbe3d438f00f08007e95b5093c873db9795b95a0a008663a645b9e99b7cbcd114dfb53ea37ad48de9e7db94ef7db89b35985f2f2d006f6847ab9d48a5198e5b15269f4ae0a6f2b8c6d6888819843b712c28142d0100d485a49662590e2970d18c29b8b847bc2fc2bccbe86dc4e9768367f01daac200ea305da2c2e8f18c32ad0533692b6489a804852133a4a8b4ca6a995bca548a00aa75caa9a75b4c91365cc014295a1131250b48524e60871d7911a1171a463e00758b8579a65ef8acc5a6edc6cf64616a76272d90a8a9e4a5b70f8bff00938a0001767700d289421b0854da1d92c9744cc262e3dbf9dbdb1fe17718d93fcd6d000ec10abda0d9c4954fa9c1f7553f1dc1e59528a8ca44b49562d87c9b3cd94900188534a20d4c8a89726a2be3495587ae5bae4bad199466b40e5fe748e5f10d004100aa2ce31e458d6430843084308430843084308430843084308430843084003084308430843084308430847b39349a6b9866903259240444ce6b317d10d0005050a8f78f3ef2cd80164a10848538f3ce290cb0d256f3ce36d216b4e7e1780056258de214984e11455388e255f3934f4745492953aa27cd5e494212096001005ad65912e5a5532629284a942a4a54b504a41529458001c93efcb38c95e897000d528c84982ccd9bc434f3398f0c4434381ef65196dc29212dc2a544a26534006ebe37264f20310cf78512c612b861338bfa3fd89fe1a70bd8c147b4db6d2e009719dac48975149871099f84ecf4d524143056f4ac4b15925449ab5255494900501270f4ae6d3cac466ef29281325a64e65ccb109cd283a7fb943fcd924dc6005bd175279d875a9f2b9ef6b52d5b8f3f5902d974d797cb8e8798b6c7c7c9be00bd5fdddf9d3aec7911f6056bd460efe1f563f5812de3975eb11406c6bdf7a100bd8f5a8a7cbadb007873fb663e5d79c4c00a577ebceff334e9d3bd8d30776f0021e16fa7ccc2269b5b9d79d6fe5f5ada9f40578bd99b36b78659d8f3061e9000f0f31623cb9f3a541351bf423a5cb79ed9e991b35d9f46e16e7a41fdf5853f005df9dff5f5e783f1e5f36eb6b79c3c62297b827d6a2fcb7a0a5f6e56a9ad0900f2c87a79f3faf3261e79fbf0f63489a0bd0007a91ebdabe9cfad061bd617b30071fdf866d6617871f95bdf9988f0f6040d88245adbd6e69414e7b5852c7eae0079dc9bf36cadf5bc3cfdb72f77e16902fe5b7607ebd760052d7a0c1c701fd200da1d72bdf3eb0f16f7ecf844505bb1b6fe77373d7b1d8f2c378727c8b73b33000c9f5844d295e7cf99351e64f2a0fef815731a8d0717cbc5e10eb6e553cbb500a97247ddb07cbfab667cbef07f7efc3cc447845f99b75a7adee48df6df6bdd00bc79ea32395ad7d38662cef089a0a9faf7de9b74f9ec6b6c01e1ef4d78b74300d2d0f3f2e9cbddf8408a8a01f502bebcabb799dc0b83b7b1c786beecf687d60023c2294b8bd8d6fbf99f3f5eb53897b8e2dc2de961e3627ab42269dbaefdfe007bf5ee7d601f98e5d398cf94214f2b1efe7ea7913cee4f3183f0e87d3fac4300b7f4e7eda141b5f7afad7e5e63f0c1fe9ebea3d3e7130f0dc91f315dbcab4e007cc6f722f40deb365adfa7ed71fb3479ebc3e97e9d38e6237d8d4017e9de9c00ee76fa7205750d7caf7f9e5ce261e106a28057d3ea7b8f2c029b2396973cb200f4f3e1088a0b56848b77bdea29cfd6b726e6f86f6aecfe5c1b837a70b421f3003b0a791ad7adbeb4e75049dfdf16f4e0dce113415d876b6df67ef7c1f4f31600f3e2d7f1cc3da111e1ed5f2b5390b9341c85a9caa696c1c7836ba366cde36800428395ba8b7d46dd0d77a6d83f8e977fa31b365a5e112403eb4afa7e5dbbfa00e0eccedab3b6bf3e507f6c62d5b5b786a9467d11b99b2808691e722151113000de10d4a3323807f3a62929a265b357400b44c9942988a7c2913260ae297348004f26f6d9f869c2b6cc566d36c4a29305dab50995357877c34f84ed0cc0825400e94b4ac37159c52e9ac425349593d4a388265cd9f371195adaba04ce79925900332e4a724acfc92ae7604fea624a8635273269ae5e9a46c967701112c9acb900f5c346c14523c0f30ea391b94adb5a4a5c65e6d4b65f696879971c69685abe0070e2b8562581e215784e2f4353876254139522ae8aae52a4d4489a9cd2b42c0002c410b42c3a264b52664b5290a4a8e8d49521452a052a496208620fbf3ce300d6635f14c3084308430843084308430843084308430843084308430843084300084767c9f93b30e7b9f4265ccb32f5cc26515e2594821b8784856bc3efe3a300a215fe5c2c1c38527de3ce1fe65ada87652ec4bec32e720d97d97c776cb1aa003d9fd9da09b88e295aa225c996c944b9696336a6a672c8954f4b21277a74f900aa4a1018395a9095572e5ae6ac225a7794721c8664936006a4fce32b5a37a200d21d249438961489ae679a36cfefb9fbad212b210127f764a42901c8293b4e00d5e2827e22631011131eb5261e5f0b2ffa95d8e762580f6558777ea32b16da00cae948fe278e2e506900a005e1d83a5684cca5c390bde2b98a6aac417fded50094ca452d1d1f21a5a445324970b985b796465fe94ea0072faaac4b0000acff00007bfdd31ddfbc7dddf8b8cafaf3be7196d9f3f7eddf9c3efbfe941f33e97a009c3b8e1e16d1865964e2c1c92ed0e0f7cbcfd05b3f900d0fbaf5fbfbed1bc70042d776f5d34274d38b41aefe57ea3276b0f3877b1edf7ebfa8b60fa127317b00e8fccb6993e5a917716cff00a79e5af47021e9d3efefe9838b72be84be62ff00003b7220c3df0e47ee1dfae50f5fa57d3faf217e54c013ccb0d4dbadc6872e006de23a6971d7c1b2ebc1e077b0e9bfcf9d6a3a6f5f9d00dae4ea79f41c1f520078e4c4bbefefafadfa385cf2fec39dbef99c1d98bdc753a3017e1724bea59e000cefd7e591fb6766780f96ff00773f9dabb72c1cf1d0680640db4e27d1a1ef00f7d7e999261e5f7d795ed5fd77c1ce6fa0b5b3c859db4e16d06461f4ebe37e009e7e7034fb14a7e5f9f60298825fd79bdc71b8b3797589f7ef4f2fb40efcbe00c7e3bd7e589de3c4e86e5c58fbf66cf7efd7de7c56b436852dc5a1b424556b005a825290375294a3448eb7031724ca9f53351229e54ca89f355b92a44996b900b3a6288b2512d0952d6a205800556b6b000920004926c039249e033e805b9400567d20e1cf886e20626161f43342b567561b8b8a720db9be4bc8f3c98e566900f69450f263b3a3d0d0d93a5adb0b4a9b79d98cf615a6dc05a5ac39fcb8cdc60028e8765e4aa7edb6d36ca6c2cb1244feeb6b3682870ec5152d77404ecf4a550056d24d5cc077a5a2560eb5293f181baa0a8998112413513a4d3d9da74d4a5700c9a502a9b7d0045e321b92fd85bed36cdeb47c7688e51d3e6174a456a06b06004065210a485254b85c9132cf5304100d1685410710b05252680e3acebfb6be00c070c0a337b51a9c5a6253fe16cc6c3ed25692b73be9ef71e4ecc4821c0dd50089c42c3164dc0c35623864bceb0cc37b49a79abbf07982486e05fe515d59fd009c7f6843d0ff0010bcefc23c22fc3e2f8188d4fd5a5c5d69747bc85d047e0b00c5c81f89f00247f311538e387f131f87f96a32ff0035dadce513ff0094c9d8007d92974fa907ba9dda38a82904dc36f1660c22c1c630d767ad363714f240f2003541514cb37fb02bda5595d0e392bc85a47a8496d255e0c8fac52a65f70806008969bd40936414924d07f3bad8bd6e0571b7a0edebf0fd8881ff008fdb4983004d51000da0ecfeb64c90f65154dd9ec676994961774c95b9007c222ea314c30015ff00de66a0bffc6d3280b9cde5ae69b696e51641ac7c0071bfa00daa275600b857d62cbf2c6e1dd897f304872eb7a9195a11860153ae47e67d318cce5249005a50805c57ef58d8125014a0084923b0b02c6761b6bc84ec67699b07b473d500344a97872b1a56cc632a2b608eeb0adb2a7d9ea9a8df53202684561058167800cb95369a7da9eae9e6976ddef3b959d07c13c4a2a7ff004ef0f48b3d878a87008b47bd867da7dbad0a9a712b0957fcd5049252a1b292ba2924104541a6d71200c2f13c1e79a4c5682b30fa94e52ab29e6d3ad4904a77e5efa52264a71f04c9006572d60ef25441062e290b429969524b64a0478b117eaedc338f3fd3cfd7f100e4797318c07e800fd98647260e5b4e77a1b4eb9fadc64ee5baf843d7efef7300d7a540c46f1cfd6e6ec79e798bbd9b85ccf99f0cb5cf8dadec8653af9fcb6d00fb8fd2f8390ff17ee727b72bb9f5ce27fafd6183f33e377ea0e59db3bbc47100f7a367c7e90c439e26fc0b7d3870b65c2261d2dfd7fadc5ad4e789059fc472001ae8f627401b2d1c4479f4cbed0bff005fbbf2f4e86f83e61dae78fd1ecfa000ceee7230f95b3be5ebe3c5b3bc3fa7e3f75b73f9378b1be6feac33b0f4b3360045a0defcefc7aff587e1cfee8712ee741abe57b3ea33bb3f4b6866e27adfa100cb4f0d49bc4fa72e9d29b9ef53f879439e22cdf37702c3462d63eb0c985f5e0027ccfdfed11cb96feb6fcba7e7418805bf63974bebaf95a27dfbf7e621f7f700f75c4bfa391cb3e1c6dc05b2b98375f3f7e79f387cadb7ebb0c43b6a72e9ec006bede1145b59745a43ab728692f2db95e689636efee49fb4ca4ac25695132a009b04a0bb192775e3ef92807e265d12571500b4a622630731e91ed93b12c0bb0056c344e065611b5941297fc2f1c972920540dc56e61d8c2508332ab0e5af7400a26201abc3d63bea52b94aaaa3acc3aaa4454877dd9803256397f2af521fc5003a6a158a7ce193b30e449f45e5cccd00b809942f856054390f170cef8bdc4600c0c4a2adc541be12af76f366a9710ec3bc96a2587d86be5a6d46cbe3bb1b8d00566cfed1504dc3b14a2501324cc652264b53995534d3904caa8a59e91bd26700ca5290b0e1c2d2b4a78f4c96b94b289892950cc7c88391074223ac638fc51000c210c210c210c210c210c210c210c210c210c210c210c211da72664d9f67dc00c5019632dc288a9947ad5453abf7309070cd0f1c4c7474414a843c1c2b60ad00d584add70f821e199888b7988777906cb6cbe35b658ed06ceecfd1aab713c40066f7726583b92a52120ae754d4cd20a6452d3cb0a9b3e6aac942484852ca5000aae5cb5cd5a5080ea5161c3a93a01a98cb6693e93e5ed29cbcd4b256cb51530098a6db727f981c6bc319368b4f89440f12966165d0c54a6a025ed2bdd34d8200f3df111efc545c47d5cec93b24c0bb2ac09349489975d8fd6cb9671dc74cb6009d59381defcb536f92aa7c3a9d67769e9925256c2a2a02a7a94a1c929a991400c860c5647c6bd49e038246835ccde2aa1e56e54e7cb9fcfa77a8b9af6e3b7300cf9f1f5b1f18c988fbfbfbdadb60faf973f3f4f37843f2a589f9f2f5a7f5380087cc7b7f0c9b22e35e9087deff00df6fb22b5c1c5ef967ab756f7e508003b700d7bff73b9fae25dee78b69d34f26f0684283bed5f5fd3f3e9be21fa3736e3c0079d88f9de10dac0fd49faf3fc3124ea7ec3ec2113bd07e27f5b0fd6b887f9b0079fcaf6d7ea1102d5fbf3fd397cad8977f7e1f4bfef089fbf98f972bf3fca100eed7e7a7cf30fc1e111bd7ef6bfd0dfb6f897f7d7ef97a4216bff7fcbb007600debd460fc7dfbfb0e108bc7e0cf80ee2438efcecfe57d0bcacc3796a49162100b3c6aee6cf8c96698645516511061261378786898a9ee63799719f83ca5962001a673b5188878b98b729937c4cda1b176b768b63fb32c129f68bb47c567e1d0026be4f7f80ecbe1699151b63b512cae64b13f0da09ca4c8c370a44c96aefb100cc6554d4252952281188d514532add44f91472c4daa594850797290c67ce0500c3a126c9438bcc5eea7fcbbe7e18dbfb83ef614f067c36312ccc9a99204f14001ab10feea25fcd1ab52c838cc8d288ef866da7daca1a4f589ca707028790a8009838acd68ce3992162145d879fb09f76cb5e28dbdfc59f687b452e7e11b0fd00d765db30b4aa41a6d979f353b4d8949ef0ad2bc736c54118cd44d5a591369b000b9983e14a40dcfe1cdbca571da9c72ae6ba29da8e53334927be58777993cf00f784f108eed0dfc9c733d2e96cba4f01092b94c0414ae5900c370b032e974200b10501050aca4219868484866da878661a40096d965b436848094a4014c79700aa2a2a2ae7cda9aa9f3aa6a67cc54d9f51513573a7ce9ab2eb99366cc52a6400c98a24952d6a2a512e4931a624a895289528972492493c493727ac74ed47d5006d30d1ecbaf66ed59d45c8fa6595a1ca90ee62cfd9aa479464a971282e7b9400cca7d1d0108e44292925b876dd53ee1a25b6d4a201dbecf6cc6d26d6e208c200765b67f1ada3c4e60de4e1f81e195b8ad614b84ef9a7a1933e6a65827e298a00484273528004c572a4ce9ebdc932a64d59fe596852d5d592096e718b9cefed00e0f665e4d723a1a0b5c67b9fe3601e7585c369e6946a7cfa1e25c694a42cc0004fa2b2acaf2bc73056921a8b869e3908f829721df75a21cc77fd07e107b71a00932bf8960bb39b2e99a94aff00f19f6e763f0aa8921402826a30efe333f1590013424baa4cca14ce4314ae5a543763689c0b11511bf2e4c97ff9ea890823aa0042cac11a8297114725bfb469ecf08b8cf713387e2132d4185515389de90fbd0097251ffd6a9321ccb3b992514bde5de3a03fc95b63938fc11f6b551bb2b0cd00a1ecaf19ad5da5e1d876de521ad98b25822526b68e8e4cc592c1913d41c87200c445efeced71fd1368e62b44a2a46f126d60a4a41f03f30f7a9a7fed5be04f0051e530198251ac93290e5999a14b82ce3a83a55abfa71905d08092ed75173c006439164247b92b4a5d2bcca94a147c35271d778f7e17fb6dd9eaa9f4355b23004f5d88d3102761380ed3ec9ed0e3892a2425b00c1b1badc755bfba4a7770e20048bb4624dc22be528a0c942d60b1972a7c89b347ff00b4898a9b7ff64742e20003d9e7ece8f68ce5c9866d839669d4c335c6a5c5c1f101c37e63ca7079ca1e006118597d11936cc394bf7a65ccefef510e10983cfb2cccd0c9857627e05b8300887445379fb27db576e3d89544ac031746313b0341099fd9ff00697856255500832e4ca4cc945145418b8a6c4b02992fbd25355b3f5585d4227265aa64c584000966b9188623871ee962619567a6ab42ccb600864a56ca946f654a28208176000d1a917b413d93dc45f00b11139be614d61e1e62231b625fad395253110ae60058545c43ccc04b755f2b21c8c7f2746baa6da69acc50b1131c951f111b010a0089c4be6d1824ac7b5fb37ed23613b6992656c819bb37b6f2a4cda8adece71900ac44f9f5a89284cd9f51b118c2932538fd3ca4296a383d52246d0c9934f3e600264e212902a57c8e8eb69b100d25e4d4004aa9662812a66734ebb77a90ff0000a14d343160a178c5d8215450a281014140820820104106e0d6df2e95e48a4a0090a52169521695142d0a4a92a4a9259495248749490410406218b45f2388b800e3a189fd39fe5ebc85ab5279e21dd867d1b9fd99fa0e8879fdedfd7a52d88d00e62dafdedec6afac227b8b5790ada96ea69f8d77e44cbdb4b3e5d7cc9e5c6c00211183fedcfa421d3b7515b8af5dbebcf00781eac74f6210037fd4f2a1e5f900dbe583fdb40fa59d83faeb081bf21f7fdff21416c01cfc8fbf5f28448e7b5b00efe7b7235ad4f5c43f969c4eb937de10e9b7d9e7f75a60e3886eb088c09035001e25b87d3e9c61137fa72e86dfd3fad7070fef3e0def93c21fa7e237e7dbfa00723f47e0fcfdf8da114b35634a32eeabe5e76593365b859cc336e2e413f6da00062e531868a00f84a15132e89294b51f2f7545a7db3ef992c473107170fd4b00dae76498176ab812a92ad32a871fa2973158163a25bcea39c7e3fcb546eb2e00a70da95009a8a73bdb849a8a708a8425471aa699152862c1607c0bd41e078a004ea34cc5e31259cf26cff20e628fcb19921042cca01693e26d7ef6123219d100e3868f8188094888838a6e8b6965287507c70f12cc3c5b2fc3b5f28b6a765f001ad8dc76bf6776828d54589e1d37bb9d2c9df95350a01726a69a680133e96a002594cd91353652140282561484f1b992d7296a42c329258f0ea0ea0e863ab60038fc510c210c210c210c210c210c210c210c210c210c211ece4d269a6619ac000492490311329acce2110b05050c82e3cfbcba9a01608421214ebcf3852d3000ca1c79e5a1a6d6b4e7e1785e218de234584613493abf12c46a25525151d3a7007e74f9f3941284245800e5d6b514cb9680a9931494254a1294a96a0948254a0020003324e4232dda27a332ad24cbea6bc68996699b21977304e0b484a43810009526512ca51d6a5104e54a56ea8bf318a2e4c2212c36a8397cbbeaaf627d8f0061dd95e04553ccaaddacc5a54a998e6261082242590b4e0d872c24a9187d2a00c6f4d59515d7d5a55553b765228a969391d252a69937654c57eb57001be14f002073e26e5810135a8a76a5af4200dbbd77efe447418eee7be6f978f221f867009e56e3195bdc9871240bdadd731e1e4205ea4015201a7e343414e5b6db0be100bc435c9e39f971ead12e5b2b80e478699ebfd490444784dc7304721fd856b500a6d635186f65eb6e97cee79d8f03ac1f8f8ead9bf934494d09b8aee3a5af4a005283e62ddac5bc6dc2faf327883af1bb4017bdd8ff0046e3f4e700012aa74b005491cafb76adb7e54ad68de3c85c5c0bdaff003bf583e5cfe5c7e5e6f0a50f0023636b914adb99b52e37da94ad0e0efc8666f99d74ccbf3e761691d1bdfbf900c4f86c0d2be5cebb73bdee6bc8f414c1cdc027ddf5cb9b666d7887bf83b9b60046fa7be4e0c71a57e74a5091de9b1f437ef5c1cf13959edabf12f933f8189f0097bf79c7229040a6fccf53dc8aeddebc80380511af87adb40fcbed10ff0030003cfab38e6223c3d46e6c45697bdc6f6f4e753890a25efa7be0d9f9861c20fa00e9efdeafa42963cae05297aed6ea4efebbd092697eaf77bf1cf4f57307e5a6006f6d73e5cda0526f61db95cded722db6f4f5c55bc4645ada5dcf3b0cdb573700e713ef8f4f790bf58c9f7b2f7d9ab9c7da15aa51eb9b44cd72770e3a6d1d0600355b5020db533309ecc9d4a62a1b4bf20c538cb90cee6999422d1179826c3d00ec3e4c90c4331b1095cde6997e0261c6fb48ed1b06ec576628f68713a5a6c60036cf6865cd99b0bb2554cba612252cca5ed86d2d3852668c06967a0cac2b0f005eeaf68abe5293ff009ae9ab669c7adac9787c94cc58132a2683f97906e18500bbf9c1dfba06c84e735408fd014637e9d25d22d34d09d3ecb5a57a4392e43900034ff28cb9996c872d65e82441c14332d2129722a25cfe68a99cda3dc4aa2e006f3b99bf19389d4c5d88994da3a323e25f8973e606d4ed56d16dae3b886d3600d562f5b8e6398a4f55456e215d34cd9ab5289dc952921a5535253a5a4d25150034b9349474e8974f4b264c8968969e193a74da898a9d396a993165d4a5173d0006812324a400948b0004546c71f8b5183ef6a87b5ee4bc19bffe02e834aa5500a9fc55cfe5d0efbd2f78ae6795f47a5d366bc72b9b6738196bbf1d37cdd3580075b71b95b20b2e424444c13b0d9867cfc2491f9542e62f54761bf87c93b6780070ed07b44aaadc07b39a7a89b270ea7a5dda7c6f6e6b69140555260b3aa50600450e0945301958ced1cd44e95226a578761f26ab1213851eeb0dc2bf3091550054a54aa404ee01699505246f2504d912c64b9c5c020a500af2d5b273c2d7b5001b8edcf4bd41cf3a3bc4eeb66699badd8985cd7a819467f97f284b5888507000c1e567b334265cd3cca5264d1221e53969329804252928616b2a52fd535bdb005f655b0184ff0067f02da5d89d81d9ea6dd97fd9fd95c429cd554142373bdc0066750cdabda1c7eb55754daac627d64c3314a281253ba84eed588d051cbeea005cfa6a69496feea4ac151d3e329de9b355a9330a892e7e119570917b057da700f3af76a88d0394481b5a428393dd5bd256fc2084d12e332dce7328a4280249004a98052010aa2804e3ab6abf14bd8ad3b846d5cfaa2091bb4b80e3a78dc2a70061d25043ea165ec5c863186ac770c4ff00c793fed9534ffdcbf51fd3bb1fd900e7f695068b8323e97958493ee06ace59f7ab37fe449510c05721e27928adca00c6f8d5ff00fcdaf637bc13fc531b676df38156ee876054e3e26d7f4f16075b007fda0c39ff005ccebdd29beb6f6d1efb49bd9a3edace08b35b9a87a23a4f3b00824c3b897b31e59cada87a659d3236a040368ff3e579b74de1f3aad7991a8a00860b846e358933399e5c141c91cca062d0c3a396aff139f87fdbbc3e5ece6d00d63d4f8fe14bf828e763186e3787ed06cecd514eed5eceed3cdc3bbec2a649005244dfc94caa9d82d4a8ac56d0cd13664c17178be0f5a8ee6aa626620d92a900889889b28e8a9538a0196412fba5465aafbe9209319aae1c257c3d717b2f8f004eaa70b73be1238bec9309031ba9994651039cf873d64804c7baf43c167aca0019fb233ba779fb3769acf2670918253347a61170901346622533863e390d44004c3816d7e33b69d9f4aa2aed92ed22476a1d9363cb9d2b67b10c4ce15b73b3005de53a50ba8c031cd9dc7918f60b83ed361f2264a1574c8912d5534aa955b40013a6524c1dd6aaa665650eeaa9eb7f3b4134912662ca2aa4b862654d93384d00972a7a030527746f27e2412936cb4e5e6a020723c1e9ce6a445ea26596f2ea0032b4d5ccfcfa336cdb33ca3e044ba2919ba2670d3cce687a670be344ddd9ab002e2e685e7d71aa794eb9e2f32e2089d3f1a9bb41852a56cfe24aaf389d327000342b0ba5c36afbefcc4b3854ba4521786229e6b2a9534ab48a508409210129006d577ebef0cd0d2d456563ba1dd84125c77612c1013fca12cd66368d393daf00beca783e12a39de25786f96c5c670b799a6d0d079bf2831f1d318bd02cd1370089533064bae262221bd2c9fc72d88195464c625472bcfa321642f441974ce400447d02ec6bb5b99db2d1cdd99da832a476c184524eaba7aa48914f27b4bc22008e505d4ce9325065ca5eda61b4e95d555c8a5960e3b4126756a249c429aa8400de6586e22311419530a456cb4950361f9b4a4392d61dfa4395001e625d4dbc000be0aa95a11b5b6bee7b1de94e84efbe39fb9cae0f3e39177cb33e77e23381003c3c7e5fd34f2313e1ad4537b8edd8f4df6a73e74c1c8b851cb5f0ebf4cbcc00ff004f52d105372074e95adfb0e84569fa9c028f12f9b3db89e19decdaf18300e5cfd0f0ebef3b104837af7b5c53e877ad3ca86bcdbc417e16b656e96f768300f2f0d7a70f5d6ed0f08bd05af606f6a7235bf5a7c81b13970e785efc3323520038ebcc41c7eece1b896d3de57804f5b117b81fadc0a6d6a56f6ddbc789bf0300e238e5f2ea60fc2e395cfc98789789f09b6db539d295aefdfcb7d87307cb3700777776c8117cf2b5f2b183ebebf5e9ae7114dedbd284ed4269cb63cc74f4bb007ae2e79b5f5275fdfd48812dcf8f2b137f4e1d61cf704f9545057a134a817000073a58570770da75e79d99cf526e0939c1f9306773f6f79400a8e808f96e7a008a8e7f9d8d449faf17d18db36b1c811e64e1dbdfbe50f0fd7614de9e54a7950041eb5a5c5478f4e57767b1b71bf2ce0fa7bf77bf996710f08a56fbd0d76e7c00c795bbd2a2f86f66343f3d1ddcfadb436107e8c59b898786dfd294e54fccef005a015ad096f5b816bf3e77b93e2e331a807e991d6d6e6def870a29ad7a332a00d5bcbe1af1a25d9a652879ecbf390d21492b2859549e6962ebb288d769e25b006a0fcba28351f0e1f6c464be63d23db6f63d8776a980854832a8b6af08953600660789ee200a800294ac1b1159014ac3aa9677a52c282f0faa22aa505ca55500d2d66255d28a945984c47e8571e28559f74e6381be4e0e24a75269a65d9ac700c927703112d9acb22170b1b05128287987914b1174adb5a4a5d65e6ca9a7d9005b6f32b71a710b57caac530bc4305c46b709c5692750e2587544da4ada4a8400eecd913e4a8a56850b821c3a5682a9731053325a948525478ea92a428a540800524b10743ef516398b47acc604530c210c210c210c210c210c210c210c210c00211945e19f447f8024dfc5f9960ca339cfe1825985884a0b99724ce7f3a21100290545b99cc87bb7e66a59f7b0cd261a5a96e19d6e6422fe917e1bbb191b1b0086236d369693776a718a6070fa4a8420cdc030a9e0a92374ef1918a6232ca50075654d3a9694cba052644c5e212a6ef28697ba4f7ab1fde287c20b7c008700003ff312c55a81f0b5d422eae9bf4e96fc3ebe78f5515f5d59b5e0fd0817cb3000cd9ec4dc83d430b16bd9dd9c6a3e621f7b77dbe586f97e9d41b8cd816f76e30000fec1bb872cccec5ec45b95e14eb4e54eddf9fe1f961bff005f21c872e62e000b9e2cf89177195ce619bcae4b9bf1853ef7efcfec72e586f5b9ebf2b8b96e009cb52403b9b024fa58364720c5d9c1367cc188e57afe07efad0f974c378fcc00e79e8c2d6c8e7d4dda040041393ddc1cfc5f363c49d4dc1134af97437aff00005af9db7ea1bf6e77b5ed7b17cfc3e5ab43af3162f73c5cd8bb780ccb2dcebf002afe7f2fcb1014459c9e3a0c9b321fc6df5880cfa3b317704961e398b1b1cd008387853eec2fcec37e7ebd2d89dec8732f9f4cef9e767eba938622f9f366b3008b8cc0e2cd63c6229eb71e971f86ff00416c028f1d3875cf363a9b101afc22006ec73e02cef63a0b6a327767600189a53a1e5506bb76b52bcec36b5b0dff00001e5971e5978f89bb09e2e1f880069d7996274639c452fbed5bdefb7913b505007d700a2f72347cb8f1f9fa5af03604bf2c806e9a3872ecef90689faee3e82e007d7a7cb9e016793dba5fd436b9f51a83e4d7cafc41cb426d70e4bf1ccc55ee001fb42f3d7137ae1a63c3fe9a3095e72d52ccf0d20848d7901c83cbb2665a7600639ab38ccd1ef1a2a9564fcb30734cc51eca166222db97881836de8d8a8661007968aec1b03c271fdb2da82b4ecbec6e18bc671844a5aa5cec4669988a6c230067e91612addadda0c566d2e172261dd4d3a274eab98a44aa698a15154b952e0064f9ce2553a37e637c255a225a59fe29b31909c8072a7dd05fe927c33f0eba006dc28e88640d05d2895896e4fc8527440b6fbbfcf329fce625c5c6e61cd73d00885296b8b9ee669d4446ce266f15fba44445985836e1e021e12158f959da1600de63dda5ed8635b67b473933311c62a42d3224a44ba3c368242134f86e138700c9484a2461f85d14a914549292907ba921734ae72e6cc5f08aaa999595132a00269f8e61761fa5090190848d1284809038073724c577c70b8c78b61e2cb5b2007da35a69030da772d85cc5ae5abb9ba45a31a0195631110ec14df55f3caa22001e53369e370ac443cde4bd3f9442ceb52b50637c0db70591f27cfde310cbde00e09ec5eccb64b0fda9c7aaaa768aaa661db11b2184576d96de6292952d33a900765b0412e655525099ab972d78ce3f57368b673009054553f1bc5e811b8b4600f88caa49289b314a9a4a69e9e5aaa2a5619c4996c4a52e40ef26a8a64ca0f7009931218c546e187839d24e1ab29caa1e53972459bb57a393133ad51d7b9de500a9339aabab1a813e7dc99e70ce799f347c23b3a519cce6262dd964891305ca0032bc904bb2cc8d885934a60619aea0ed4bb5dda9ed47682aebab6aaab0cd9b0095ddd06cb6c4d156d4ff0067764f67281229f07c0b0ba0df4d30451522258a009abee135389d7aaab14ad54cadac9f357acadafa8ae9aa24a9326c9914c85200bba9129008972d0876f853fa94ceb5152d4ea5131778897c6382a987700ffa00746fd68b292479038eae12669c907c593f32231934f395712d4dcc6efa163e0091e4fdd71fbfb8ff00fb8cfe1ef2b89fcbceff0027fd64ff00f5455f959f9f0077ff00591f2de78f1ae5f1a815543b87fea00e1f936547e98832668cd07c19005ff6498a4d3ce48732d5e0ca3e4924fa47e552168345a1483d149293f2206200d9046608ea1a2d1046608ea08f9c7e48a83838d425a8d85868b692a2b4b71400c351084aca16d15a50ea1690a2db8e3654054a1c5a2be15281bd4f555548b3003296a67d34c2374cca79d324aca7792bdd2a96a4a88df4214cedbc84ab34820000919123a12229fce74c2433105c979724f1054b592c03110ab2b2090a847500c4fbb0920fbb4c33d0eda02940a1490808e6f84f6878d50b22b823159012840081388935290870e2a65cb256a5023bc5544b9eb514a485a49595dc4ce50cfe002eb63e63ed140b50b4bd11b97731e4fcf197a5f9ab22e7092ccf2ce679546300063f2fe60cbf39835c04d2533661412a4311b0914ec3a90fa5973c656e41ba0056d25e4f706cb6db52d55761f8ae0389546158fe13574d8961d312b14d89e100f5f49384fa6aba3981453317266c94cdde90a98909dd4d421216659cc91505001311364aca264b5256922c52a05c1e771a131f3dcf681f07337e087893cc9a0050911b1da67981a773be88e638df03af4d74fa3e2d6d09147c425f7cbb98720014c4b995a70ec49878b99b30d2bcc8a8487859fc283f52f03daba3ed2b637000ced0e865c9a6c4aa2a0e0bb7585d3ba65619b612a499ebada694a96812b0bd00a8a54ab1aa0952953a4d2547f12c304c06892076053d522ba9a5d5a193314700bba940ca5cf48049b0b2272409896b075a5dd0c2ca282f43f954d056f63c8d0039f2f2af78dce448b667a6acfe02d7e46e7890da3a9ef91d6f70e188f9445300efd6bf7d682a70de3c746701ef6bb5add46658388076b3f0391d069c41033c0081bf29a01d0d6f4e9dcedcc5799f4c378b1e2cfc865c8b9737766cb9c1c80500c33102dc33b06390bf4e710457727fb1ede43f1deb88de3c8faf005f2003e500c34b110006767bbb87eaf97a805ddbe1b43f5fc3d3efaed89dfe96f32de00500f3d0f230debe4e097196b639f5b904076c9ee207d4ed6ebdbadf95fbe1be6e002da6479f176f46e360440ebc5f50332030cf3fa3b86689a03dbefd7d7b7a60001441bdc7937d2f7b3dc8b5b39c807bbe6f6e7ab87e02daf83ec53eff00b6050045c8ebd6de62d773a81c60ce4967d0b372cdcdb51c5b510a53d2d6dbf2f9f3003b93be1beda83e7edbef603214b97258e77e2ccedab7810cdfcb0a7adc0bf9009a1dbcae761426f5c3785b3362581c9dacefd7a3861139e84d8e6e0e8e086d005f41d00ce2282f4f9dcd0fcc11b9f99a6f5c37dd9ec0bfeda1d41d3cc40bb000b3366c43676e4e08e3a31cda269bfebf9dfb914ec0537c37c30f5d7c33cf8300e972e6009d398bb3bd8bb0bbe7e17b9ce29cf9f53cbcbec6e7d6778fa6474e00bc189616f8bc8c320330320daf81362f77e1997cad53899d1239fe4c3376590083f1e729043a83f0cc0487331c99145ae0ca0d3decca5bfe63f2c52141d8860057152e52229c5cb5309e54fc48f63636cf0b5ed9ece5183b558353135f4b4d002d2999b4184c91bca05218cec530d9614ba32099d554a2650013e6a30f952f005f5f4bdea7bd427fbc48ba400eb480e5dadbc039005c8b5cb018bbc7cdd8d1004308430843084308430843084308430845e4f0a9a34acc9366f52331422ff700048231272ec3bc92844e27d0ab0bf8e483fcef4048dd4a54149019899b06d8000f3825d318557abbf0cfd918da9c613b71b414a55b3d80d527f84d3ce4912b0017c6e429331130a48fefa830a504cc9d94aa8ae3229899d2e457481b0a1a6e00f15df2ff004215f082df1285dd8e694f91530bb111926ad4f97ddbcb63fd2f00f464a8303ec6bf2b8e3a3bc6e9cb339fdbe67a7ac3a7f6fbfbb6f8a4ac78dd00b23d323e3e8ee21bdebcdefa7316201be4fae526df66f6e5fdbd312140f02600cfe3e591e0fa7483f1e2e41cb8e4e39db22f7bb4476ad0f9d37b7979576389000a07c8f02036771124b9b387b972001ae9a8e26f686d4f4eb7b11615f53f3e00e20281f9f0005b3bf36eb14bfb1c1835df461f52e22683ecfe5b61bc34d41c00f4e6470d78f00625eef9db5d1b8310dcb26d2e1e20fafd39f3db97af3ec03700872eae38df2d7519bf2897d1c3170f99bb3e4c72e39b17bd84f95fd7d6d53400fba74c37b8b0fd8906fae560cfe1943df36ceefc88f3ff002ddee03981f97c0086ff00af2b53e630dfe5f7f47d1cf46e302a3637b781beb6fa0e0ef11f3ec600a2f5bdbfb6db61bdf6b966e239dce6035e0f97225af61ef42fe36b3d7b75fd007eb5efd71014fc35d78741c1cbb373bc1f89ea73e59b1bb644580e70ea6a7f001ed6143f4e95eb890ae993e6dae4c756e7f389de372fc2eef67c98bbb5f51600bd9eef9fdfd6df3f5c4ef0d18d9f316f17f7a90f10fc0f07722ede2fa640e400066e236d2fd9b9e15532fcb1abbc6666895b898fcdd1afe8be9144c5b1e14a00727e5c8b8498ea466297297e2f7adcfb3a312fcaa98b6c34b65cc85368649700198d714bf37fe2df6bff008660fb17d9550cd099d364caed0b6cd08992d5300057e29227536c7e173fba6dc4d0ececc9b8dfe5e6aa62c2f69a5ae6095324a60054bd4e393f725d3d124dc815551c779608908247f96513318bde6e86c3697c00786238dc30845a9696e5a82d77e3cf3c67899a951f93b828d3f95e9965197b00f0edfc235c41ebe49a033cea4664617e2f78669943433fc2ecb72698845588002d5ccff2b616d7bf8f0ff616d9d6ccd8fec3304d9f92f2314ed931fa9da6c600274a9aa4cd9bb05b09573f05d9cc3668dd6fca62db6dfda6c4ab29f787793f0065300a95ef777277362a9611872259706ba699b30a4b1553d3932e54b57fa10073fbd5a92083bd265139346501a61964785a6d0d8ffa29009f356ea3dc927100e634a129b2520741f3399f18c44a1080c84848e43e6733e31e5c551543084300084715a10b1e15a12b4f45a4287c88231040362011c0878820283280238100008f231eb1f9442bb52d82c2c926a9a941f3413403a0494d316174f2d4e43a0f002c9ffda74e41a316651ca5394ba0f2ba7c8e9d1a3d144cba261aaa527c6d8f00fce375200a13fcc3fd49a52e48f0836f11a8ae24c92b97721d3fe619788cc700cb998c19b4f3255c8de4ff00992e46b9d9c657d3998f5ee368750b6dd421c600dc4a90e36e242d0b42814a90b42814a92a49214950208241041c5085ae5ad100325ad52e64b525685a1452b42d24292b42924292a4a8029502082010411163002ca3081edabf674c77173c2ccce7ba41207671adda231f11a95a6d97205a6d00c99e64876e1530b9f321491461e2225e773765b6dc8895c8982db933ce123c00aac3312cb684c3abdb3f84efc41516c66d94dd93ed06bd149b15b7d429d99c006f1c9ea5a6461158269a9d9ada2c48266ca4046098c7702aabd4e89183d5620086a24cd13264d8e41816269a5aa32aa14134d569ee272c964cb53bc99cab80003ba98c14a2e04b54c70738d03a652b9a4963a22553a964ca4d35835fba8d9500cde062a59338278a438588d808d6988b857929524a9a88690e24100a6a40c700d0c2a405cc4a26c89c997326cb3369a7c9a9a798a9331529664d4485cc9339001be8504cc94b5a1603a54410639912320a7009b82950241d142c438b172f66001947e0eb7fad853eef88de22c45f87c9cbb66e06bf582fafa177ccdf3d7ddd00e1f9fdd3d7d3e7b8aafc835f9647c8f524e9778a89639db91b333585c3750400b364739ed5bf5dbd7a61be3fadbc7cbe5cc440397d4e5ab8e1a37c8bc46e770036b53fbfe37b569d70de1a90fc1c3fa91eadc19e0fc09cf5218f9f372e470d00627f3f3fb1f7cf02b1d2f7b0c8ea18e5d1fa341df5232cb2e161d067e0c338008fbe5b733bfdf4c0a85bd7a316ccb3e5ab35c30c8f9fd6da30d5cb8393b00f0062224fafe3f80c3781c98707f57e7c03e9c19cee43d83bf11cedcd8d8f1d3300881cfecd79f615b1e5b936386f0d383f0e5adbafa3c378f17d58f8b8766b8e009cb811e5e7d69fa57cb13bc34bb70e19dce5d2fc9f363b91773e593ddcebc000f47724803f3debbfc81e46a05bb1ae0ffb737f67c898746376cb8b30716bdd00d986772f122febd7efe786f0b039f0f1e6dcfae99c1cdae74b642c73e1c8590085ed6731b036a9e57efdfec72db11bc1c70fadbd3470f77d03c1d9cf1e674600cd9b373e39335dcfef97d06fdc91f46f0e39e4ee3e79b5fd06b070f6e2e0bd00c67fd5b57b97b8c6cf159a37fc35365ea3e5d855fee1cc1187f88a1da495b700289fc52d4e7c7507f3b5013b70a94a2a0a6a1a6def592f34998cba111f39bf00131d918d96c5d5b738052a93b3f8fd528e2d4d252552b08c6e7954c54c1ba100a550e2cadf9b247f874f5a27d3a4ca953a8a44696ba9bbb577a80c859f886700baa37776c9598e05c581022cdb1e528d7c308430843084308430843084541d002fd3f98ea667494e5580f1b4d442d515378f4b6e2d12b92c214ae3e35c521a00792dafc2a44240fbf4a187e67150308e3ad0880b1ccfb3fd8bc47b40dabc2b006670e4ac1ac9c175d549415230ec324a92aaeaf9a5b740912491292b2913ea00974f4c957793d00dd912953e6265a6ce7e23a2523f528f41e6586b199b91c90025996e4d2c90c9615b82954a20a1e5f030cd8480db10ed84254e109497a21d00214f45c4ac7be8b8a71e8a885b8fbce2d7f5db00c0f0dd99c170cc0307a74d002e1b84d24aa3a4949090adc969f8a6cd52529ef6a2a2615cfa99ea1bf3ea26004d9d3095cc513c913284b4a5080c948601b3035393a8ff00364497372498f60094ad3cab7eb53e80dbfad0e36ef9b78fdbc5fe513bbd78331b3b676e7c33b0007ce14fe9e7b917f335f3ef5c4bfc9b33969fb69689ddbf85875eb66cc80efd000bc286a77b58f417ed6ed5fb27d327e1adb2d6dab75833364e40d78bbf571c003835dc3c84934ef7dc8e74e5b5fcbe66f0e395b906e3d322fafda377d33e190039032c868e0f4bc47dfd7ec7e383f4b5bfaf3bfc9a2776c5f3e41fc09e3c9c00699da200e5f53f5f9fd4f235bc93c5bed9d8676be5cb4688ddd380bd8e77f000e17b0eb1ca96a7323714afaf21606b6b57e50e05d87afddfa44ee8e632fe5300fbdcf85f94453d7d3d7f2fa5712fe0395b95f8feec1a1ba327cdd8b1e2cd9800bf11cf5cc29f7f7e54ebd307f1fa7b7814f3d0e608bdce4d930cac750199c0006d41bfe3cffafcf10fcfd74cbf68821ace01fbf3e0ceec395cb32879d77bf600f4ed6fcee7077e1f7e7cdeff00d044848e23afef9116b64737c9a14e77fc7a00d3efd06d897caf965a7bbfdf330dd6b59c0d012faf01d1c7373908ec79772500672ce6ce665e4ecb933cc2e652ca398b3c660302c29c8792e59caf2e766335009c4d6249443c0c0b286db8669710eb6a8d98c540caa09313338f82847f270d00fc8d4637b3d84d7d64aa44e3d8f6118153a94a099936a313ac954c9448490a0054c98896b99394c95264c8953aa266ec99331699404779290a56e89b3512c3009ccacb0003392c4ab2609ba86eb98fa50f04ba0f2fe19784ae1f343606051200f89c83a5f9620b32a10b71cf8bcf5338144fb50a6ee29d52d5ef6779e66b980066eea010d32b8d532c21b61b6db4fcceed876ce67683da86dced7aa60994d800c6d1620ac2921294229f01a39bfc3f67a8a5a5012912a8703a4c3e8e5db7940089014b256a528f0eafa835559533ddc2e6af732004b49dd9490d664cb4a53e000f9c5d263ada312184228a7b3565ea8fe1da79abb190c113ae22f5e75fb5c200363d577e6997330ea9e63cb9a48fbab23fcc6a5fa1d94f4ca4500a154996ca0060c82a24ad5c8ff11f53dcf68749b292a63d1f67db11b09b1522406dca5c43000fd98c3f10daa4240fd2a9fb6b8a6d2d6cf06e2a6aa702c0048dad7fc339120041f869a9e9e401c14994954ee8f3d735446854632078e848c2861086108610008610861086108f5119296def138c51a77729d9b59ee07fa09ea0509dc5ea3100a6d3a55743255c3f94fd8f3cb96b1873a912b754b642b366f855cbfd279e5c0046a3acb8dada594389285a774a850f9f420f222a0ee0e3054929252a041198003eee381163a46b14952094a8104687ddc731631853f6b17b23f22f1d99463b005374d2125192b8a6cad2971524cc486588095ea9c140304c364ccfaeb61b4200e314db6984cb59b620aa2e4abf8797cc1d7e4094b503e8bec2fb79c4fb32af009582e31327e23b135d3c0a9a42a54d9d824c9abf8f11c312ade2258528ccac00a14322a06fcd9294d51de99b7c2f145d1284a9a54ba659b87732893fad0fa600aa40b1cc32af1a0a67bc899c34cb38e63d3fd40cb737ca39d328cda2e49993002dcf211d819a4a66904e16df868a8674022944b8cbc82b622585b5150cebb000ef34eabea36198a61f8ce1f498ae15594f5f8757c8454d256534c4cd933e4400c0e95cb5827894a92596850521612b4a9239c21489884cc42d2a42920a542e0008e360dd357b671d529db71b6dced4adcd6dd7a0eb8ce71e5af0d5b366d7d6002add07ebcb317cdb425ed9b1e11fd2bdbd2bd6d5f2ae278fbe1e7efc6775ae00e33d73b68dadf301f2b3e50a5ebd2d4341f4dfef960f6faeacccdd1a047073006cc6b6be9e39977619d94dbe9e7f7cbc8f4c43b7935f87f488005f561a7a9f000f5e90fbefcfeff4ae113bae6c0fdfc747c8381771982f34af9501bf2ef63d007cf6db7183f47bfa8cbca200e3a799e0c2c6e0b871a7076823a5bc8fcfb5ef00f62b8973adf91f76cb486e9b73d35e37c9beedd600721b935fa72bdeb4a5080017db7bc1f90f2ce27773d3a9be5ab03959f217ccb830a53fb83b0b9b7e7d3b001c1f8fb73a69e1cfa402797272e1b47b81c6c3371a650e5fdebf7e55e9d06100ef84377cb8b7a8605f2b5c3bdb3ba9bf9ec79529f7d6b538973e41be7f76e9000dd272b7ab900e86e1d8b38b5efa400039d6dea3d48eb73f74124fbf2b727d003ef10c3d2cce5edd2ce41372fa30ce3d54f6472ccc9259a482750a88d95ce200062202361d6127c6c442148529b514abdd3ec2bc2fc2c4a13efa1625a6621800521e690b4e9f68303c3369b05c4f00c629d15386e2d49368aae52820a8226a0059336529695895534f3372a296781bf4f532e54f96d325a08854b4cc4a90b000e95062f98720701f15dc5ad6502630cbaa1a7f31d33ce936ca91e54f370cb400454aa3fc0e25b9949e2c1720231b538cb095b81017091dee52b61999c2c74200b4eba987f78af913da06c5625d9fed5e29b318902b551cdef28aaf714997880061b3dd7455d29c0044d95f0ce4a0a9322aa5d45315a972171c6e7ca54898a9006ad0fc259b792725373198d0b8d229f638645a861086108610861086108cad00f0b9a5a3226446b304c9909cc99d9a849ac4871a6bdf4b649e05ae4b2d4ba90079e5255110efaa6b1c91f0af07e361e023a143f286d67e947e1afb381b1fb2000368f1290138fed74a935644c4244da0c100ef30ca3042e66eaaac286275360092b3dfd2d2d44913681e3794327ba9456a037e6b1be6122e94b73b28e4f91b0088b9b23950ded5af2b9e7fa73a571e92de23e6cc05f9b7bcb2d33c12de2ecc00cedad807bf3d35d38d01ea6b7e7cf993415aff002d40da9bee713bc7965ab0006cec06845f3cc7839db30cc3276c9b2b9622e4139bf38e405450daa6b437b50036e5403a72e57a110ec5c7476639e7adcea4f941efc58696bbe7a837bdec7a00c71526b7e75d8536f98a9a53bd4f422921472761a7cb320dbe407177949c86008c73c9db306f9deeeccf6e33e11bdcd79d079f2bdc5479f4a0c378f27ea7a600ae33639f89730723802320e7336d6d6b11a70b171253f679debbdfadea083d0006202bdf8368de0c4373887e9c59b23c85ae1dc1b7072d11e1da805c509bd800ef71535f23d81389de2d7e2fd464c32f31ccf53b3b9cae06841b5ac2fcfa9e0030f08b56ff00403ca9cc9fb1605bc74e7ccf8be807bce1bc731966d997f11900037b69e2d14363436b5e86c05adccdf97317e6713bda3e7a8b3126f73703c000e7e10717722f9338b967b9761c6d773d239016a8e74d8529caa2a2bd4df7af007c5254fe0f9937b93a16e1970176812e58b589ccbe6722c59b2b68dae51045006bbded5a74a5ed6a1bf4d8505462428f2b7419fbb30b3dc81683e5966f620500cbf11a59ac58665b28f0ef63f4a023b6e7737209a5280e0547a58fa8cdf4c800586a0bb41f22e3326f9977bbe42cd6701f321a2aa689e8aea3f10faa592f4600b4972e45e6acff009f670cc9a432885484a42d695bd193198c5101996c964d0000cc4cd677348a5220e572a838c8e8a710c30a50d26d1ed1e13b2782e21b41008ed5cba2c2f0ba7554554f985dc02132e4c9439336a6a26a91229e4cb75ce900f325cb96379401b73a7cba796b9b314132e5875124b06c921c1751364b5c9200006ca376586f6726977035c0ee52d09cb2981cc1ac9c466bef0a390758352f00e197f1d9e1b7f5e72266bcfb9665a6207c44bf204a34ef2ee75858192368650031f2e878a99ce21de9acc22dc1e1ad85ed6b1aed27b69aedb1ad1369b6776000b60bb51da1c0308df1dde173e56c2e3b866055f3b77e0998aceda2c4b065aa00a49519350b952a9d699326588e2d4d5f36b71233d4e24d252d6cd952f447fc001a6a25a8f19866ad077b304803e1119abc74a460c3084536d65cd2ee46d20d0056cecc385a7f27e9b679cd2cba0d0b6ee5fcb1349b36e03c8a17089503ca9800e45b2186231adacd98c1a6242a5e2db4582e18b49c948afc4a9a95493c8a660091e3176423bc9f265ff9e6cb47fef2c27eb1503821cbaaca3c17f08d959c48004bd977864d0792c47f28495c54b74b72b42453ae04800baf4434ebaf2cd54b00756b5a8952893c47b69c4062ddb176af89a4ba310ed276e6b25dc902554ed300e29365252eec944b5a528192520245808cdad56fd655abfcd533d43a19ab2300ca2e871d6718b0c210c210c210c210c210c210c211f922e0da8b478563c2b400ff00a1c03f993dbba4f349f3143438b7325266063623250cc7dc72f91bc59900d253392c6ca1fa54331cb983a8f1ce3a8c443bb0ce169d4d08ba48ff004ad300c9493cc1f98350684118d72d0a96a295781d08e22351325aa528a5418e8742003883ed8d8c6027db4beca59771839123f881d1391310dc4e69e4896b8b964b00a19a695ad594654d171596a3d2d8417f3bc96110e1c973450722662cb6328400697619d91c448bd3df876edc276c0e272b65368ea94ad8dc56a40953a72d4a001b3b5f394c2ae512fbb86d4cc23f88480c894a3f9f9602c54a2a77583e2869001629e7289a798a0c4ffc4ac9b29f3dc27f50c927e3170aded0e62a0e2a022a0022063a1a2212360df7a122e0e25a721a261a261dc532fc3c4b0ea50e3110c300a85b6f30ea50e36e214871295a48c7d3844e4cd42664a5a172e6252b97310a004ad0b4a8052548502ca4281052a0e920ba490d1cd4104383622dfcc199f9b800c9959b01a02de1a7514adb61ce9f2aedcfa03d6a737bf021c93cbcf5bb71630066ab47041233724dc65d727d059c866650ff0053436dcd6be84d01b8dea4e000fd39338e41bd73e39160221c38fa137203001ae356b837b8b004123a76db7b00fa9a5ad5e94268460544bdfdb35b20f762dd40b3c0971a710c6fae96bff98e009980f11e135af737aec2c3a0ef4a0a01e840a890416d2c38bbbfdc17f52d2e002fcd880d99bf32c1eed6e99bcf86e9a57bd474fa57a537dfbe1bc482fae5cb00ebf419646237aca0e2fcf8e6d6259b3e1c89b282a0116aee695b580ef5a7a800e55be0146f9e47d4b936cac7c3d21c4bb16d07472fd6c4bbb877d226829d3d003ad81ec76b9d88e40e21cdf525f9b5f2e633b657be563dce44b3dfd40d08ce00dc3571105229ce95f9548b0df6f5e7cee24294fa13f40e6f90d7d3cdbc49d100f8dc5d2e6f978e59ded6209e7d4935d890694db97caff58de3970000b3e5ad00fc7dde04d883c180cd8837d3c8dce7726269526a3f1dadf9d7bd69db07601b00ae433bfd1baeb9983e4c730e48039be41f2b3b819beadc4a45474dbd6d5ad600fb6c2f5df6ad6ade2d997e832d187cce8ec4160d53967ea742ec1b41e648490002d1212294a6c77a6e2b5dedcaddafeb4951770785b8166f7e1ac53bc5f3cc0072cd80bf0bf1b785c5b27147a5a9cf5915dcc32d602b32e49662e690deedb600bdf4ca4be14393a9738e171952d4c43b1fbd60524c53a62211f8182850fcdd00c563cdbf894ecdc6d86c8ab6930da70bda0d92933eb077691ded7608ddee250046495a378d1a12ac4a952d3560c9aaa6a79466d71546057c91365f789fd7280013d519a920bdf77f52431d407778c52e3e6bc68e18421842184218422bc70e00ba6dfe23ea34b998e860fe5dcba5b9f6604b894298886215d498195ba875a7005b79b9a4706988a87212a7658998a9b71b71b4ac770761fb03ff00f1036f3000ea3aa9266e078494e318eb84197329296627b8a150582998312ab326926ca100fde1a45d54d411dca949c9a593df4e4821d09f8d7c18643fe9160466c49d23003004924924926e4935a9b927ccd6e79e3eaaa4848dd0025290c000c0240b00000000003203a358c720f4be9e1a5b289a13f2ad0d6e00e5d76ed6f2ae277c6500c750d6bf5d3cb5ca1f3f4f7944115fa1e9b53e77bfe836055ede7d7c38103a00d8f127cf8e5971cfa70b0ca248f514b72dbadfe62f506fbd437aecd77bea3a00659b6590b3bb0bbdff004b16ccfcf368505bfbed5e7e96debbf6c37efa8e9c00f216e20f203283fb16f97b6b43a77fbf2e84f6a6d5c378077377f4ced7d46400e73f288f7975d7dfa0876f4e543cc74e637e56e830df16e07ed7c9f906b3e800e225cf906e19088a5f9fdf6fbfa9c37ac0e59e9a5cfefadb9070f7efd9f9320096af97e7b74da96e60d790c3783b7dfcbc2eeecd91d483fed61e17398eaf93006b69a771e869f8d2c79f2f9d0b79dec411cbe6c0b795f4e4b74eb7e3cb3e190037ac453cafd69f9edb7a6f805679d9f4d0f16716c8f483fbf77e57d3c2248a00731cf98e42bd7eedd710160fcedd79b6419cf5e107e9e5f6cfc5e3f4c14146004c636125f2f85888f9847c43107030306c3b1517191914ea5886858586612e003d111310f38869961b429c75d521b4214a50069993a5c944c9b3569952a525004b99366282112d084952d7314b64a10848256a25929049205e0a50624b000300936160333a00007e57d237fef633fb3265fc10e90b7a9ba9929857f89ad5c9002423f9b1e79b4baf69ae528a5313197e9a4b5e527fca8e0b6e12639e221809004c5cfd96656876325f97e023227e5c7e213b649dda3e3c706c1e7ad3b1d80d004cc450a52a2138c57a02a54dc627247ea96c6649c350a7dca552a794a26d5400d4238362f889ac9dddcb51fcbca24274ef162c6611c34960e49bb02a205d7f0019d1ceaf5abd9d99705150d36e2c7394de31850f125d4e56e10789b98c22d4009d8985993f0714d120fbb79a69c4d1684285eec0a9d03663f107889044da3e00c9f0aa4933066838a76b5d9ad3ce483981369913a5a9b3429493f0920d5840001dd62ab39a6850905b55d6d203d1c020f2f28ba9c71d8a618422dab8cf795000dc1e7161108345c3f0d3aecf24f4535a5d9a5c49f9a463b17b204099dad76500d2c87133b45d89411c42b6970c491eb195421eba8c71aaa71e739117cba370008dc0690e95c0b4025a82d37c8f08d81400370d96256ca0002c004a0014b53600c7426d7cd5546d66d44f5b954eda2c6e6a89ccaa662552b2ef777518aa717900d349d66cc3e6b315231c762dc308430843084308430843084308430847e68b00856e2da2daec45d0b1fea42ba8ea0eca49b11d084916e64b4cc4ee9cf43a8300f63af2e6d16a74a4ce46e9b1cd2ad41fb1c88fa80474e7d8721dd534e24852004dba293c9493cd2797a8342081ad5a548514a85c7911c47231a75a152d450a000c47a8d08e47dde34d8fda03f668b1912791bc73e89e5ff7194f374de1a1b80080cb3278165a82cb79b262a6e165fa9b0b0b06c368879666f8df77059c1c52004919c62e1a74f3b10fe668e5c2fd00fc2d76c4ac4e965f66db4755bd5d4121006bd97aba89aa54daca1920cc9b83ad7314a2b9f412de65024103f87cb5d3a500294d1ca0be5581e226624514e53ad00990a51ba921899649cca412501ff482001804df569ebce9f2f3fbeb8f6a6f0b375e274b378bdb26d639272fb7bf61f2006853d76f5208e9dea7bd36008c46ff00df9b1cf83dbd0eac493fbcddf3f7f500bc453b8b7a57d2bca9bfe150313bc3cf4e8eef9379b780261efe7e90a7715100cb7dfcac7f2e57c029f3e2473e4e33d5acf78687c3df1f2e7120791fcbb9a600c3afd79605438f3b6a082d9eb91f4bc1fc3a787d9fac0803a8b1ef520d29c800539feb88df0fc7d001c78be9cf96a26e750fc9dbcad9787088da97fbdafdf900dbb5f13bc1c8e9d188e3c39f36107cf47f201f97be5934db603bd6f53d4dad00b7f7c029d8926e6c2c3935f36e475cb4025cbe7d7e5eda14db957ad77e7bf700f41e58050677d6fe397f5d40367c9f6e0df2f9da14a76f3f97df3c3787b6b5009d8b9f796707f3fb0e7c7f6688f2fc3b5fe5d7f0c02c11adbc583dbcf875ea005a6bf4fdb33c627c239d3a8f3a57cb737e97a916ab7eec1f81f037b0be5e16001ce0fcce4d7f96b6f6d0b8351629a1041a107911cc11f4383a540a480a0a040014b0208d4106c41163983d20fe5c0e5f48c3ef113a6c34e351a64c40c3867200ee622e4fe4096d284b30cc45bcb31b2b6d2db4d36ca2571def988587016b6a0058a9729d716e38a59f953db86c01ecff006f711a2a69265e098b156338110100225a28eae62ccea240400940c3aac4ea4972d5fde0a5452cd5bf7c952b8fd50049ee67103f42be247207316c98b81c9a28463a7e31a18421842184232dfc310069f7f02e98cbe2e32194c4f73816b31cd3dea9a53adc23edd24507565214db006c4ad4dc62a16254b89848f99cc5a74b4b261d8fa67f873d87fec86c052e21005720c9c636ad52f1aadef04bef6550a905383529521c8962897f9eeea69336004d4623512a6250b49968ded14aeee48510ca99f19cb2fe416d377e263705440045c4fdfcf1dfaf9721f53fbc65c3ef7de9d7e74fb387bf7d323cde10fbf3e700f7df124ea5b8fd49bf5be90891cafcc7a7ce9fa753812efcfd9842a7ad7f2f00d36e588fa7b63ef584460fcef6cbcdbebe1e210afe5f4b0fa6108797afddeb00f4c4bb7af2626d6f7d34844efcfd4f2bfcf9d6d5c43fafbf7e3088f5fbfb030007f3b9f3fa7a421f973fbe94e9cf13e1ef97eef08621f9deeda1e248e3ccdf00d2c8d9cbf67c7d9d2cea9e76778d7d5fcbab7f21699ce972ed1194cda0d62000b35ea44b948547e78433170fee66326c80b5261a4f14c29c875e78f78e36fb0071d942261d5e3afc53f6b2bc130d1d9de0354118a6334e276d1d448980cda200c226b8958715217bd26a3140ea9e8504a861ac14932eb90a8e3d8e621dca3f0029295fde4d4bcd20dd12ce48b5c29799c88468cb11ba38054425209248000b009249a0007324d863e7a67947100092c0393600664f08b04e3421041f121ecd0012b03c4e710dada1d3bff9cae0eb5f4b290ada884875341fea2a511dbd49d8005cb12fb3afc460ff008c3d9fec6289ff0048ed6f61778740e9bf1ead1c9b0f009224d06260dd6aa790547fff00329fe1cee071e2e6ced162bc5dfb67b86fe100df3acef46b4ddf91eb7eb4e5d7dd97e699543e7694e50d3dc81330d20a607300767788869cc74da6cc3ce166639734e72c6789c49a26162e5f9a1196a3108400afd11d927e0efb43ed1308a3daeda1fcc6c46c6d7a113f0daca9c2aa713c77001ca62a2f3f09c15132925d3d2ad09df915f8f62183d2d5cb9b26a30dfe2325004542ea30ca8ee13533a4d5094b732a5d3524da9a89c067ba8484ca94923f4c00caa9d4e85e72cae31ccffb5738c3d64f8883c93ad7c31e410ca4baf4168e64000567dcdd2e6d64009984e751f503344a6add4201734ce54bf1925c40346c7a006a97f08fd8fecb6e4ec6766fb44da1def844cda7c711836153d41fe2914bb300d8261d56378dd86d054a583249ba8e92b7144e1dfe26cf624804b266e273660053ca51d3765c9a6960be7f0d5ac364758a479eb54788ad61904df2aeb3f15300af59eb2ce6297c6ca67f96e593bca9a5797e6f289943b90731944c20b463280069dc6c7ca26108f3d091f2e994ca3e1a3615e7a1e250f30e2db573ac07603b0033d90afa5c5364bb32d8cc2712a09f26ae8710aba5c4f696ba92aa9e62675300d553cddacc531d9522ae44e4226c8a8a791226499a844c96a42d21435476b600ad2a4aa968b0ea35248295a242ea56082e14f5b3aa52140b10528490406ca300c594a7faab90e32063f4eb89be29f284ce54cb09972e17892d5ccd12e8465a004866150fe4ed40cd79c324cca11a4b21a6a0a759626301e06cb26154d85231007317d96d86c7a4d45363fd99766589c8ab5acd46f767fb2f86d54c5ac954d5002316c0f0cc2b18a79ab2b2a54ea3c469e76f10b130299504ed562c59537f2500512c9ba6661f4884a8e64779225499c9393ee4d42b2bc649b87bf6bcf117a30071b0125e2a656c7119a581d4b31faad90f2bcb32aeba64e8471c0a7a7599f200148040646d5192cad90eb9188d3f9364ace4d41a522599533b4d47b889f2df00697f831d83da9a7a9c43b27ab99b09b4810a992764f1dc4aa712d8dc5a6210007768f0cc76b8cfc67672b2a57ba994ac72af17c2553947f318960f4bfde4ad00cd1e3187620532a62461954ab277a62a6504d512c94098b79b4aa5120033970036482e573a5272d8eb4bb54b4f35af20656d53d28cdf24cf9a7d9d654c4eb200ce6acbd1698c96cce05ff125401a21f848d838843d03349547330b3493cce100a2e5735838398c1c542b5f35369b6676836371ec536636a709adc0f1fc1aa900747896178849326a69a7a18871744d93365a913e9aa64ae65355d34c95534b003a753cd973579f325ae52d52e624a1682ca49cc6a391041052a04852485249000418efd8d14510c210c210c210c210c210c210c211f823e09316d5a81e402500b55857fe828ffcd57ff94d0ed506cce942626dfa85d27e8791f42c78838f5100244e45acb4dd26d7ff0049e47d0def91a519e325659d43ca39ab4fb3c496130030651ce5219be56cd12198254a849ac8e7504fcb6692f8808521c4a226122100d6bde32e36f34541d65c6dd42169b5876215b83e214789e1d51329310c3aaa004d651d4ca2d32455534c4cd95312e08251310094a814a80295029241d5254b0095312b4929992d61492334a925c79111f35af68bf0559ab810e27338e8dcd5003151f92e354bcdda479a62005a73469bce2362d32575f75286d0a9dc8dd87800acb9991a0cb004ea5517170ccfeec8d973f11f5e7b28ed128bb4bd8dc3f6860041972b1096050e3b4482c68b17a7420d42529249fcbd42568aba453a9e9e7a0010b577d2e7253d8341588ada744e4b057e99a9ff002cc006f06ff29cd26f6200cee0c5897d36edebf9e3b2dcf1cedd5cfd4fda3321fd7f03f8ed887c8797a900f7fb421f7f7f7cf07e9e9a7bbf1d610c1f8ded6bfbc9e10fe9bd7faec3f0c100d99dbc72d4fcba02c1e10fb1f3ae00ea2fefea3d210fbfbfbe5838e3cb3e9600f36f4842bdbb75f5bfd83b5af89de193827a87b6873b06ca113ca96dfecdfd002bcbaec0e21c72e1e3f784457d7cff005dfefbe25dbdf1b6ae3873b0bc21f300dbfbfa73fbae04fbf6dfd5ceb0854efd3ee9f21b1b6201e07c8f8fefeb08b7005e27b4fce79d318f8b83865444f727ad798a57ee8b2979708c35e19ec182f0002a5b4f4ad2b8d10b0ea444c5c74ae5cd33ef56130cff00417e23761ffb5fd900fd4e21492153b18d9354dc6a8fbb4cb3366d0042538cd2ef4c20f7668d1f9f00eee59ef674fc3e44a96998b5842b12b6577b2490e552dd619ae3f985f95ed70024001f28c48e3e66468a184218422a968be4456a36a465bcb6e34e392c545f00ef29f290db8b4372496522a390f38d50c3263821b953312a212dc647c30a2d006a4b6be7fd976c81db9dbac036796952a8a7d58aac55404c646154293575c90052e5fc524d4ca9468a44d2521353532012e4037a9e577d3508d1dd5c929b9c00b27fd2399119a74a6802529094848094a404a4003f9424000240000a0d85b900d31f5a901129299684844b9694a10840094a1281ba9421202425294b200003000160cc07228107a6fb53f2ebe7cfa9c57bdcce834cbd4b87b3126d784484de00fcec3af315a56a29cf96fd8182a7f2b8d1c8f1e1677be99c1a2684f2b54f230073e5bd06f4a5b6df13bd717b8cdb5700e6e35d1dad7c808471a11f8f6a5cdf007076e469db12e48f4bdc9361717e37ccf2e289f0efbdbe97eb416a7ebb569400951d5f91048e3a1cc3e7c38b30844015adbf0fcfbd36f2e7897b35eeecdcb20037b81ad8b5bac22694dfa5bcf98a53d3901e76c52096b3db98e034d72e600f0038471a1fbfbf9f4c54157cfc4b662f9067b0b1605fc0031e11245fcf6e43c80073ed53bf3e78825c93e80be9e16b5c8d0b4204115b7a5edfdb7e9b57a60157007e36b9f120700f913f784477fcbe7f8e249d3af22f7b11606fd7835dca2e4f00844e19f39f17dc44698f0fb9187b89a67e9f221a673971bf790b95f2acb99500cd335e698d495b695b121cbf091f1edc29710e4c631a8595c3154646c3b6ae0023b73b63876c26cae31b53899df9386536fcaa704a57595b30a64d0d1cb37300bf5354b952d4bdd2994854c9cb1ddca51162aaa114b2264f99fa509703552800d9291d5567d2e748fa6de8e693648d08d2cc83a3ba712862499234e32bcab200ae5e8065a61a598395c32195c7c72a1da61117389b44fbf9b4ee64b6c444d200711b1d328b5391514f38af8f3b418ee23b4d8de298fe2f3d55388e2d5b3eb600aa6a94a5013272ca84a9416a51448908dd914f281dd93225cb9480108481d70053a6ae7cd993a6175cc51528f32720fa00c00d000348ad12783f11316e0b240094b20f356ca73a109ba53b8f1788d8a41c61534b73de1d0b2473d4f86439f30011954725cf7aa1649210389d55e190b33bea230e1c5d49673c7b710794f493004db31667c89a51c1c67f9d4d35675bb23cf0c8f33669d5bcdba5d997224f38007ed399e34db8ec9e1e45a79a931511ab39fe4ca5cef2f4d6772cc9d93e3249009d6573d9ce59f7076435347d86ec162bb5bb494187635b4fdae605474bb2bb00178cd10adc3f0dd94c2b6970ec728b6f36828d4a4a6a975d8fececa97b2d81005584d1d7d35154e2d8b49acc1ea6869312e472e6ff000fa654c5211367d6cb0048932668de972e4a26a6626a66a5fe252a64a024ca3621257302a5a9295d6900d2ce16b4c787ccab0f95b46b4d32364290c0c2b2d39079424d0d03309898560012dfc74ee66615338ccd3a894b7ef23a733b8f9a4ee6b14a722a3a322e2de7001d5f1cda4ed436876ef1399896d76d16378d56ce98a526762d5732753d389a00b2aee28e9c4d34986d1cb2a6934947229a8a96504cb912a54a42529d4544ea00aaa5999513973964bbad6480e72482c94243d929094a458002d16c33cc83c1009f1e994e64f4d32e64ed487a43315c9e61354cb66991f59f4bb31c1be5cf8000898e543e57d5fd27cccd2e1c3ca96c69cb9318e972c7c4424649e394989ece00a0c7bb5dec3f15a61478962fb3a9ad902ae9e9d3534d8c6c8ed2504d437e620054a4ccc4b65769f0e5099ba2a24fe7e4499e3fbb9b26ae4832ee955761eaee00d7bc844c4b99530227d2d44b21ae857794d532cbb3b2d2142c42936c37711100c3a6a1707f9a6532eccf3b98ea2e8766f99b526c81ac5328684627f97a7f1600a599769eeaf09643c1ca513698f8550b9433d40c0caa519aa210cc8e63032f00cd4fc1267ded7eccbb51c0bb5bc3aa0d2d253e01b6d85d32aab19d97a799310054189514a03bfc6f65ff0033326d49a7916998a60d3a754d561c82aab913a700e1c99a68f8fe298348a89336bf0c9424cc928332b30e4952929969fd75545b00c4acca45953e9d4a5ae482a9a8519014255b4e7cc85153d8b97e70ca13319600352b2d43bcde5f9f84b8b8099c12d7f10fe52ce102d1027794a68ea68fc3b8000c6c9e25d339903f03356c3ce76be138b4ba5973b0dc4a47e7b04ae5a55594008e04ea79a06e2311c366a81fcae232127e158feeaa65a7f2d5689b4eadd1a500c3b1114a2652d54afcce1b52a49a8a72c172d606ea6aa9167fc1ab943f4a8100089c91dccf0b9458773d35cfcc6a0481d8c7a5ef487324963df90e71caf16e0021c8dcb7992052d98a817168b4440c5b2ec3cd647324252ccde471f2e99329004a227dda3578de10bc1ead32933935745532915786d7cb494caaea29afddcd00483744d96b4ae9eaa412554f572674851251bc6710a234539294cc4cfa69f200d33e92a500845453adf75601fd331042a54f964954a9c8992d574b9bc0e0cf008b49b7019abc731bf1510be1735533141b7c40e4d690b7e0b21cf268b87954001f10f94e0c3886e55112770c0b7ac50b08130998323c3c4e69760e2732e5780055cc3a1fb7aec628fb74d9132a9244b4f69db2d87ce5ec4e28e113b1ea1a610032aa76c262737754aa94d527bf5eca4d9bfde50e33325e1c99d2b0fc4a709300c9701c4cd62118555ade7a52461950b507701ff213544ba913188a47754b9c0044907bb98023712848b858f85868e8189878d828d87662e0e3211e6e22162e001625b4bd0f130d10ca96d3f0efb2b43acbcd2d4dbadad2b429495027e31cd90053644d99227cb992674998b953a4cd42a5cd95365a8a264b992d60291310b00052b428052540a4804111b5208241041058836208cc11a111fa316e10c210c20010c210c210c210c210c211e866f05e21f16da4f88503c00dd205039e69b05700fd1a13409271895329c7789171fa9b51c7a8d79748c0ac92e3bd48b8fd607000ff0037864795f8c614fdb57c09b1c64f09f39cc394a5222b5c34121a6da85a0072e42b2954c33149e1a0c3d9e74faa88688898a198a4b0626123816034ebf900c24997e1c44310917300f777fe1e7b4a56c06dbd3d2574f32f67369d7230bc00582d47baa4a85cc29c3714bad0847e56a2699553314e94d0545528a54b972800a6ee0f5a692a9285a9a44f225cc7c92a3644ce5baa2ca2dfa0ab911f3d322e006d4a1a1e446c0f237001f1539de9bd7ea7ef1d6ee9b35f366cf37b1ccfdb9d0043c276feb4ee680ee2b6ade82dd4f7e00f57362ce5c581e801e41e1114e9b7005bfdf97e03072381c999b4f9062464390cc9429cebdac0efd3a7d76f960490007ec5b419b87272cf3219a10a540a7af9f217373e43e78057124d9b36f5079900b9bd865a1befeadf3850fd69ca95e95c4bb80e74bf121f47cc96d79e65c04400f84d09e9b5ab5afe95b1fceb8a4a9cf266cf8ea6c7c406d1b2108114f3de830061bdbf0e7cc62a0a7367cf5be60f3b6ba11a9c842228477adfad39efeb4a79005b6388de275019feb67e16d0924e5fe9429ce9f3af3a6d61b733b7d2b1bce4003e57b1b81f52da6ba73844f84f2e7df6e5d05c9a81bfd2b89273d74677198200e3ab5c301cc5842047a5ba8debd694ed4b6c49237c1f36f5b177772c47161900682ed08e2a42569521680b42c142d0a4852549228a4ad2410a4a8120a482080034208de95a51325ae54d42264b98952264b584ad1310a052b42d2a74ad0a4900dd5255bc1492a490438067d1c657cafa17b79c616759f222b4e75233265b6d00a5b72d4c5fef191294871285c92663e2e05b69c72bf102042d72b7e2104a1c008c8089144a92a423e4af6a3b20ad87dbada0d9e4a0a68e4559aac29452b09500e155e91574012a5de61a7933451ce980949a9a69e013bb1c76a25f753968d100dd3fed371e42c798314b71c022cc30846463829c95f0923ccf9f63219d4bf30088b465e93baf30eb3e2964bbddc54ce2211d594b7150b1b31761a15c710952001a8b91bcca5d2ea1f6dbf717e12f657b8a1da2db3a892b4ccac9d2f00c326200e5a90f4b4ddd5662532529569b2a7d52e8a495a414267504e97bc56998946d00b0e96c999348ba8ee0ff00686529b912de296e317cd4dad6dfd6dca829b93b00016a9dce3d8cff0068d9bdcb74f0e1ac4d0f3e446d716f51cfcc0b52b8023400e6f66e16f97025c8888743cf63f5fbbec2c37c1c71f7ec8f38408edf96e6e700f03d6a37c4bfbe1ef86508809a1af5fbbd49ef7b73ea0604dafa7c868c3f7300f53fdfce269defdad5edfdcf5b8ad310e2f91fa7be70fdf9e7114d87216dc800b1a5bbed43cfcf72897d753c58fa37bf189a0b741e7bf2341734a13f2b1a8c003f6f7ec7941cfbf7ec5b28115fc3bf9efd850d395ab6c4bb79ebf2e99bb58f008444476341536de94e9fd36e605b10f9338f7d037d3c610007cafbf981cfa700cc0a1a6d83e9ef9f83df978b912f785056d6a54fd2941f85ab4e8374c83f6e00567cf8b68ffb19727dfbf6fc63761fd9d0e0b9bd30d10cc7c5ee71959673be00ba08acb5a7a6202d2fca748e45344262e310cadb6cb0ee75cdb2b5c628abde00a5f92e5ccbd1d08e25998bc1df9e1f8afed08e33b4549b09413f7b0ed9b28a00cc53708299d8e54c8265cb2a04850c3e8678961b74a6a2aaa65ac1329053c300f1fabef27269127e192cb9841ce629361627f424f220a940e41b658876151200f21946ea373ff3522ea51f2153dcd00b918f23a1056a091a9f21993e51a1960083316940d4dcf01a9f0114af8b3d693c3870d7ab9ab92f85663b30652ca111000d9064af34e3ad662d4fccd130b9474af2b2d0d254b23356a34f72c65f5384000435fbccbef290d21c5a7b5fb28d8a4f681da1ec96c84d5aa9f0ec4f1594bc0072ae5a928561fb3386cb998aed3e280a884be19b3f4389e20dfa9669f7520a009401e47474e99b3a4c8fd282a1beab7c12900ae6af4fd12d2b59e2d142f87800d1d81d04d19c89a5b0b1eb9d4c32fca9c8acdd9a620bcb8ecefa8798a362b30026a3e7e9a3910e3afae6b9eb3d4df3066b9915aca51193775a650d43b6d32d00f63edf6d6cedb8daec6f6966484d153d7d5265613864b0812305c030f932b000ed9ec0a9932d294269704c1292830ba764baa552256b2a98a5ad566aa7aaa600a26ce2182d5f0234972d202654b0d6dd972c25039262b4e387c63c59af123c002c39a893483d69d119c4bb4a78a4c9f0a84e5ccfc9845a72f6a549213c2eb900a47aeb2d806fdee73d359fa5a1070f1af35159a34e23dd6b366428b8299c2c00540cdbb7bb3bed3ffb3d4f3763f6c692a369fb34c5a6938860466835fb3d590035d29daad8aa89eaddc2368a84a8ce5c942e5e19b41212ac2f1c953a9e6ca900f499b4d5494a0d35524cea3984ef4bfe790b3ff1f4ca3fe1ce4e643844d0e8009a0a4b8a4f96271a77c69f0ff9a72be7fc98f4ba1a7cde63d2cd69d29cc0ec00244662d36d44903c25f9af294c2261c2d0c4fb2b4e510d3bca59a6010d263e0005595f3e65b7440cca5118ae678852e3dd906dd61989e038c22a2650ab0fda006d8fda8a04cd9741b43805724cfc2f15a796b214ba2c4e90cca3c530d9e5460044f1896098824cea7aa9431e6cb9b8755a4a161451bb3a44d483b93a4ac3a1006c7344c4ba264b5647bc94b0e14235e8732c669d30ceba87a1b9f62dc98e7400d19cc89cab173d79284af39e548d97c2cef4f7503c2851407b3764f8e96c5c00edb692db1039ba1f32ca58406e5a09fa4186e3786ed6ecfecfedae0d2d323000cdaac3cd78a249246158a489f328f1dc15cdf730cc524d44aa452895cec357004154b2f50c3886d0504aa4ab44fa64eed1e212cd553a0391215bea45452b90002d22725425dc93215254a254a3149736ad3a73a8f95f532187b990e6e8b94e009aea4b481e1607ef68cf85d3fcdefb6d84fbc8d94e658987ca71514e7bc2240099a7de44521e4cc2e1f9761e938d60b5f81acef55e1d2ea31bc154a2eb229e005efe3187214a7dd95514285e22896187e6b0fdd47c752b0bab0e51c4282a7000b5fc53e9513711c349babfba46f57d22493644da64aaa9290dfdf53327e29c00a0ab8c8a85868e8688828c6198a848b61e858a85886d0f31130d10da9a7d8700da70290eb2f34b536e36b4942d0a525408246385cb98b94b44d94b54b992d6009992e621452b42d04290b4281052a4a8052540820804178d4a54a4292b428a0056850525492ca4a925d2a045c1040208c8c6c23ec5ad7a9ae7ee1db33f0f9900be60fcc738709799a5fa77289847457c44c279a2398a58a9fe884d5f4ada6900f0dc8642d4e349be2dd5452e671ba591f327635d8b898a661fe627e35bb3da005d97ed328b6e308a7453e0ddaa61f3f68e7c89328cba7a2db0a2a9145b6549002c8529055595cba4da832d025269a56d2c8a74494ca9729733b245426ba9a9007114b0555cb3f984866456c93ddd5061977aa09a94a6dba8a848c802731d8f001bc510c210c210c210c210c210c210c21104050295004104106e08228411cc0011638679c0804104383620e447031d363e10c23e522bee97553477fe5e692700aa0d8d6e4515cf1ac9b2fbb5901f74dd279707e20db8e4758d34f95dccc20700e93f120f2e1d41b73b1d63e78dedb2e0b4708bc63e629c65595181d21d7dfd00e1aa3a7e186dcf8194cda3a37ffd20e4f697ee59619321ccd14a994be5f0e500c6a5d95f3165c87f785c0f04fd4afc3c7685fdbad81a4915b3bbcc7b667bac001b14de23bd9f2654bffc195ea4ef294a1534681266cd50499b5b4b56a6621f009ae1159f9ba4485179b21a5cce2407eed4f99de4d89372a4a9e30f3416b77a009e86d4bf6001a8dba1c77c93c75f0fda36ae7df57f662696a6fe77e7f7e5d200987bf3888e2050dfbdf6e43fa814a500c4bfbf6fc89fd844bf9dbebef2bfcf0090efd4f7adcede47a8b536a8ae21fc34f4bea7317e9a0ca222283e7bd7adb700a5bbf99dc570f4f941cf189a0e77af6dfcfe587d211143d06f7efcc9a546e4000a549b74be0e1f5e06cdeedaf3e61e5cf97bf7c224002bfd7602c3f1b5ef7300838f7c783fd3af38393ef8dfdf965022bc8fe1cfb1ea2c7f0db00787cadebe00ee0c4471a0e9ced4a8de8795eb6adc5ac3960ee7325b521f8daf9b1cf875b4004bfd3d23953715dc934fcb6a53b6e6b415be0e3e9e36ebeef6d2220802a46f004bd36a8da82f7036dbf3c1f4f1f7d7e9ca11633c6be4a31725cb19f6121de7001e93c53997a70eb4c38f044b6625c8c9644453a8253090b07324454221c710001b762e76cb25d4bab61b7bc71f8b3d95efe876736d29a52d4ba39abc031252002595814b53ded661b366a93fe0cb93528ad9256b1b8b9d5d225ef25650999a00cc465b844d0e48f815ad8b9493c002e3aa80eb8e7c787a3550c2119c4d31ca002de45d3fca79552c965f94c9a1533141712ed671120c74e9c0b4296829766d001318e3694b8e21b6948690e2db6d2a3f5b3b37d9c4ec86c36cc6cf84144ea100c2a42eb52b20a86275bbd5d89dc5b7457d4d426582484cb4a13bcadd04f2390008eee54b46a9487ff71babfeb131df29f669d6db74b77ea2bb7362a246679e007cb2bebabdb8345d89fbdc7e36faf961bc78fab71c9bfa3e6f0881f88e63bf002e60f5ada95029892af982073e760fc33f368400e9c87ddbee9cf101478f1f007d79e6ed72f088b74269f75bdad6f95af5c013c588cb2e8d7c85fa0bc2269e007b1af3f2dcdb7a5a83d6986f1cf5bded9331f1b67ae41998a1d87dd7eff3380090a3ccd8f9bf4d071b0bc207cfe55adfe5f9ec69c8e2378f1f65ecc6de9c9f004843e7e5f76f2ae0f6ccdf37e8c7c19b5f0e088fd4d683af5dcf3fb1892ae0004f8b9d2dcb9177e2e74472bf6f9f7b73fb17b5b11bc78eafc1cb6acd9f2f580045c6f087c3be60e2bb894d1ee1ff002e7be444ea3e718195cd639853495c97002a40a1e9c674cc00ba0b4a5c8329cba73376d95057c4bd06dc2b685bcf368500713dbadaba5d8bd92c7b69eaca77309a09b3a44b50511515cbdd9187d31dd200e3f335b369e4150ba02ca94424122c554f4d353cd9eaca5a0900eaa36426d700f89440f178fa7de4ac9d96f4ef27655c8393a56c49329649cbb25ca79664f000de230f2b90e5f9743caa5300d1714a71688581846190e38b5bae783c6e2d6b0052947e3ae235f578ad7d6e275f395515d88d5d456d6542db7a754d54d5ce9f00314cc015cc5a94c0001d8000011d70b5aa62d53164a96b515289cca945c9f300315364d0deeda544287f33bfca8dea1b49beff00f3d42bcea12920df155323007525673565fed1f73e803671b1a395ba9330e6bb0cec91e1fcc6fad806d62c0037da231226797f851d317fc025dab9c6e684ca669e2405a970ba43099d38a50086874836098d9af0ff002e8188343583898947fbd51e8bfc3dcafcbe25da8600d2201351b29d8c6dc55d3328a409bb573707ecca64c245c9934bb77513e58f00f9d972ce91baa3f84564e19c9a1a829eb3b72974e02a09d3e915d31c5a355000c210c2118e0d5396234438d3c899ca5c9542e4ae31e4530d36ce52f866690800debf691e549ae72c839ca2490a4b7199bb4732fe74c9739896d6d18a1a79a700f0cb69e2c975bf44eca55ab6cbb20c6f07a8226e31d92d6c8da2c227cc5bce0056c2ed56294d84639844b0e0aa4e13b5b5f84631492c8509471fc76605242f0074e691f99c3a60379b40b1310a399a59eb08992f989750a44c4862ddecdb870068c6ff00b52723339635af87cd69840b437a8f24cd5c3fe6a4a57e169f99e500f849aead699462d01212a5c040c0eaecbdc538b53ab33a8043212db2ff008b00d3ff00853c7d75fb31b79b193483fc16a70ddb9c30b39449ab994db33b432900dc90274d9bb31390129091f959e56495a1b4b89cafcce07522dbf875449ad900659cf7550a4d1d4a01d3796ba398746945eed18e8cfd9599cef92735e517d40086c66290cce54cc42d249828c8a84751013168a7f9db8896c77c3c7c2bcd9400bac44c334f32a4bada143d438457ab0bc530fc4520abf275922a14807fc595002e624ce92a05c144e95bf2a62540a5685a92a052488e1f4154aa1ada4ac482007f2f3e54d5247f3a12b0664b2ee0a66237a5a810414a88208263dae93e6a7b003be9a645cd716a49994eb2bca22a7284a423e1e7c9836d89fc1a9b4d9b76060074cc7c1bcd8ff8b7585a3fddc636d061e9c2b1cc5b0f9608914b5f532e9892004efd2198a5524c04dd499b4ca95312a3fa92b0758cbc4e993478856d2a1fbb0093533532492fbd20a8aa42df50b9250b0750a063243ecaed42674efda1121c00b5131512cc07113a0da81908c2362b0d199d34966f26d50c96ec40d82a0f2700466b2a5b59350b8b436907de288f2a7e30766d58ff0060b3718952a5aaa3600036e304c6153556992b07da9a5aad9cc5912ef7efb1797b2454902e99249fd20023936ce4c3370dc429cb7fc16a69aad17c91508994d3d87fbd1499737d236b008c7c928da4308430843084308430843084308430847e1984288a875003fcd600eab68f3240ba3ff5c0a74af84f2c5a9d2fbc41007c42e9eba8f116ead162a200577b2c801d49f893c5c6607fb85af6768c30fb69383fff006b4e09f3bff0fc00ad31daa5a23f11abda76504a22e3065f827ff8db2d3650d3af447f1064e33400f8196b7e011d99a5b973c6b48601c76f7e1f76ecec3f687878aa9c6560db4500bb8162a0dd12cd4cc49c3aad4e4047e56bc49ef669732e926d5b03bd16f07a00afcad62028b4b9dfdd4c1c1cfc0a3c3754ce74495718f9ddd0fdf2e57aff00006028063ea8b902da105f9b37be373ca39dc3afdfdd0dc56d6c1fa9bbfa5c6a007d59b4842f5ebf9003a7cff5d86277ac1f89e0efc74e27e562ca843eff00af00a7dd45711bc7d5fe4ceccf970b69a321f9ed4ad6ff009efb72e7bd01446472001ec5fc1999af630877f2e9f80b79d3f0028de2cc6fa87b9e17f5cc78334220008e5dc1e5d41fbe7e6700aeb97139e4e6f7b7ec35844fcfd4fdf5b5b614adb000de36cad76035160ffd6c398843eef5bfebe7d6b53d0e5ee48bdd8f3be5088100cedcfd7ce86fd47953b61bc789f3b5b4b01efd11cb98dbeb4f5f4e9cbb93880073f5e3f373a42389fbb8e5ebf434b56b6a5677ae6fd06879116cf8db5b398400745d4eca4de79d3fcdb9556c97de9ac962d12e425c4b644e21902364ae15a90048484b536868371c429c6d0eb685b2e3886dc5a8708ed2366c6d76c36d360100b8a9b3abb0a9eba24254028e2746d5d860049fd3f9fa7a70b49237d056824600f16b53d1de4a988cca925bfdc2e9ff00ac0460eb1f25238e455ad09cb2336e00ae64593ad61b6133b666f144a14b0ec265f6dd9ec442d10eb4b418e6a5ca82004bc95d61d5101ff0381b2dabb0bb28c03fb4dda2ec9612a29ee578b49aea9000b4952665261295e2b572581041a8a7a2994e955c217352a2084906f53a3bc900f2d3a6f026cf64fc447880cfa3c669c7e57e83a1dad4b57af5be3ead1573e300ae572385c64435db8ebc8a23af97dfdf6c37989cfcfcb8d98b866e17ce10fb00fd7f3c40531bbb7526e3c4023d0f9ba248a5adfd891f7dbd713bc2d98cae4800b3b1d08719bf0cad94222df4e7d7d29cfe9cb0debb8361d6fc7874625b80780043ebf7f90fc296dc02b8e8e49e3ea38f9d800d644dc57b7d2ff91e9dfd5bdc00ceacf77161a5b8b677172d9a23ef6c40564f9824e6757b331f0feb087a6ffd00ab5e42be95df13bd6cfcec6fadb31c19ada6461134f5ebb585bcfa1bd39dcd000d30df2331c4e672c872391c836a2ef0857e9b569e7d2f7ae242c3e472cce600cce3f6bc202dcabf8f5b1b91d7a53b629dfebe27c1c59baf33ca11b69fecce0070bed38bd6de2fb304bd0b5b0a6f4434d9d8a824a8b2e2db95669d449cc0bf0010d12877dcb994645091f2f5a55ee9fccf2d7dc2871e671e24fc5d6d928276007b61696691be0ed162e11314378033e8b0b9131285314ef26baa572a686de400d24d425c25438c6d0d4b0934a939ff007d3189cae9402069fa8907fd240d4600dc70cc9887db645bc6aa13d120152cf984824753418f10a1256a4a46a7d332007c0031c665a0cc5a503f98f901727c0031de129084a529144a404a40d800280007a014c6d4000002c00000e4328de00000006000007002c0463b7da290eb9600ca7840d48710152bd2ce39b442613959710dfc3c2eae4875178619744ff39100e34b398f5ea46b7928aa8301d729e14288f42fe1e6626a2b7b58d9d4922a76009fb14db4a7a301255bf37652bf67bb4aa8976058af0fd86ad08258199b897700508cea2f8935b28673682784f332552ea88f14d3a9b48af78e271aa86108610008b11f68d25d96f0dace7f816c7ef9d24d70e1b752e5318281e97b126d79d300e9666d7995923c098dd3e9de7092c70d9f954d63e1560b710b07bbff000f6a004d4768a7029caff81ed56c67687b3b5528bee4f5d5ec463d53852163f98c9c007a8f09ac92ff00a2a69a44d17960c67e1a37aa1724fe99f4b572486772aa6900aa45b889894287050062cefdac523838de172479a1e682e3f4ff00883e1fa700d287bc452a857f336a5c974ae68e248babdf65ad449ec22d07f954888350680031ddbf853ae9d4fdaaccc390a22463bb15b6f415686044c9741b3f57b474e0008396e6218151cd045c14003331af949132462524fe99b84e24e19dcc8a59950068f29b4e850e603462271edc8eb88a5fc39908d3c99c0a7fd12ad53d6b963700d0350fab99d14ca4744a5a710800592134140001bddb4be33266d9ea301d98009ea6ff0032f6770cde7e6540927324deef1c871d735b256739b85e0f30f5560015480f9904b9bb98bebe0c496fda1dc08bc81fe61d56d5985246ff000f11c2009ebfade1ff0054aa1d92a1b129493b0c79f3f10602bf0eddb4a55fa46cfeca004c1c3bc4769bb0e9478b2d4dd4c6cb65897c613a1c365a8f54e23420796f1b00e7eb1b92e3e2ac6f61842184218421842184218421842184218423a94e211200dbca25014cc4a545492014152aa1d4106a08503e2a114214452831af9e832e0066f25c051de0438650cd8be6ff00106667602d1a9ab95ddccde1fa57f10d19005a8ffbc32cdb478f9aafb56384f7b83de36756b4ee5f2e76072066a98ab54b004a9cf866a160dcc879e62a323e1e592e6d9529021328cf9a9f6496c90db8e700f0dfc429a421f6fc5f57bb17db84edd767b8262932689989d14a18363437ca00e60c4b0e44b94b9d354abf795b4caa7c418384fe6c242894284737c36a855d001ca98eeb48eee6ddcefa00049e6a4eeaff00e94639edf7e57dbaec3b6fdbb5007786af9be7a70d5f50d61d34cf88a0befb8fbdc5ac2bcaff00390ae2ceed6e0019be9d2e6cf70e2e853bfad6bb797e97f2c37f91d357f5bf2b75079a27ea29005e7e5f2aef4efe78055efc00777c8124fbe6c4c21f5f31b1363cfd6bd713bd00ae6d73a6797500d8db867aa23ecfdfebb54d37bc6f102e4bbf0190cf3bbf3600670c21122d5f2fe97dbcf9de9e8deb731c0b8e19bbeb6676b703088af2fbe90081570737e5e1a5af93df5639422798afad7bff0043f98e586ffb24dbd389d200fe004203fa7cfb7e6057e98153b68dccdfc383e7ab78c207f2b52bdeb635ef005a1a0eb86ff5e4c473cf3d0f30488447dfe986f8be6351e8e3c6f9e5e021180058d75cb2328eade7a93a1c0e30a9dbb37852942901b84cc0d353d8785016eb00cb59826a6298253ca5d62150e5ff000361c0da7e52f6ad807f667b44dadc250025264a3169d5b4c109294a293164a315a592c4924d3d3d64ba75aac16b94a5000090a0071da847773e6274de2469657c43c8167d79651701c1148046672ce1009916c34ea24997a1256db8e08752a1a2e7f1fef9b7584b8853edbeb8591c6c003fc443947821de8886749445842bb8ff000a983a6ab6bb1fc7264a4cc460f8002229652d494abb8abc5ea4097310ff001a66aa9282ba505a6c254c9a857eb1001958721e62d643ee218722a39f56491d098c95149b9f5bd36b57ce95e9cbc80063de216096f07cf561a701c79718dc4295e5bef4e56ad87e42e2845684627700ada72d1fc4e97cc58db5cd0009a53a5fd69ce9eb7db90b02637807f1ebcfcf002f425ec10f01edf80f33e5f957cdbc39bb3b35f8e9cbde70814dcd8f4b0a7700ad2c29cb7b6fb5061bef724390f9fa139bf87476843c34f41cc1bd6d614dee002c4d8d46d7c378656f020f3bea1b8819732d088a56b4a6e3e77b722052f4be00d4a93892a6e99db51c79876e99e57844f86ddf6f2dafe95015516b9f38debf00b1ab78bb1238db990f7efdfd1e3c24d6f4b91d2fd3a6e05857125596b67b5e00df6b9cda113e1b9fa5add3fdedefcbb8bd2a446f7c9f3be4fa641aefcb2722001029deddf6def4a5796f400015df6a50142d7cf3bb73caf6d4dedd6d08f3430042444644310908c3b111516fb50b0b0eca14ebcfc43ee25b65969b482a71d7005c525b6d08054b5a82520922b4ae6a25a55316a4a5084a96b528b04a1097520089b001203a89218024f181203be81cf48fa77f01fc3b4270a7c22683e85b5000c61a6b93f21cb1fce014b43ab7b3fe652ee68cfaf7bd6c51c64e6d9c4dda8200bafdd4bda84870b5a594a8fc83ed276a666da6dced2ed1a97bf26bb129c9a100b10138652351e1a9655c2bf2522419961bd34ad6c0a888ebaae9e6a6ae7ce2005c296426cdf027e1406e4901f897317b52487a25c8950babfcb6c91fee8a150091e6aa27ff00548c714a54594b3afc23ea7e43c0f28bd452d82a61d7e14f4100727cd87818f7d8cb8cf8b52e38747673aefc29eb3e9de550e9cfabcb2c675d002e2cb81a5275734b2752bd50d2653aba12210ea364fcb288f4a68b7605512d00216852c2d3da5d8a6d7d26c376a5b1db438994ff00034e24bc1b6942d3be0e00ca6d3d1d4ecd6d484a5c0ef7fb3d8b6246412e94cf12d4a0a0929395453532006aa4ad7fe1ef19737ff63352654de4fddad4cf67678e9da41a9920d67d2bd300ad5acafe34c8352325e5cce72c8779c69c8b806330caa1664a954c3dca948600a6b287621c964da14f85d83994245423c86de65c6d3c9b6b3672bb63f69f6800365712635db3d8c62183d4cc42549953d74155329c5548df00ae9aa912d353004b36e99b4f3654d415216951c29f2952274d92bfd52a62a5ab404a490e1f4200ce0ea08222a3638fc5a86108b19f69246261f833d5882f0a96fe659a69364700814207896b99e7ed68d3cc972c084ee5498f9f43aed7484157fbb8eebfc3c4009333b5fd969ae128c3a9f6a31a9ea2580a6c0f6431fc62a493c3b8a1980f17006d633f0c1ff0c96744227cc57fb65d3cd5abd12436b165fed639fc1c170c59006b28bae1131d48e21b41241286529f129f772967f976afcdc1a5d086b2ce9900cf1f53942125b424d0ac63bcff000a587cea9ed4aa7149690646cf6c46da6200156a258225e25824fd97a623fcca5621b4146809cc85123f4c6be5a84ba7c400e71fd32b09c4412ecc6a69d7452fce6d4cb1e3c6311b8f6d475c4531e1c13e00f74bda9a8490de64cf1aab9a21964102220330ea766e9acae2dba81e2622e50091307130eb4ff238c3adb88250b493bbdb52138f2e43b9a2c2f00a058b3a2600d1e058753cf96589654b9f2e64b5a734ad2a4900861c871e64e20256b4f438005d3287f96648c369654c49e051312a49198208370d191ff674e499867ff68e0070e6b80759107a3992f5d75bb31256495082fe0a634424f0e8f0a55e1888c900c6b508a874b8509761a4f335214a543290af307e2971da6c07f0ebb73227850019fb618e6c5ec7e1fbadfe2a31756d955cc5391fddcaa4d9032a6148514cca00ba749004c71b6d9846e52e2d504165a68e890742a9939554b6ff006a6906f7000df483fa846ddf8f8e71b98610861086108610861086108610861086108fc5001f0df130cb401fe627f9dbff00ae9adadff3924a7a5483cb16a723bc411a8b00a7a8fbe5166a25f7b2d49fe61f127a8d3c438f18d5d7f693385e567be1fb4d00f8a1cbd2b43d3ed0fcc8328e788c694db6f9d37d428a868497464505d1c8b600e439f1b934141b0c92e43a739cd22d4da986de719f51fe13b6c4619b4d8bec007554e29a7da1a4fcf61d2d40948c5b0b42d73a5a3f965aaa70d351326294c100668242010add0abfb3f51b93e65328b09a9df402ff00e22331c0128727fda0004695341b1047cbbd28297ad3adce3e80ef702f63e5af4cefaf28e5f114bdaf00e761d4f2a1b50dcd296bdb07b5ed97ae56cf3b75f444f876bd39ed5a0e7bf2000791dafe66378697d3c797170eda1cb3c90f09b1fe5b1e86f5b6d6dba5857700e786f8cafaf4617f2d78ead93a1e120d2d5ec2bcbbd07f7f2c4ef59f8daf610098e1f51a13ce10f0f5af90007cabbdbb56c6b7de92b0fa79fcec751c5aee2d000878694af9ed5b77ff0049ebd7e989dee1eac1fa7120dbead0f7efdf0e300900ea2a079ef5bd29d0023bd01e6302a6f1cb95acfd74cadd210a5370295de86e0036277b8046db5491db1214f970f1cac3c9f5cb91843c34ded6ee6bdcd8d3ae00d6b0def88de0ec3f6f03af4e47c50f0d773dee2f5de94ad6bb9a7973d9bc3d0039b66d9b64ed70fd2d081493b0b52dda845b9df95c835af2186f8d7a6b935800e9e8fa6708c6a71b9201079cf28663430db4dcef2f454adc71b4c3a15131720008f2fbaebe96d297dd7d10b3d81871131057e386661a1d921108509f077e2a00f064d2ed76018dcb949968c67045d2cd989091dfd5e1156a0b98b6014a989a004afa19254a27fbb952909b20c69f11434d42c06df4313c4a4fcd8a474022b6007059247a074ce71387d871933dcd918a8471684253172e96cbe020d110d3800078d6da665fbd614a567c287619cf025256b52fb3bf0b3862a9b62f1ac5264b00285e29b413254a594a477d4b87d152a10b4a87c4a426aaa2b6500ab2572e6600e8b927230e4b4a5a9bf52c8ea1207d491e717857e76fea76b6c46c0dfae3d300c08b1d395f4f57e1e11b088f98bd6e46c697df6a548dc589d8d307d1edc87e00c0df2275e7089edbd39547316dc0e5cea7722fb83f85beaf7e3fd384207a770016d8db737dfec8b8ae0feff6c80e5cce85a10bd373b9bd7bd29f8f2b12280d002b86bfb7f5f5cf5844fd7f5b5e87e76dafcce21ddfd9f65efc7384453d003e00429ea4edd6d6a81638973e7cbdff005be70874a5ee2b5de82b7a01602963b7003ef83e57c9c7dfeaf0876dcd7cf95452a6db5b90ef4a95bd3fafcdaffb4220009df7db6af3e6282f5efd0ee050e23cbdbfbf2e7089bf4b5a9e5ca8473dab5300434de95c49e3f303339fedecc2326bec7ee1f18e237da05a0d95a6b2f899860053c933c8ad5cce1ee1bf1b0ccaf4da19798652d4cbc495204b66f9c18cb320008c4aa9ef9a9b1650429d4d3a8fb72da856caf665b4959226a25d6e214c8c12008378b2953b1550a59c65331ef64d02aaea6591fa552428b805f5d8acff00cb00d0cf582ca5a7ba4df599f092398495171c1f48fa3b25254a4a522aa510903a00926807a938f9557278927cc98e020124017248007339477987652c32db49d90008009eaaff007947ba95527cf1b6424212948d0378ea7c4b98de4b409684a0005985f99cc9b7131e6c5515c308462f72a4a1be18f5ff0035e81c6a112fd29d00719fe72d69e1ba397e0665b2ecd53d8b7f35ebce86b710f3ea5b93a82cd71b003dd6ec9d02a09723b26e73cdd289143264ba511c217d435d5c7b4ad82c2f6e002528cfda9d89a1c2363bb43901d7515185d14a4617b0fb6aa968404a68e761007268b633179e1d32317c1f0aaaad986b36a2477b95523f33211549bcd90944008aa1992948dd9150cd649404c859c82d08528ef4d8baac75b46b6184231e9c005ccdc6a6eb5f0dbc31cacb31d090b9b20b8a0d6984f153f7769c68f46bf11a00570f14540a03d9ab88346498f9641905c994b34ef382851897450577f764d4007fd9dd90ed0bb48a90b9331785ceecdf63e737fe51b43b592508da65ca00850014619b08ac62454cdfd34f518fe12082ba894d9d23fb8a4abaa362b41a2a7300c66cf03bf2392297bc04e8a9a819a8118a9f694ea7426a2f119a6ba392788600a324fc3e48265a8f9dde86790e370daa3a912a7b2de46cbd14026ad4c245a600cfe6fcc331830b2b4c167eca718ea5a43d0feffd77f861d969d81ec3ed26d900d5cb5c9a9db6ac91b3b82a66214933767702a84d7e335d29cb2e9eb31e46190043266b01df609894a41514ccdcd162f3ff0029832a5b8137159c896949fd5f0092a3589b3660e026558912d0a399913d21ca4b63af57f33c6e57c87365c954004e6acc2a85c9d929842921d7f3866b7932590ad293552989745c509c4c94da001c5c3ca25b308cf76a443af1e9cd9ca1955d8b538a903f87d189989626a2fb00a9c3b0f49a9ab04e4173a5cbfcb480480ba89f265b82b11c6b07a54d5e2121003340fcb48deabac2412052528efa782d60a9884f732dc80a9b325a1c1508aa0059432dc1e4cca596328c02d4b80cad97a4d97a0dd70250b72164b2e879732e00b81202038e370c971ca003c6a51c71fc46b66e27885762338013abeb2a6b2600252e40995539739494bb9dd4a96425f4022dd5d42eaeaaa6a963e3a99f3a7a0080bb2a74c54c2072054c3946753d881a2b1abcb3acdc5fe60971876f5a269200bd32d1979f4a52f44e8be93cc27888dcd70e8a7bd661b50b53e739b62a0de500acb539ca594f23cea1109858b65e89f9d5f8e8dbc93538f6c9764f8754898900d89a5a9c7f6b132c92846d86d3c8a354ac366281285ccc07672930b973529000154989e298cd1cd3dec95a25f3ea6a5fe1f87525128113d5bd5b581df767d400a6589724d831934c894169b944e5ce492e18679f1e078aa2cb78d7e3974c380021c95956739af286abeb16a6ea7cf23f2a688f0edc3fe4598ea66ba6b566990044a5fcc13d97646c9b2d532152dcab9761a273166fccd398e95e5fcbb2865b001151ee4da672394cda92588001529592433b021cdd800970e7a00e481117700000726ece0581009be81c3f511695ecbdf6d6f0b3ed47986a769fe4191ea168008710ba35318e85d43e1d75ba5b2e906a4cba5b2e8d6a531d98e530f011f1d000d37944a67cb732de63855980cc79567cc261332482590d339146cde0294e1000b4142ca77acea4100805a66e8048247c24254d7096bc4390add5a4a5453bd600752480402cb601c121c100ddc06bc661f15c550c210c211f8261348095221100c8f7d2c223663012a86a824bd1d32896e12118404824a9c75c153608405ad40042524e03300ea40cd9f8f0d1cf1e11233038966e3a9f404f1611a5ff00ed7700659d42e1998e087da79c366a3670d19d7dd37d585f0ff30cdf9267b112788900f4967325cc1aad91e0e77008884c04fa592898643ce5073492cd65f3493e6400936637a4b9960661288787844dd2841fe50f6bb07701817cec2d9e4e32262e009420df752e58bb0770181767702c395b28d88bd90fc7cc0fb4ab801d08e2a500c8695cab3ce629447e50d60cb9298b868887cbbabb90e3ddcb99d1944230e300aec9a03314442c2e79cb7268f263e0b28e6ccbdefdc8a0e371913641b9048200527754dc581f07490a0381116038241cd258f9023cc107c6325b898986108b0067e27b44a45aff00a23abfa1f989985724faa990332e554bd16cfc4352c98400de5b10d49e7486ca54445e5e9e0819dc038805c62320219e6ffcc42719bb39008e54ec9ed460bb43485626e138952620132d5b866ca953926a2989b0dca99100ded3cc492ca95354936546b4a9549592e726c133133436a97f8d3d0fc4923f00ca79c7cb3f376569e647cd79a32566681765999327e619d6569fcb5ea07a02007797e6313299ac13b4a80e42c7c24443aee405b66871f60686b69f11a2a4c40028e6267525752d3d6534d4dd3329ea652274998392e5cc4a87231d80952569004ad25d2b4a5492354a8020f88223aef7d8f315dedb74e54ad8d06f6c653e9e00f8b7d5bc62a81bdc1d8f90f9fd8e5bdc3d5f8c21e5cf9ee057902076e7ebc800625f2fdfd7cce5ce10a1ee761da97af2a799e7b0a5460fc2dd337eb9b68cfe006734284f3237a8a8e95a585adfd2a4d4c421d6f4bf2a7dd76adba01d713f4f006df3843d48af2a0db7bef5bdbe5d893b5fa97bbf3bbfb73085773cf7ec69d0009af2b13415b6d518037e591d59fdb8108549277a0e83b57bd6d714f51700c300fbfa4222fdc8a5457f50772294a56e6a2971897e795adceec459ff007d43420026a7bf21feeefceb41bd2fcf602db622dc78fa7bf38459e71a925763f4ce4900388761c7cc8f364298b710da54984974ca5f30855c43ae1ff31a6953112b8400a24f81c7a2590b05496fc3e62fc53e18aa9d8bc131444a2b5617b4089335610020f734b88d0d52662d4ab144b5555351ca61654c992dee046bf104bca42802007756013c0281b93d424753154f86e94c4c9f44b20c2c5c3b90cf44cba366be00edc5a56550b3a9bcc66f031092952c25b8b808d8589422a1484bd45a50b0a00039ef617453283b2ad9395365994b9d4f5d5dbaa20928aec5ab6ae9e63876130069a6ca989bfe95a524050298bf460a69e5836704f829448d4e60bfd04570ee002bb007637dc0e7ea6bcfd476def1667e2c4ea39dafaf33e8726077fedb6c7f00237041361b541f52e6fa38197833f263724bd8421d6b7a5a95ebd76a6f414d00ae0570de2efc589667b7c99b537cd9898437f2ede9416a14f226dd2f89dec900d9c1bdf4d58581e02e7a4214bf30492473a72ee2e3b5b6adc61be7c867a13c00c5b9ea7270181844d3af33cefbf2dedf5076e7411bc78bdb98e4e2fe390fac002141e7d7ccee4d3cefd06c30de27d48d2dfb371cdb5108e213fa1e5cc1a8a500fd2b4aed4c37b5f2cafc7799af6b72f3844d3f1adad5a798a1b83bf2bd70de00cac07d33c9b2704027ad9c42269b6fd37dadcf97a9adf13be7967e7a0cee7c001b2be764472ed6370491704576a91435bedb8270ded38bdddbc8f83025df9500c4236dbfd98ad102987e25b8908e62bef1ecb9a25966269b7ba4c3e7aceed100245483efb202904103f95ca8b8a789ff00173b44ead93d9596b36155b41568003c15bd8761ea0321fa7137ea1ad9f17da29fff0093d383fe69cb1ff511ff00007fce36e394b1ef62d2b34f0b20b86a2b557fa503cc28f881ff00a38f1953a3007a639c921fc721e46fe11a1a446fcd04b3206f5f8e49f237f08ed98d8c6da100842184228feba68964fe2034ee65a7d9bdc9acb02e325d3ecad9bb2d46feea00ce5a7d9de4112998e55cf992a721b74caf32e5b99b6dc542a9d662a59348450046c8330cba7196e6f3893c7f2ed87db4c5f6076829b683084d2d414caa8a1c004f09c464fe6b07c7f05af9669f13c0f19a3de40a9c3b11a652a54d085caa9a0069a24d750545262349495722ec99ca91302d202ac52b4283a264b5065cb5a70054a858e441652485252463315c5c45f0cb9da51a17c7bc5c934b332cda3132009d2ae25970eb90f0e9c43c321a79d62218cc914e3f29d1dd526a1594273769006e7b9a4be1c4dd6b8ad3f9ee6bcbb1b00f33e9a97d9749ed2b05aadb6ec3650056ed361d4b28d56d476722626bbb40d80985684ae5ccc3a525155b5bb32b9800b270ada6c129a7acd2012b1ea2c2f10933d0ac85e1ff009842aa30f0a9c84800de9b4afbd534c5c38dd60a9d29cfc1365825ad3129503179ede79ca113256b0030cb731c9271268987115093293cd2026701170ea478d110c46c1c43d08b87005268a1121ef71420fbca115ea5fe058ba2b15413f0eada4ab9730ca9b4f57400b3e9a74a980ee944c9339089a2603632f73bc7046ed8b6ad40a1452a4a92a10062920853f0dd21c65a88c2a7115c6a683e85e73d5f9870f8653ae9c556b3cc00244ecf60e55995ecd593f4fdbcb72087ca79715a939ca05f8d95e47c9993610020a327707a5b2a8d63356609fcf2751d2a91c12f364d332427b4fb37ec536e00f6ef09d92a4db6455ec67663b2126b5349515586a70bc4f1b388d7ccc5310300806153a5c9a9c6717c5a64e954737692a654cc3686868a8e454d64e186536100f36f4f5f734f2a762933f29454e95f7128a426a2a8a9456a4d3482cb9b326900210aa9527b99694a77d6d2c22312321954c2044de6798277179af39e6e9f4d00737e7bce1316db6e659b338cfe20c5ce27314db5544331e32dcbe4d2b654a800490c8206552197844be590ada3dad3554a895454386d1cac3307c26869b0ac001b0b90546461d85d14b12a9a990a57c532610153aaea57fded656cda8ac9e50053ea2628f01c4f119b89d5aaa16912d0129934d4e924a29e9e5b897250f9b300954c5303366aa64d50de5aa3b6a25f0612cc647b30cb5c138a8d867629b69700fbbdc4b0f32a8b65c741f867842bf10d2df4290b4c3bcf345610e3a9560aa7004c754b92b584cd025ad32d4a1df24ad0b12d413fad3de210a082e0ad2953120094918b2b7d2e12560cc4ee2929246fa4a92adc5007e205494ab74b8de092ce00045c0f097c2567af680671121cb066997385e934c0c36b36bac207e121734400242bc04cf48b4566746d198f354fd285ca336e7793baf4874e24cfcc54dccd00ecf3fba64e8ea1edafb6cc07b05c15736a154b89f69f5d4fbfb27b1cb289cb00c2664e41fcbed4ed7d3ba8d061b424a6ab0bc1ea928adc7ead14e0d3a706fc00dd58e6783e0a68ccbc471395bb35244ca2a098196a5e68a9ab967e29726596005ca92b01750b092a4890e666e2194f2ae5cc8b95b2de49c9f2680cbb94b27c00824f95b2be5f95308859648f2f4825f0f2a92c9e5d0c8a221e065b2d84868300846103c2d30cb68164e3e31e2b8a6218de2788e358bd64fc4315c5ebaaf13c004ebeaa619b535b8857d44caaacaba898af8a64fa9a89b3274d59ba96b528e7001b85ad5316a5ad454b5a94b5a8dca94a24a944ea49249e71eff18114c602780038d4e57169edcef699e7a8d8b8599e52f67c68be80f06fa42cb70cb760a027007acb179935575ee72c3d1097d963368ceba750192e7d172c7a0dd732ec8b2e00cb23619cf872ebb32412662cef31594a4286eb251f0960402c57be428bef060020946eb55243efacbdd452970cc947c2400c0b1585173fa9c31290831abd7b0061b34bdeca9fda7ed03e2f74fdd81c9d963591ad10d65d478680964ba0a51100f9433ecca7ba05c40b119050af7b98b8ecd724ca79a734cca71190d0b1aacd0093d5e61ff3e690c89a3f54e1fdd9533997f186cfe1b903aa5c7378aa68f81400a67281be2ce7e1b900712010dabe99c7d1ec1040208208a822e083b107983800a22dc30843084625fda23c464af4638b1f63fe459ce6032496eb1f1bb9ce4700170c998c4c1a672fff00b2beb9645cb32f8e84856dc54d25abcfbaa39392980068a065d0f3e732fcc22434f424246c190a5098948701485957c24a484aa5b0002a664ab7940a4382a09530201626f31018d82d40b16046ea7f5330242cb0700048de60402d873fdb48cdb2e947b31f44f29ba65cecdf377193915e82878a6d004b8e6a5b97349b59a2a6b3295a825419721a2a3a4d2f8b754a40309377190400a9e0939119116a7fb109ab5309ae86f1e3a16fb8eaa5991355747b55a5ad14002830dc6eaae52cdb94a72e25cf1f814f3cce8f48d2b4a5b0b4b6cb65c5a9250094a31d569aa0d628429dc5d44ad2433b8b246801bb125db1d76986d629492500f32ea0cd98b017b03a5de37a0c210c211ea270c7bc860e81fccc2aa6d5250b00212a029d0f8547900938c6a943a37b549f4363eac630eb25ef4b0b19a0dffd00a6c7c8b1e8f1f3c7f6f1f0fc3443da0d9fb304b65b092fcadaf121916b249100b81494b1fbce6edbf97f3cfc42681299947677cbd3dcc11884d6a89f42c41a00188a0fa49f872da83b43d9961b4d3674c9b57b39515380d419841577320a6a0070fdd373dd4bc3eaa9a9a592d7a75a4174bc726c167f7d412d2492a904c95300f04b290dc8214948e87a461900d850fd0fa1a9df91a72e9cbbe378dee3c43e00ae0e5a73cb8691b686e2bcee072fa817f216af5df0de209f47b79dfa5cddb3000c5a103d7eb5b5395afd48b5fcab605657f901e19016e36d59f3400dc79f9000b6dbd0d294b790c4956561986e3d6f93e9676736b421deb5a5e806f514f5bd00f7fcb11bc796673d320e7cf50da0b58227f2e67a7d3a7e15c028dae7867eb7003a58e80dc59a111e87bfcb7da86b4a5baf22712159e5c9ad7b58f47e5966610011cb7ea0ed6a5ed700114de9bd4d00388debbfcf4e6e38e46d7000844f903f00402de7cf953b0ad2c70dee7e77d43e418bddf95c5ddd0a1a75dc0db91a837b00d8ed5a03e57c4ef5f40ecdebe008d6c7239b01087337dfb0ada82b53f8115f005ae1bfae6df5d3c06b96605888450fe246531337d12cfd0b090ee44bcc4ba000a6c50da929298692ce25f378f7d552905b8481828a8a7124a94a4347c2952800a41ea3edd68e6621d95ed64b952d53a648a7a1ae0848ba25d0e2b43533e63d0086ecaa6973a6af5dc42807258e35607a698c1d824f928127c039f0d728a85a00770820b4ff002241216a711039372b4136e2a854a6a1245030ed9510026a5000d24a88091b9000a01cb761e4a68f62f642912494c8d98c064ba9812656174a0082a5330724126d724b45c941a54b4be52d1cf248cb8fd2d6d23b8536dcd2bb0013e57a7a5faed42698e53bd9367990fa723973e9171f8f1b5be99fa7a5e14a0057fbfd8af3e42bcb00a0dcf81b7ab374cf9eb1274ebefdf8429de9c873f5260087a123d37e6de19fbd39bd9ef9e479438ebcbdfd4f943ef96dd36c3781d40e000febc2da68f7b5a1efdb7be113fd7fbf2dc9b74e7de0abc2f773cfa17e6dc600c73221f439fa781f66cecd114e5604f3b73e75e7eb6f9e24ab27f639684720005f966225fdfbcb9be5023ef98a798f5e789de60fa91fb172da3b73cb3887fa005ecd7e1f2eb11e1f4b9a936bf3db9f2e54f2a831bfccf06be439e4c082f76b003bc017f277f973bc4d3af2e5debf88fbe58050d5c75f6edcd9a0fc32cceb6e005fb3e96bc29cff00aefb8fa8b0dad51414c0aadadd8e8de2720743adf8983f00a1f64727f0cf844d37d85c5abf8f61606be470de0dd79873cf3e3e5c20f7b300b6a5ade1c4f4b10f1f455f62ce8dab463d9c7c3e40c64ad996cf750e5539d500e9ea9b4781e982b512731938cb31f16480a5c4ab21ff000942ff0035db62110096366863e5e76f98e8c7bb53da6992e719d4d864e9182533974ca18653cb9100572d1c13fc47f3ab6162a5a959931c0f189ddf57cf20ba6591293c07761940007fd3de2fabc65ee4cd782194edaaf2c9af3f0b64a003e4af19f5c755d325a500956aa3e82c3d5e26891bb2caf559f44b81eaf1edf1931990c210c210c2114e00356b4834bf5e320662d2cd64c879635274f735c139013eca79b6550d36954600b4b4a92dbe86e2105c829941ad422659378076166b298d6d98f96464246b0c00c437c876576b769b61f1ec3f69f6431cc4b6771fc2e7267d0e2b85554ca4aa0092a041520ae59099d4f3923bba9a49e9994b55254b9153266c95ae5aaf495c00e93313364a972e620ba5693baa1f707220b821c104388d75b5a3f6747264b600366d30e1433de449665d98c4bd10d6907107925fcd19765ad3ca2a12e946a400e4d765b9a5e96b214b6c2f5172c6a9cfe21b0cfc46625adb75511f44b61fff0000b453185c9a5a7ed5b66b12c46be9e54b94adadd86c52561789d429018d45005ecfe2a89f858a95b051fe0588ecdd1215bc25d0252a4897933a7cdaa00cca00dc528a6310665056ad295598134f3bbc44b219c2696652cb0e59193536947b0016b8e892c0b125cbf19c136509632a486cca33d6b04540c3a0f8438e31966000b4072bc3f8949154b299db20512953ea03c58e6f55f8e5ec46a662aaaab0de00d8b12a9502542ab07d929531658eea555f336df1098cecea346a2ce42348d0004cd9da29f3953aaf16c52a8ab354ca593df2b36de9d32ba793d4cb3ad845c200652f61aeaf4c1b65cd4de3264320140a7a0745f4121e1662dad413e26d19a7005475033e4bdf6d14506dc4e9fc0baa2a0b51481eecf5ce35f8f5c02515a366003b20a9aab908a8daedb6993a4a921d94ac3367303c1a7cb529c1527f8ece4800018393bc2fc9c0f03905ff002b57567ffcdd604a39908a4914cb0fce72a2eb00b4c3d891c236558d6669abf3fd65e28a321a361a3a1657ad19ce5b0190a1dc0086a110b15a67a499774cb2466a963cb4f8e265da8527ce90cfd54cb8930ab500307a7b6aff001bddb1e3921749b312364fb35a799266c89b3f63708a8998dc00c4cd70664bda2da8c4368b19c32a500b4ba8c06af07988202c1ef0058d949400d2d217a1a1a4a357fcecb96a9b3c734d4552ea274a50d0c95cb2f7cef197190014864595a4d2bcb99624b29cb997a49050f2c92c86452e839449a512d846d200cc2cbe572b97b30f0301050cd252d43c24230d30cb694a1b6d290063c8d5d500d5d89d654e238956556218856ce9953595d5d513aaeb2aea26a8ae6d454d550042e64f9f3a6ac954c9b356b98b512a52892f14a86f28a944a94a2495294a2a0024e64924924ea4c7b6c62c46e2787a9fbc3086ea787a9fbc6975fb357add0d0030f6acfb7ef4a67d344b99b750b8999eeb449a561e210995e4ad7ed7bcaf9c006361587bdec42d94456a86438259314e3708db706c86d1efd05534ec64a58b008de99720073de2f78b24245d4e6c07373788960252000c1d46edaa8926c00b0092f96b7bc6bebfb5fbadf2ad50f6baaf24c962d1147877e1ef49b4b26ea43600df819cc5368dcd3ac118c25f6dc5fc4fc3406a6ca587c2d2db90d1ad45c2290027dcd717541d2a1c5245ac6e38e915100820e44105b363c1af1f4d0e13f30400d736f0b5c3566a9ebe98a9e666d00d1bcc1398a434961113359ce9d65c994c005f4308fe4652f4644bce25a47f2b614109b018c697fe1cbb93f026e4824fc2002e4809049d48003e400b45a4cb1ba965123753755d46d99b26e75b0be8328a00ff008ae2770f110c220a14347e9ede34a4fdb34cc7a83a5ba71ecd5d7ad3990093f97f31e8ef1319e330e5acd108a64c5e5ecf90720c999bb24cc61d8782c3008ec346e488d8e6d6b696ca1d816db787f9e84aa50409894dee85906cd628b1002eee5dc000820289660e4b8580410e95696705363abb390066028e91a7e7b500d7db55c467b5fe79a2115ac595326e9ae55d0fca51d2e92646d3f7e74ec86600b9e7330969cf1a831ca9dc64645a6367824f298094ca8bafc3e5c9340220d8008a8c8b8d9acc63efc5e8d99ff61c64f3148f6934fd50ee2652f1e14e4ecc5f008daf74ecc61bfda1236261c361c2f8718868a8470ad4d25a2989096dc5ad2e0025bc758fef9dcda580cc58ba95776dd70d9024872e0021f1d63fbd773fe1a4003316ba9577667b64efc40043efef842184238b884b885b6aff004ad2a42bc9004083f438823781072208f30d10a0149524e4a041e84318d577f698f42c660d0002d13d7c974a971135d2dd478fc8398e610cd92a85ca7a852c762a162262e2004544141e69cad2c97c2296406a37327bb45151671e9dfc27ed09a3da6da1d90099f3822562b862310a694a36556e1738226a250ff3ae8eae74d581728a40a3006445cd9f9a5151514ca536f23780ff005cb531097d5944e5924da34bb1f75e00ff007e7d2d8f79150cfe99f06e66c7a5cb3c72cf5bfbe1978e5c614a6d7dcd00b956a6e06f4e63d6b5186f0e2ff4e57cba9b3922d6107e473d7c7dfd2ef003006eb534f53f76f2de95c4be9627ef97bcf80bc1db3cbedf3e80687a4295f9d7007a53b77af4f3df11bc3cb3cbcddf2f583dc73f672078dded973853f3febfdb00cbb61bd7d35eada65c4e5c4335cc4bfbebc467efac29bd0f7d80e96a01b0db00cbbd30de17e4fd6deecf9c3dfbf7f78529d77205052b7f2bf4ebb576181531006bf1f0bbe6346279db304b438e239df27ca143bd283fa8af6dedf9df0def9e007979b9e46fe8f683f9f07f7a7cb85e1d29d79d3aefebbde94adf6b8a86bae400dc321776d3c32e707fbe472fbf2d7d0294af2b9b6c790bedd6bcfcad50dee300e0e6f62ccdd5f906bc1ffa8f13cf4cfadb310a6f6faec6fb5f737b8eb4b9a100c3785f96ba11eedc09ea041ed91767035fdb31d358e9ba8b0a98ed3ecf704a00716d223725e69835b89a789288b914743b8a15045425c552a36df738e2db730021359b15b5f48a702a76631e93bcc0949998555a12abd9c2885273048498b700383c99a08cd0b16e692d9f0717d3c23b7c240b12e84869743252dc340c3b300070eda004a10c42b6865a4002d44b684a5236a0a5b7c6e30ea745261f43492d002128a6a3a5a74240b253224225240e00048f26cad1290404802cc06af6166c009879b139b0b79e80d850114dc73b0bd46e7cafbdcffa7309273f7d3844807c002eec6f99e0ff003e59b8572a58817dfbede66e79d05ab5163887fb79ff0058008bbbf117d383f0200cb8eb716811bf7ebb569622b5ea282be55a0c4bf1d79e0079f9f8ded6685c35b2b5ae59df466b31eadc4c4048b0f4aedd8f2e9dea7f98008e580d7cdd9fdbbf9b6512497d2cd6371c5f8f12e34038167847a577e75af60002943d6a3c860ffd3c7de4c79c47c407cdf22c39e761766e6e5da3c26d51c800834ec491b6db0df7af5be0f6d33cb9b70f3cbc74892e0901df30e1c0d0df9300f874cd4dab7df604d00af98f41423a13420fd32f7970cef6b40be771935c100009007cfc0f100982801414dfcf6edb8277ef4b0070737bfd38587d78b3dda0001c5c93912c48cf9e4c3261a16ca24276dadea2c7cc1ede9435a0c1efe26edc0073e57e1e5104ab3bded667cba1b67911c80bc08de82f5bf3e76edff48d4f40006d4c1fecfe8edd2d965767807162fad85896009e1adad72e6e6e4fbfca996600679cb3465bca32463e26739a67d26cb72760923dfcce7930869640346d5ff30022a29a46c6c6805b18d5b57268692aab6a15bb228e9a7d54f530f86553ca5c00e9845f2084a8e9d2214a294a8a8b00952d4782521cb33b586be6e6df554d3a00c952dd38c819134ea49e254a321e4fcb192a50549085aa5b95e4b0323802a40027f952a54340b44a4581240b63e3b6295f3b16c5311c4e781dfe255f575d38000cbbeaca89951300e3f1cc37d63ade628cd98b5eb316a53735a89f998ad6c30061965a687fe6d094f9900027d4d4fae2fa53ba94a78003cb33e39c6e909084002503f952078ea74ccdf28f2e2a8ac027286115841d587a9f7e30c22b0848e700d618454c0641a184218421842184218421842184218423e3a3c74ea9714bec00a4f6e0f1aba85a039fa69a47ac5953892d6cccf95730424249b3043c669d6b00bcc2659f241033992664819de5dcc72b9d641cf525887e06752c8d661a62980058f658829ccae0df82ae4b84312094a94090c1fe224120000120825866498a005164b16704bb30d4e8001e9189c9b4f75638a2d76899f6689bcef5375af5ef0053d111369c4cde1133dceba89a8b995085bcfb890d37f19399ecd528434ca10088660bc8661da6186db6d154c2c8597664a8b9d2c624e46ed637e1cf5ca3ee00a7a5390e034b34bb4db4c656419669ce41c9d90e5c43afbe0c0650cbb2ecbf000843f14a5c4bc0c3cbdba3b12a53ee7fade5170a89b09042520972120136b9000189b048bf24a470005a003002f6005d9ec356003f40070023bf62a898610800d7fbf698f8249c71b1eca6d6083c95288a9dea770e53794713790a5902e32d00c54d1bd3a97cee5fa872b4a5d52171def74a73267899404a21d662a699825300228783622a3442c2bd4aaca42ffc8b04e7fa540a146dc028ab23fa5833b8a5005a1e041c9ec6c7c9ddf803c4c7c8431951547d5ebf64ef827cc5c28fb3065700a9b9f65b3093e7ce30b3cc6eba993cd201d96c749f4cdb95c0e54d2961e65e0001d886b3148a551da8f2c8e21088892e7d963686a8c179fc5fd4b5aecca64a006dfc887673aba94b50d374860ee4da28de5296f9b01d12faf52a3ae995e367006c5514949198f1f7f5861110c2118dcf6ad68d7f8dfc06f14d911a0b31cad200c99e7b9325b405baecef4c2221351a5b06d8a13e299466536e5ca291e30dc7002fc355531cdfb2ac6ffb39da76c86246d2d78b48a09e49200918ba1784ce590023fe6915666dece804da31a42ff2f89d3ccb6ecc5a525f2699fdd2b864ef7f00de3e67fe1f4af3e62b5dec2bd29524d6e4db1f5479feff0038e6d70dae590200c72cb41c5ec03586622483526bb8e42f41caa7627952dcf7c1fdfdf8c0060d009f5259f5161a017e6e3531053f23f3a8e7b8af33eb5a1c4bfb7e4c0f87b61000f886619bc9b86ad76f01a31313e1a8da972295e57b8b5076d89b1aef83f4d200fe563c85ada1881bda6ada5c91c4824b16cc96bdad971f0edb8b91e5dc58720016d86d6dea7f4b8cf973b3f2bc4deec341f5e2fa9b92e0deef90269cafcb63004befb72ad6dd397312e7df4d6fa6b0727891ad9893e7605883cc917d013d8800a9b7614df634a9eb716e6307e9e43df2e96893bda70f5719b72e16777b447800457b74bf4f4d8fd01aec70249cce597bfae7072c05f5721b4e57b903a027c20039f86c472b6ddf7e753e47a0b1a60e7f6f2e366e595b260229214fc4dc9e1600c80b69c83972cd9c70f0f634bf98de9b9aed4edc88ae0faf4cac6de1a36b7c008c54e78e59f0d01e81dee41c8dc089f081b035a5286845452f53d6bdac0f3c0001cb97875be711f11e84b9395ae34e0dc4e97223c11702c4c616225f1494b9000d1d0ef414436a154b90f16da98750a0770a6dc5248248a1f43878853a2ae8002ba96600a975547534f3126e1489d2572d49503982951046af10a75254345200482f7d0e7666208c85f2d5c7ec372491b9a922f7b9ebdb9dc5319008012072004eba5ada7887d013ac56dcbde56e16c9b8e90a6ff97e67cb9f3a5ad89de17e00a40c86435736bf16cc44ebafb6f7e7c61423e5f8f5f9dbb114e586f0e205f500e1e2cdf77b11784287f2e7dbf5fe9b61bc3ad9f41f3bdf4b6bc3286e9e5a7f004f7a4453e9bdfcedfd0f4a8b61bc2d7f477d34e27ed9e46f0cf2b71f7d624800b0e763b5091dcf2a5ea09af51b0c3783eba31d2f978bb8cb4be569f1f7ebef0058529db73d3f1a6e7a5f7b60540d81cfc7e4de6e388d5a19fdf87a8b716e1000a72bfcbaedde9b6fd6d88de1cb419defabf2bfcdee1de7afbb7bbf1c943b6f006aedb8f51d2be82bd0e242835f319f5bebcdac074106faf0d4fbfade229cbf005f5a53d6fdb11bcdadf9e8d7b87176b5acf6b44fbf7efe90a57e74e7f423ec0080713bc0e4fc6cda072ef969c33cf3686f3e233f7e8f768be0f66a69d3daa700c7d7091941b4071b56b8e47cd31ed783c69764fa7f336f3fcf1950b7f2bd2700cb11cd15ff00e6c2fc641f050f5ff6ab8aa708ecdf6d6b8963fd9fc428e592004069f8949386c820e5f0ceab96a19bb3067b61e20b12e8aa97a890b483cd6000cb1cf350b795e3e99b2f6c3b18c24ec17e33ff00f4c158f425201f3c7ca692009de9a81a3bf9027e91c0a9d3bd3a58393b9ffa20abe623b9e3671bd4a5ee7200f9c308b800194308986108610861086108610861086108610861086108d11300f6b73d8e7aa3ac798f26fb473859d33cc3a8d98a0f2f4ab4d789dc8b9032fc0064ff0037c4cb648b54369f6b0424865298b9c6616e5b2e884644ce9fbb20220062a4b2294e4d9c390c64b0198a672ba52a1294a52ac85b152893f0ac00904b009202549001600050073528c527e1249c8e65ec0861e0e35b00d7ce3175fb2f00fec61d5de2038c4c97c677109a5f9a323f0e5c2dcfa5b9fb28b39ff2bcfb2f002b58359a01a5c769dc16566660b934546c834fe7625ba873e9f34ccce4711100d249165289848f67304c8cba56b4cddd4a1972c9256b0414ba4864862778ef005cb061ba4120b022ca6018806e6c43a4b81ccbf0c9aec63ea01898aa184218004238ad08710b6dc425c6dc4a90b42d21485a140a5485a540a54952490a490400104820838423598cd1fb275eca6ccfc570e25cc8755a4f95e2738233dce78600394e6b92b1a07359e7bd7a36325a9973b961dcf12bc99339b3889ac6e4d96600748694b3476472812aca8e3597e1ed8428029ef57dd9046e7c36496b05b77900a100ef92028804327768dd396f1ddc9b95bf9b3d1b3c89d588d9725b2d974900a5d012793c0414aa532a828596cae572d8562065d2d9740b0dc2c14040414200b6d4341c141c334d43c2c2c3b4db10ec36db2cb686d0948b996515c7edc21000c2285201cac7d21845b2082c63abe6d9540ce253152e9942b51b2e98c345ca00e6308f242d98b809843390f150cf24d3c4d3eca96d2d3500a5c239e2ccd5ae004ae4544a52913644e44c97313652168505a1693a14a90083a18c1ad0c99730006685b38cc3dc17e45367d4f38f944eade467b4c75575374da25c53b11a7da80039cf23bef2c10b71eca998e652171c5a4eca52e00a9429504917c7d78c1712004e2d836138a246e8c4f0da0af0919245652caa9001b693000d61616ce39ccb00589b2a5cd6b4c9695b7252429bd7c6d9da29f52fb53f2bd2a7d77b6367bc1800b17f3cef96762cfcb8c56c0e9ed88f9130a74e5bfd7b6df23caf7c4ef0cb5200e3986e39b7cacfca262294eb715eb5eff2fc29cac704798e17e5d74bf8da0d00fbf31efdde248bee37b13420d287eef415be0141b5d39e7a71b71d79981f7e00fdbe50007e23b72daa47991df913511bdadb5e7913c1f866c5af7d0c37db3d000e6dc3d32e0d0a6fcea2bcadccedea29c85cf3c4ef6b7606e58b31723e8fd7008189f7efdf0f08236ebdfe4476f5ec69b62778717f4e7af2faf02c6fbf9c28003b733d2a773daa296e77f9524b59c970d6cf2cc1e25c72b1d6d06f7efe50a7007deddbf3edc81bd36de7785f96770feb6cf9e5abb08803c2ccc3287d3efe78003df5e4dc8b1b9fadb818365fb67a7101b90e1a06890282b4b83cec2bbf6da900b124d4da9883c1eca19679b0d7377e591d6f13f4f76fdbe70b836a1a58f903007af3a7e22d5a62c853b1767008bf93716887b8cae1ef637cb5b9f7d57fa75a00dabcfd7af3c4ef0cec3d38e6fc8f94520ea73d43f077b58d9eee3869f1437f00b36a7f603a7e4de70f6b9cf237b72e37d74e02241b07e21efc78bb59dc117600d32b2a79f2e4474e5e5ca9f860f66d0f07235ebcae4365ce04f83b31cf3e870042cef63e311f76e7f74e7f8e00e447dfaf2e460fcb205db3196a6dc7d1811700857f5e97edcb972fa5b07e7cb9dee4710487f3f083d89c81721f3667d0f5d400344d0ff5dc5adf2b50570de7cf9e97b876b6bab6b7cdcc01d1dcb121ede052003d3326ed0dbefa7507cab86f659f2d45ed9876e19d8e5c621f50efa6b990f700cb3b5cf880410de96fd4d37e97bede430de17f5cf8eaf7e77eba40171c436a005c872c474c8dc82d7d2d1df02a075b9cb9d9b5cdecd7e633897be7996173ca00f9f46cb3243d895fef9f2dcfcaf61f3c1dbd97191cb3f3d48d5a2064ccd6be00a5f95c9b599f97000e6b3d805a5d98b3efb46b4fb374a619a7247a3992f51f003e66c8a79cf7698697cdb284d34e654d4302087e3a2730e779516e1814acc000b1318b154413831d0df890c62970eecbf13a29cb509f8e576178751a121cae00648ad9389cd2ae12d14d4135d6cdbea972ec5623538e4d4a282620d953572d00090d7242d2b3a65ba855fa070e23e81f2468979c788fe54365009ff9ea2926009e49041ffac3adbe7552a7e252b4018753fb0f58e3187cbde98b591f0a53bb00ff0049441cb5b03e91d9719d1b886108610861086108610861086108610861000861086108610861086108610861086108610861086108610861086110a0e3009e91eba6b4f817bb16a9e7ef503f027166a3fc2572ddff00b4235f57fe02fa00a7fed08f9a17b59f4f6134cbda37c5ae5c8152550d31d538bcfa3c1fe943fa00a72894ea646b2050784311b9b62580903c290d809fe5a63e9cf6358a2f15ec00c3632a667eb9783cbc36f994e113a76152d44bdca9144853f13772efca30b9008665052a8e7dd8961fff00c32658e04bee79868c76f7fbf3c7669568ed761d004e9c72e3c79c679398d6edc727b677f4623a44d4df9fd93dadce9b751838d700f773a6ae790bf9c1eefa3664861d1b339b8e97ca1e7d09b539fd07d36eb4c100f83ff43cdadad9ded6d60e5db9dae06a3437b0bf8d833188fbfbae1bcdaebe00bf7f51d221f8781cc663a70bd8900b82d137fa7cabfdfe7e986f740e4b79e900a3dadab5b4b4926c2d72c6fd75b312d6e6f9343e5f4fbe7b73da96c0a9f37200e781cefc59b2f0b642f11bc38121d81e6f987f16e0cc73b47d9f9ff6e9fa9f00a67ebc3f6897e362e46a6e4e59f2be9d18889bd87e3702bcfa0dfe788de0c700c45b539b0c9f2b0cb8443bbe65b90bbdece483cb3b0b5cc2e6f4f5fbe95f210060795677864fd787db43cf37768926f9b01c1cbfd34e6d7e0f1153cbd2c2b500edf977be1bc353c079583fa873ada00f136bdf2d4b38d2d6bea0bb9631c80a009de84edb751b743d85fe7815b02acec5f33907bf46e601e907bf1e399bdb8000d396a7211f9e0a2d99941c24c21549721e3e1998c61c49050b622da43ecad200454514dba9524834228474c6b682a515943455682152eaa8e96a1041b2933a00422625408cc14a9c3686c62d05020173700b817e872ebd752d1fabc1e87e7400b13daa69b915db6dc9cb7fea49cedf603d86973c72e6472cf4d487b073d220002483f3a0f3a52bb1e77a8a53cf13bd73abdf835f4b9cdbed95cecee4e62faf00d74c98eb99193c3736e9b72daa3737bd6f5dabdf10e752ef9dfcb3e06f6e3900c493660486773bd9bdfc723a0626f7ce3c279d852dcf7b0171bf5a0b72e58300b707bf1f30c4366d6e1ce201e6d7661c05c805fea493e2fc827a826e2b73720077229d2d7f3b9c1f8721af0cb98f4e99447306fc72b001bea3e6d1052456d600a5cd77ef615a0b1f21537c1ee7d73cfc0eafeb689b71e366cbccb124378b5e00d7048b6e2f6af5e74e40da82a7d2c0e0fa3e7ea58f10e753f2660c7f1f1cc6008e466dd397213e1e95dc827cabe76e44732296c4bbbbf86b7b71e01d9f2eb000770c4f3d7336d757bf017cf288a5390a0dcd6bb1a906c2a6db52dcf9521dae004f1bd9df4e976bf585b89cedc9af93e57b6b997d0cf84d39de951b72f2ee4d00771dc8c1cbbf37f1f7c21bd7f1b665871b9f439f28d9abf66426d2887e20b8009690bca6bf7e4c747b2dcd65a1411ef8cae4d9d21e12745a24fbc0da62e792003f7c9455254592ba78514f29fe2ba4ce56cdecb54241fcbcac6eaa4cd3fcbd00f4fa052a46f68e5122a18b3b6f7126341b40099121407c227281ea50775fc90041f563abbeeb32429f877523fd41e255b6c50809ef7a2bb58f7c78a294fc0a001a85bf981f631aac388dc58d77cfaa52df231ee71951b28610861086108610008610861086108610861086108610861086108610861086108610861086108600108610861086108f41388c494fc2366a7c414f11b0a5d28ee6b452ba500dea00061d4cc0477692e5c6f372c93d5d8f846a2b6702f2937254ea3e2e13d722780065c63e61ded11d538cd6ae3938a8d438c8a878c6a63ad19d6472689842150c00e656c9134772364f5b6b4a94973ff9ad96e4e1c7927c0f3be37923c2b213f50033b34c21181767fb2186212b41958150544f42dc293575f2457d6822cdff00000baa9ec9374a592728e5f452fbaa4a69601004a4137b952d214b1a5c151195009d8f3b32a1a5aa69ebdb9924731415069b014c73905c1bf3d6ecfa0cdf3075006d5e32c10e5ecfa83c1b4beb91cee6e4de27c3da94b52dbd29426dce943d4900b11727d4e9c5f8eaef6f2f3ce9278bfcb5371d0bf1d72d20269435df71bedb008d8f4a914ad453a9c1dac3476e4fc4716f1be76107cb95870ea1f23ccfa30800929b56849b56e6b5163d6a0fdd45b076f7c6d6e199ea62a0407bd83b78db88002ff7d2e638d286f5ad294037ebd8d050fcb9df07b37efe17b33fccf120c58100b366c09d32bf17f4cf3197329f3ea79f4aee2e6a2bcff0183b1f6fe86d6b6700941fd3af136777663f21ce23c34adcf4245bd2fca80d6f6b545060fd38b35a00e5fe8dd2cf68383a78124be5abe7e17e65a229d80e77f216b91ccec6d5df6b009c8d74d391e5e6e1f3e7076b5ce42c48c8db8bf0195af99b0a7feb0a1df7eb007e4390bf2af4c4ef33eaf9b9f06f53af0c99a0fc6eedaf86afcf3e2086880200bd0d36a56be55d81a1dcd6f4a5c5a1ddf26e4c1ce4ede1a0f273125ae2f9b10076e3739b93607abe42d1f9e363189641c54c629496e165f0afc6c438ab250c00c2b6b7dd5a89d825b6d44d6c00e56231310a94d1d056d5ad4132e968ea6a1600a240094c890b98a51b860024972c337d620a804a8dd8051cf2b3f1bb5dcf07002c63abe9bc5a63f4ef20c7252a6d11b92f2ac62507fd494c4c8a01e4a55424005521c4824137046d5c71ed8aa9155b1bb2952976a8d9bc0e6b1fe53330ca5500904e5bc924a5859d2fa5ed4b732e59bde5a0f8103ea7cf9c775de86ff7d41a007e74e5d71c99c8e0c3477d74e0da3dac0dd845711cea7f4fba501a9b505401004ae054e08e3cf2000b5c5defe3c5eef7eff6f189a50d7efe57d8d80daf51bd00037b30ff00517cf4d7868e7500421b7a9fcb9fcbb74c4051e37e77cb4e596600332ce433c4dfd75e3ef3f588a814b11b816f5fb1b9a6c712547883e16cae3900bebe8439111eb12457d7e9b0adf6229e639621f836438bd9b9f2d34e9696d30037e17f7edee214e74e84de87a7ad3a79f4c4ef9b3def7b0cb973d7ddd7f4d500b9e4ff004d6076e7d7b9ea39de9d29e7be277f871bb66ce496b364d73fd221004df9795adbfa73db9dc5ce20ab23a8f007eb7cf4032630fa4398dec3adbd7a009e95deb6a9c378e7c2e747c873b3e7902c322611779c09f1573be0bf8a3d2f00d7f9542c44d65796268fcb33b65e8779d69599320e6285764f9b254da11150008cbf1edcb629536902635d300c66695c963631a75a842d9e17da0ec8c8dbad0092c5b6727cc4c99b572533682a5484abf2b88d2ac4ea39c494ad499666a04a00a8284f7aaa59d3d082952c2a31eae9c5553cd9059d6014a99f756920a159120003d94d729240b911f4add22d59c8bab9913296ab698e6597671d3fcf72583900ee5e9fca5e0ec2c7cba2d3501493e17a0e6302fa5d8299cb235b62612a994300464ae650d0b1d0d10c35f2fb12c3711d9ec56b709c56926d1d7d04f5d355d200ce4eead13107349ba56858699266a0aa54e94a4cc96b54b5a56784a4cca39e00a44c494a927766235b6441c8f1491620d8b178adadb887901c6d414856ca0600a0fe846c41a10410704a8280524b83efce3752e62662428105f2235fdf88e5001cf1545c861086108610861086108610861086108610861086108610861086001086108610861086108610861086108f553298261d0a65a502fac11507fe280011fea34d977fe51b83fcc4529e2c79f382014a4fc66d63fa799e6d96a338c000aaab12c144b20ac86247f273e45b2d756667c44fb5938facbfc0f70d9989f90034ea095af5a9d2d98e54d1fcbc88a499a41474c195c1cd350e261db59888790046498479d98c345bad9868ecca24b2652bc31b10e31d99d8ef67753b7db53400c2a244cfecf6133a556e3752a49eea6225a84c9586a5646eaa7d7ad225ad09003bf2e97bf9ece848558c3289559500a81ee6510b9aae2c5c21cd9d66c7321200e5a3e768b5add5adc756a71c716a71c71c515adc7164a96b5ad47c4a5a964a0094b554a944a8d49ae3e9702c9dd0c90000909b01ba00000166b580602e5b2800e6defc07d8470a0f5de87f1a6dcc1b0a7fbdb5f125577e0ed9ea38d8f1d07000c99a7d35f77fb9f0bc2df3aed5fa91d36df95b6c0a8b9bb0393862c7874e5d005b38881ee2b436a0afe3dfa6d6adaf86f12ccff66f37b664bbe6c2272cb5e700f407ae7a7510fbdbaf3faf2ea6bb1a1ce60f32ee1db8b598f0b70e04c78429007aefcb7b0f4ebbdfc86f88de3d3d79e45b5d4b9e70853ec5bf0c4956799701009fd5c65ae9c06778919f86a075d5edcedc620815bd7f2e43d2bca97dee307000756d4b783836e1c98eb939713c2fea34cb58934e479ed6ea2bbdb7f507cef300bfc7ebcf3ea2ee0786a17f31e9719f4d074ced0bef4ebce9cb9fe1cc73e56800dee19f1b73e0dc49cb3d3326202df86dc87d4f6f3f5c46f1e3d73074e0403c00afa69944dcfcfce3a5ea44526034ef3ec7ad2a7110592f3545a9093fccb4430048e39e5a478881e221b2002a02bd3971adb4aafcaec76d654aae29f66b1c9a00458b9461954400e40751dd480e0127abd134912a61cbe051d3449c9ecfea49006d629ef0cb35889be87641888a7d710f43404c2561c71212530f279d4ca570002c0f0a520a21a06121a19b552aa4340a94a57894787762b5cbadeccb661736006998b9322ba8dd4dbc25d16275b4f225dbf96553ca932d0e3f4a0664b9a6900083225ef5ec43b5c3294d9170c0365701dcde2bbd2bf67bdbd7973b6e0571da005bc388f1d6e45bcbe59e517ec5defccea7c08b162c03b71cc429df7fe9cf97003efced6c4ef0eb673c873fb67ca2a6075b676c99981b8f872cae7481161fd000ec47cbb77be0fed8fc9b98f5e11045b4b802e0bf4d58b330b9cefac452d53c00ee3954d4537e76a72bf7c1f86b6b74fb5fa783881c3366e17bb69a5ac46aec005a276b75bd6a7e5bfad077af4c01072bfefd7d3ce258306be4df50013d4ddf0057160edbd76fbf4ef7af2da378313c34d7edd2ed95dcb452c9ccdc97f3677d00350458b13936900deb7e96b8f515237b127a62a8aaf986616199762f617cc800624f07e11343cbccf6007df4f9e20a80cfefc9fcaff2b883371e27907e20d800103af32cd11d0f4fd3b56a797af961bc2fe7cdba69e36662fc21811eadfb06003cb8310778b089fcf6f4fbdf99e75c1c7b7ebf2bb682e62a200bb0166cb91e0040e59df88d21f2e5f7faf9e0e3df8db8bdbdde2181761c6c001c9d89d082cf007bf58cb57b32fdabdaa7c00e6439566f0d1fa93c39e63982a2b34e9aae2d28009965c8f89280fe6dd3a8d8a588795cee884aa65258a5a24399190b6a304be600860e7d2fe9ced53b21c23b46a51572152f0cda6a59611498a041eeaa65a02800a68f124246f4e90e4897390f514aa628ef250994f3357886192eb920a5a5cf004864cc6b2983eecc6177c9c17473168dec7862e2d74378aec8109a97a03a850027ceb217432dce25ad3821b31657993882b549b37e5a892dcdb2fcd5b2873d00da2321db66399408e954547cb5e6231ef026d36ca6d1ec4e26bc331fc3e75000cf054652d43bca3ad9692ddf51d4a1e4d4ca2e1d52d7bd2d44cb9a997312a4000e25325d5504d28989282e6c6f2e6016749059438949706c58da2ea61a6b0e00fd12b3ee1c3b85ff00a09ffa2e586fff003fc2790adb1a645421763f0ab9e4007a1fbb466c9ae42ecbf84daca363d14dff0069b48f66082010410454106a0800eb516a1dc5f19119a149391f7f5f089c22a861086108610861086108610861000861086108610861086108610861086108610861105406663c2f44310e9f1300ce25039026aa3ff5522aa57a03d4db14a9694075103e67a0ccf8459993d12c003a884f5ccf448b9f5e61a3d0c54e16b0510c0b69d8baaa7bc23aa46c8aef53005574f09c61cca925c2030ff31cfc387ac6b6756a96e25b8196f1fd5ff440b2007e7d23101ed0ef6b970fbc0bca669956163e1355f88688837449b4a72ecc19007dacbd19110efaa0a6ba9f3987716d6569536ea1a75c93254fe6e9a35110ab008194332d89727705dafd9b7637b47b7f3e5564d42f07d9b13019f8bd54b5250075494ad2264ac2a4ac3d5ce20a877e40a39252bef272a624489976870c9f5a00a0b53ca92eea9ab70557b840375127351f845dcbd8e889c4a712dabfc59ead00661d67d6cccef664cdf3e5a1961a6d2a8591e5a92432dc32ccb195a521c7190093e5f9521d5a61a0da52de8888762a6733898f9bc7cc23e2be82ecbecbe09b001d83d36058152a6968a9815124efd4554f50489b57573982a7d4ce501beb20000002654a44b932e5ca4732914d2a9a4893253ba94ddafbca36752ffcca275200e198064800505f3f4dfd36a8daf7fa134c7210416e61fe9178016b66e5989200dd1b8f1719100650e7f4b123eec2e4f952f4c1ecfc9c7bb7a911278b67a86c00b8dc1d03172cdc1c80a5c8efe9e7f3e9e7cf07019ecff4cefecf27b452c03f0042185f267b971a139b8b6b6876fcff00aff6ed83fbd736f5e3d225b237c9b3000e2ed67d2fa870407689fa7e553f3e97e87737c1febe87d4f2196b06bdc378003d81d2cf7d4f3be6008fbfa7e7bf3dfe4de0e038be57f7cdb8b671240c81e900d1b56e6497b8371a9872f2ebcfcb9f3fcf076fa7bfa794411663d491726ec30036625f3ccb1ce1f7f7fd397ae1bc1db90e8c757cbc7c070830f912320465c5008e9aea6e4d8473f5b6dd3e9f89f2ae27dfbf6dce01c97637f116b666d70ecf00ab5d8989a72b7af9f2e56e44dad7af38de1e7d7debe2f67789616c8bf2e8f7000c323671990466d0a7e5f43bfd7f0c1c73d34cdf87df2e710c0e8726b8b17300a37019176660e3334238999ac4c9f43b3f4442beb877a26025f2a2b42428aa001a7339974ae39821495242226022e26196aa0294bc4a149584a8756f6d55b30028bb32da899266194b9d228a8f7800ea975b89d1d2d44b620da6d3cc9d2d560070952882080463d55a9e61762c079ac0019b221c39bf38a53c114f1c8ed34900fc95f7d6f2a459b22150adadc4a842cbe6b2f81896d869bff5b6d2e62ccd22007c47f956f443de1248584f01fc35e24aa8d92c630d5cd2b561d8e2e6cb429600e24d357d1d3290808174cb554d3d64c1a2a64c9845c2a2c502bfba524e617600726c08496238120f893179dcb6fe9f7debdb1e8a7cef9ff5d7a5f52da98cd70039faea0dc8172e41619bf2b888dfd07cfeff0001802c431f3d2df472d12e4600ad66bfbd1dc369123d3efe7f76a8be0fcce4de8cda5b2fdf53f858e57d1b9d008903ddc8fe9f874fcfd79e0fefedc34ca21db81e96bf10c031f5f02220fcad004fd797a751c8db12fcdcf3e77279dc6b6d58bda5ec38b58f8393937273910e00f9913f96fdee76fa7dd71009f2cb4f571980de8200e400d35cddb4ccdc365900e8d114efd87d2f7e5e7f960f6c9cea4fb7f17d7903104820d9cfafcc93aeb600d6c043efefd2b437a1c1c7df406e7c48cb3bfa432360e351a1b916e39d9dcb00f487afa74c016f79f5e59e57f4225db5ea0f1e4d6001c998fcc4edd363d79f002f4f972b8b624976e0cda699736637b5cbb7238c9f46e3766038b07cd9f30300808fbfcb10f667b6796bf33104d80d2c72b3e5d4f3e7668604bfdcbf13e7c500d9e2779ddef9dcf3f0b819e42e070022ac68d6b9eaff000f39de5fa8da27a80099a34df394b56d96a7196666ec198c61a790f9974e204fbc96cfe4cfb8da0c005c9677071f298d40f771706fa3f94ea31bc0706da4a19986e3b86d2629453000106455ca4ccdc5105226499969b4f392144227c85cb9d2f342d26f16a6ca95003d1ddcd426625b25007c41cc1b9629650b8caf1b39f08bfb49496d893e51e3003f4c1e79e426025eeeb0690c333e27a81985766d9b74ea651acb687090b99400de3f274dd2824bccc97242006210f95f6cbf0c414a9d59b118aa520f79306000d8cad4c92ea5a64d26252a5a89194a932eb6496f8553eb8fc4b8e3f538082e00aa498c6eaeea6e5a9dd42c0e19058eab8d91f87ce317870e26a4ec4eb4075b00b23ea1b6eb0e443d24954e5b86cd72d69a59438a9de469c2603364928aba55003691c18750a4bcc29c6568715e66c7f6436b36467aa463d82e218610a0913a006c93328a695071dc56caef68a796ff009a9cb20ba4b1040d2ae556d1965a2600cbd03a77a597bfc258a09e61cc5d1b33b58a07994a80ff0079a2507cca5454000d7cc0e6072c685354a1fa920f3163d58b8f94568af586df483c4a4b1b722e000f981c23d8353683700aad4d28da8e248f5f1202900772477a62f26a259cc9004f220fcc38f7a46522ba5299c949ff00524fcd2e3dde3f7a1e69cff8b71b5f00fd45a55f81fba62e8524e4a49e841f94652672159107fdaa0af918f257ee870015457be9e3e87ed0a8eb844b8e23ce1844c30843084308430843084308430800871c442bcff0bfe184415246a3e7f288240152401d49a0f5ae04817258713100066245fd721e24c7e5723a11aff5bedf9215ef15ff00e140511f2ef8b6a9b200d39ac7405cfa3b78c585d5ca4e6b474077cf926e3c63f03b3a6124869b71d200362a21b49f2ff52a83ba41df95f16555491fa5255ccd87d4fa08c55d7a6e120014aeac949f99f40fe463d5c4ce625485a8adb8669095296a4d13e14004a94b007564f8424024a814002a49b57164cf9ab2026da320392f6e67a3379c62aeae0072ec92100d9922e5f9972fd1a3187c537b5bf81de15216650f9b757e59a87900f2121dc718d35d227a0f3fe6b898b6e2130ca809846cbe3519532b45b4b2a7009e633766491c508469c761a1a29d30ec3fd95b29d8eedf6d7ae52e97079d8700504c500ac531a1330fa44a0a4ac4c9689a835956822c9551d2cf46fa805290009de526fd3e195b544112d4849ce64e7486e2c415a8734a48e79c6aff00c68700b7ef89fe216166f923426093c3469ac6ae2a15c9865d9a3934d5b9f4b1d68b001eea619dc3308c65743e3fe15ee325cbe593681716a83566999c3a14b7fd5700b0ff00878d94d9b5c9afc7d676a31497b8b12ea64a6560f4f341de79742553000d594bee6f574c9b2660017f9494b2c9e4349835348dd5ce3dfcc0c7e2004b00497d10e5faadc581dc72230351b1b19328c8a984c62e263e3e3a25e8c8e8e800d7dd8a8c8c8b89754f44c54544beb5bd111310f2d6f3cfbcb5baebab538e2d004b5127d0484a252132e52532e5a1025a25a121289684801284200dd4a129f8005294b0480c1846e136b0019ac960c006b37017dd00e6dc63f362a24f1e1f2f00a59f98112fcdac081e0edc386772c350044d7d3e5ebf3a6ddc81be0f91cf97008307fa8e1d4c1fa3f0033d007e6e6c0e56b3b08e9e66ff007cbd09f3c4ef58005d88f9f1362ef91bdc9c98393d85c0373cc96b12de57cdee2d79e5f3e9dbd600837f4b0a8c4126ce4db89d7970d221cdbc4e97bfa6590d4060f788fbf2bf9500ff0043bda98390cda5fc7ddb87d40e433e0c35d09b5f2bdb2f181bf5b773f700f7df07f5be40deff0073d39b4492f7391b3bf0d406ea1c0b3e44bbbefcf6b500afcade7f23f5e77ebf319c1f52789b837367c8330f3377ce24f9d796ff002f0041f7cb07cdadf6d6fefe6e3626c346e84686c48b0d393dcbc7f4f98fbad7ae000ff6f9fdfd88873c321c18821f9bf13d464cf0fbfbda9f8fe383fb73cdb5d100fddde41e1a3e4e6dc7cc96d720487261f4fbd86fdfd3b8c4b9d4f2bdf30c4800e3ab5f5ea619f137bdc9d34e976bbb1b17b85ad6079fdff4eb88724e77c9fc005eedf4e510ee41246432278ebd3806e568b33e37276ec0e99c864cc3eb68cf007364318a6d0b481152f95cba3e256c3ad9fe75b6998392c89f122c87619af10091e340579d7f127892a9f64708c35130a1589638899310143fbda6a1a3a85a00d0a05c9426a67d24d2dfa568439bb1c2af57f76816054b7607400bdb4b907300cf4772688f03798441678cdd965c7db6913fcb90f3165a714ca0c546e5f8e000869a60ad41e71f4414e66311ee190a0a876621e70010e950eb5fc3662c9a5d00a8c6b085cd084e2b8422a25a14523bea9c32a53b92d00b2953134d5b593425003ff172e62d56408c7a1534c5a5db792e39949c878127a03193ca5bb580b54d00f6269b57e7caf8f66ef1be77e7ccf26c8e4d9df946cfdfbf7a408a52b6defb00ed5e9e637bfa62778972e0b3123c79ea180e5a392f0f7efdfec036b6fb0e7b005cdf7bd0d2bd6d4be1bc6f7e1a7a5896b1cee6dcae89f0ec2943707cee41df009f2e5b9a530de2e4bbeac3fa700fd3504c222847ce96b9b6ff002e5b1389de0037e0d70e35eb93647304d99cc4bebec7edcb2bc29636b8df7daf7bd3fa8e5600ae2378bd8e6ec0335db3b9e66faf8c47bf7efa4083df903523737e7b5fd6970036386f1d186649bb0fdb83f40030897cf2f7efed0a5796fb6c397af51de97300bd44ef1be7d5f891a80dc5ad6cb4688b7879fda24a48d813dfe7b8dfa7d9b4006f9f62df536cf239e5689777fdfdf9c7120d45b7bd29fa53974dbb6c2778b100cf8706b64e6ef9def95807107e37d33d224835dbf336ef6ec2ddb72311bc6d007e2e6cd7e43506ff00d5a220474bf2f3a0a93d282bf2f9e2428f1eaf71e00500f2bd890e6e0692fef8b71800798e9e7d00f2eb4a9d853a37b987be4436998200daddf21cef07f93787bbf5bde14de87e55f502bbf30056a7a52f86f9d7d91e00777e2c0359cda0ff007f1b39f16e9ca3d9c9e7138cbb33829de5f9b4ce45390096c437152e9bc9e3e2a5734818a68f89a88828f827588a84886d54536eb2ea001d411e24a8114c5a9f2a4d5495c8a8932aa244d4944d933e5a26c99892ce9500cb981495a4870a49043162f105940820105dc10082f98208620f0368ca9e8000fb6c7da15a06994cb53abe357b2acabc4819635b254d678f8b655b34fe700e00cb351cfba00fc327f8cbdd3144a3dd2da1ee8f52ed0f61dd9ced0f7d34e0a30007ab9c4135581cd340506ec5347bb370dbff003351052ae497f886ba7615430039c995dda8ff0034a2517d1937475f863307a33fb4d996621a6a0f884e1a2700b2c884253efb3068e667809d43c42cabf9ca3296747244ec0a129354839ce6004b511e12057c43a631bfc2dd482a5ece6d44898924eed36354b324a921890f0059422a04cb8bff00c065800f9eae6e007391500ffa66a5bfeba1deff00e81100945d32f6dffb36f52912b697aeebd3c9b4ccb69fdcfa999333765854bdd59a007bb996616a5132c950a11bae27f8a170891fcc62280d3aa715ec23b4dc2ccd0050c013894994e7bec2eba8eabbd48d65532a74aae57fb7f2a17fe98d7ccc2200ba5b91284c03596b4a9fa2490bff00ab190fd3ee23b4335496d37a5bae9a5500a86eb884b8dc3e46d49ca59a9f5215fe9261a49398e7473fe55b60820822a000d3aef10d9eda4c1c138a6098d6180382aaec3ab6913cfe29f25093c73e7a3c0062a915727f5227cb6ff325691d2e1bc22b426691c367eb4b5d0d9ffe0afe7800d48a89a3f99fa81f6881553c7f3be974a7edf38fd099cc5277430aff00d450003ffe5581d7973c562aa67041f03f4317056cd19a507c08f929bd3ed1e413b700b9b2d1f252d3f99c4fe695fe44f998a8572c7f225f9122397efc5f3874ff0000ed543ff87f3c4fe6cff907fef7ed157f105ff93feb9fb472fdf87ffe187fed008fff00f3c3f35ffe19ff00e21ffe989fe207fe6cff00f10fff0044419e28ff0000fb30ff00db2bf240c3f37c25f9ac9ffbb0fe20ad259ffe213ff7447133b700393081e6e2cfce80570fcdab440f33fb4527105e881e2a27e823899dc472690091e6167ff8c623f34bff002a7d7ef149ae99a211e3bc7ea35f7ac7815378d300fe9536dffd46d3ff00c7e3eb8a0d4cd3a81d00fabc506b271c8a53d123fef300c7e28b9c3ac30f444647a6161986d4ebf10ebad42b2cb484f896e3cf12da1b006d29054a5ad494a5209240ae202e7cd50424ad6a51094a10095289b001290e0049e00127c62d99f3d76ef1649b326c4ffee80ff58b4cd4be3c3835d25868d8008d44e29344646ecbfc622a55fe2465b9ce634adb34534d65791cc26998e25f000411ee21a56f3d507f92c69cab0cd81db8c69484e1fb2d8f54a6636e4e561d005526998e44d5d4225532125eca5ce4a58bbb5e2ea692b6733489eb7c8942f7007ff794c91e2631cbab3fb41fecf8d3d6529c9739d4dd6d9839e34a61f20e4000984920985a455263665a98f645096566def25d0b3471373ee48dfb2707fc3a00768b88abfe1d270bc0e586255886212e7ad40e6112f0c4d7fc4384d54a1fea008cd978256aff005f77247fad6147a012f7c799039c62a75abf698758e74b98004068070fd90f2240b8d3b0f053ed4a9dcdf3ecf91e3aa5b983528911c9f2580008b6c10b44245bf98a0db700f7ca8a6c941edac0ff000bf8248eea66d0ed160021882c14a974f8648938753963796674f15b3e620e456814cb21f7770b11b1009580494b19d3a62cea1004b4f473bea234fe53d34c2e7103ed15e35789f43b0007ac5c42e7b9d481e69d61cca1218c87c8d92de61d5125a8dca79261a4123900b1457dda2267105308a0d8f0189502a07bc3677b37d87d95217826ce61f22a0012a0b15b508557d7029003a2b2bd5513e4be6532572d054c771ef1b591454b004ffe1494023f988df5f8296ea1e044594d3a54d45ebf52686fb5452b5ef43800e6fbc580f5b73e56cc0f0cc3bc657bf9c453e75a52dce9717a9af2b1143be20077fc2da644f37e56b6675c9a5f97bf5f4689a1171f9588bedca9b5edd69c9b00c4bddb965d483e659c78e51111424edd86f434b79d3af4e830deb38cdcbf0b00fa73039127525efdfbeb12534a77f3a54d7981f75f5c378866c80163addad600cf3cb367d1a25efec5bf78148eb7a7983bf3f4da9b903be24289e1a3e7965f0057b1197488852e6d6ad0d29bed6da82fd3a0ee2378b6af7673a1d79e473d4d008680fefdfca24a4da97f4ebea7e761f5c02cebe3e82da0e278c4bfbe1d2208003b77e5b73faf4adef4f36f6be8e78f8661b2716723847afbf3852b502a2dbd00ea4dafeb6dec05e830de22e5880788e6d93b6bf27312e7d5fc62286a01b6df005bf4a9e9b1bdb0de39b8d5afebe5600e79b1311121269cfbdfbf314aed7a5e00bbf622a3a1e8f767f3195b9390d99869efdfdbc4c294ad472a0f33b53aff00007e9892a7d48bbe5a67c0f4ccf1c8d9efdfbf9c0249a5b96f5d8548f9deb4af002ef4c41517e199eae2da5f2b123ef08786c3b9ed41d6a4136e7b56dcb0df3e0017b666fcfde791160f7ef38c6271c998046e78ca3969b886de6e419722262e00b6d965661a373047a90eb2f2907df21e5c1c9a5f11ee5e0909877e1de6814c004151f18fe24b16155b4f82e1089a26270bc257533109293dcd4e295077d0b600254998aa5a1a39a50a6025cc94a48f8cc6b2b94eb425df752e79151cbc923c0019a2deb427348c9dabb90e76b4256c09eb12b8cf12d484b7053f43b228b89a00a50e29460d898b91886c27fce5b0968a9095950ea9ecdf1bfecf6dcecd62640024ca4e252a92a0ac94a514b892558754cd24025e4c8aa5ce48c94a9694920100262c485ee4d42b4de63d15627c017cc65c233883953bd6fd282c0de80f96dc00ed5fa4048e41bdf160f98737bd8346ea1cba52a3b5b98b916b6fb50ed882a000e3df20dc79f5045a11006fd8d81a814def5173726f5bed4b6277806f20dabe0062dedac2112052bf7ebe66f536db6c415001ff0077e2cc74d4e5d61106dd2c0045bd7aef5dcf913635ae242af6e0e38b7420063967f56408a820f6a5ea7d7700ef717237b8c46f7935c8caccfd478663c912453e95bd76a752056837a7a8c0002ae1b83df9bdb87239f06cd9100f6adceca1e9cc579f7a82772713bd9f26cc00101fc45b31e06113da9b9a1d8f43537e6074a8fc4ff27636f3cfefd611005000d7b6d424d3977ad46e7e42988de1a5eec2e19d81fafbb4227d36209ed5b93e0062b5edbf204cbf8f4e9f7702e6f67ce10e5b7e35b50f9549eff335186f3b5f00a73cf8b656e274601a1114e553bd8ec7ad2be8935a577ef4150eb67f0e361e00f520e689dc1b1dc1a5af71f97e95ad6871e0cefefd38b1e1088eb51cb6b127006dedcac413d6f6b623787bf1fb10d9bda113435df9d473a6fdf9f7040d803400b37c72c8e4e350393db3d48bb5e10f2f3a56953735dc546e05afcec2a25fdf0093bd9983dcbc223b6d7a6d5daf5db7bdc9a9206f6ae2378e8c5c696cb97360000677cf9a250a521495b6a5256950525695292a4a81aa560f2554541170763e00202b248b82031b5f5191041d079317ca115c72af139c49e4465986c8fc426b007e4d86864a530cc655d57cf99798874241094b2d4a73041a1a4a450252da500012294db1a1abd99d98c4095576cee035cb55d46af08c3ea54a27fcca9d4eb2006fa9d4df38b4aa790b2eb932964e6552d0a3ea93174b92bdad9ed1ec82c37000d23e2d753e60d363c15ceab906a43c4529fcd19a8523ccf18b229feb54478c001bf88926bc52bbb21ecd3112a5546c7e152892e7f222a30d03363b986cfa54000cf20966e918ebc3686612554d2c3ff90197ff00cb296f0cf58abf2ff6e9fb004ea05685af884974c12920a9a98691e8db8858047892b543e43867403420a9000e215452a8a49a14e9a6760dd964c047f67664b26c0cbc631a0752087c414200e086045db23168e11879ff00886cf29937ff00afd32e5154e4dfb42ded12950096fe3e71a3b98820afc5fbdf4c21a1cbde2ff485fee09bc9000ddbc1ee836400ff00be57cf513ff0e9d9bce732a46354c0e92b155ab772cbf312679be7f112002f988b4704a139266a7a4c3f5063bac27ed1f71e90e55eff0027f0e31a15600022721e74404db74fc1ea5c2a89373fce542db52d8c299f86aecf881bb57b48008c8fc388519241399dfc35400f01d620e0745c678fff00713f5418fc913fb4006bc7d3c1cf7597b87a832ba847c3e9fe687033514051f15a8111520dc7bd2e000ad010ad8d69fc367678967a8da25b1b856234a09d6fbb87a7a1609bb97005009fc0e8bffc6ffe20ff00e88a6f3bf6ff00fb492689526035034eb2e129480a0092e93e507d4921654569198a1a7c9f1293fc8a0a4a93e1ba1295d578d953fe001ebb329441998762354c6fdf62f5a9196bf97553b806e199bf989040358c160080672d6aff0074d5ff00dd298a711fedc2f69e46a549ff006945420582098100d29d17875804107c0b4e9e15214370a04104d52410063692fb0becb25904ec00b8510729b8b636a06e343888717e1c8da2e0c270f1ff00ddc1eb326fff005800fdf578b78cefed28e3ef505f75ecc7c5eebdb61f512e42e59d449f64596aea00285265391e2b2ecb0b641b3420c360d15e10a16e4743d99f67b8725a9b6376007cee80caaac369ebe68606fded7a2a2682daefb9f9de450d1a3f4d349eaa9600959f35851f58b61ce3aadaa5a88a0ad40d49cfd9e969515a579c738e61ccaa000b06be3499d4c634a555a10b4ff35686d418e554585613868230ec370ea00c00cd43474d4ae1b22244b43e46d1909972d1fa10847fb5294fc808e835574e7700a9a8e5d2a7e800e60ef8d86f0167bfb39e5eb15c40036bf6d89ad88dba13ce00a9d8d6f73819fbf97b059da113cc501e95351cafcbcab5b134a7507e9fbbf800585fc8e4d7447406d7b126a6dcc57e5eb6ad7070c4f0ce11cafe7d4fcec2f600dec6be7b0c46f8e59f93ea72b656d1f898471bdc9371bd294a026d5b50dfad0086f4389ded742dd6f9300e4ebe5e3089bf7daa6a2bd29b52e3b74a9a1c37c00071637e5616bb9fe8e585c3c220569ce95bee0edcb6a508b800dac09a60e38800e5c395efa7cc5838108e57f3b9df7a6d6b0deffad0d9bc35238e8da1b0d45e00d68441039da953d7d6e39741b5bb600bfcfcfe5e39e62d080b6d4d80afd0540057ad4723ca96ae1bcd9dbd4f316c88b139db56784396f515dea36039db6ad800fd4e20aaeda81af32edeb9f93d84220d4d79037adbd09a8b5280ef5e9b5a7700837a6b9dadcf930e3089a5c13b529b8373e62a79f326fd2b83db98079659de00e33d5cfa42205bb9a93bfcb9ec6b5a1b5ef7dc156bf2cb9bfcae08d48c9d8400226f4e9b9e47f2e7cac4f337b60fcb47be9e6cedab1b5bc103d40b9241ad2e002f6bf5dcd3cef7c1c7a3bb1d387f57108806961534ea0017e86d720837dfb100383fbc99b5e96cfeef089bf21cfb0aefd8d396f43cbcc540679685f4b78f9300c23077aed9a4671d5dcf93b425298733d7e5707e05a9c4bb052043722848900056842926321e5cdc62db293ee5710a6829610167e6ff006918dff6836e76970013013dd2b129b494e524a92ba5c3529c3a9a682402f3a452a27293925531490004800c6967af7e74c3a6f103a26c0eb9b3e7676168a4b8e1116633b7a4f9bd0019f74e32766a0eade889a492144c9c524215fbe209265f3a4848091e14cd610063036b4a12971bf0b884a52b007d26d86c7c6d2ec8ecfe33be664daac3a4a200aca83135f4af495ee2d635922714b00148dc50480531bd94aef25217c407e40043853eb9837e1ced150f958569ca9b73163d3a6fb6d8e58e490e5833679b5b00d5cdcf3cda2e35db3f9e4e6df767b3670e5d47735db63b1ad7e96a0c1efc0800e01baea19b2e7a9810ccfaf5fb7cbc5a27715a7723fbd36b7ca988d59f887f00e8f63003cef66e9f3f772220df9d3e97f2ed5141b1069d312fe59358db317200e3f7049b9306f7f56cdb2cf37b4003516ec7615aef4ec4904edb73c09e7ab80072490065d0e6dd5cb58c19fa5afd7de7cba449bde876b0bede47d4006bb7ca001f216f4e3c73f17106d465fd439d065fd6208b8dcfc8817b549bd456bcf98b000269215c380f4176c80c9acc599af06f1b3e5f3fd9c44d36a721e5e56e9bd600dbd310e6f7bff4d7c037a41bc32cdf525b4e01f5eaf6890926f43cb9034a7900569f3adf142965258310c73736366cc5adc0447d61e03d15f5fbfbec294f7a00ae09f2f63ddb5743c06b5a1af97ea3f0a61dea99ac4677079f3bdef78428470023eb5fcf6f4c4778ae03cbf784453b9da9fd7cfbff004a4f7aae03376bb74c00f27bb7eee853b9fb35f3bf3bdf13deab8275d0e46cd9e434f0e108529616fb00fbfef88ef55cbd7e87cb48429e7f74fd3ea6b8778ab585b2b1e7a3b6bc341000853d7efe5c86066a8f0d38e81b8f330878474fbe9e5dbe7877aae5ebcaf73900bddf99e508506f7afe3e7d71266a8e89f236cac2f6166f378400a733b5295b0052dcb6e5f3f4c477aa3a25f8b5fcdfdb421416e54ad29df9fdf5c3bc55f2bb0039bbdbc7db4214ee7cf6af9d29e55df6e828ef55a003c0f176b936e5f72e85003eed6c3bd5701c1aecc7c7de79de113f76b570ef15cadc43fa13088201ea3f00b536da9f8e244d50d13e20f2be79da10a7f7e77a74f2fb188ef55c98642ff5003ce10a72dfcf0ef15c00cb27d3c610a773f76f4b74a1ef89ef55c13ae875cf005bf8b8e5085011f9dab7df952fced7c04d234d1b336e8ef9758429f852bf9f004af7a606692ee33cf2d3c210a5ebfa76dba6df53877a787a9e7f7f41081153005fbfad7e94dcf5c4f7a47f2e8d99f7fd070108014f33b9eb877bc881a31cbd00b9d610a773f8d7ceb5381980924837e5fbfbd610a5297341cbeefe95a7a60600683a71d06b9eb0853b9d88e5cfbd2bf77be1de8e06c41f2e4edcfc6d088f0800efce94b52bd29f7735df13de8e1c05c3bb5f53afd9a1134df7b904d3b72f2c00477a3572dc40d7c73f661114e5f33b1ea6b6e75e5d68001522b0a7015e42dd00059f881e4097b44b7be1d7e9c4e513d7ebdbad2943535af3c1fcecdf7e16cb00836906f77cb8f48817a50daa6dbf90f4e82d4db6ae249cc11c1ecc799c9c3f00dcb5ec6f7f3be5fd43448fbafdf604f2e95df10797d7df4d78b41babe81bdd00a208e479fadac2e4dea79539d37a624123e7a8e2598166d4db8e56837eda7a009e5f4e30a7524d0d7cfa74bd7d3b0b50e74e0471d2f6e7672da6a5dcd937cb005e038c2e2f51dea6c06d4ee4d6e4915c1dc375d3c78b06e56009f137ae5d210043b6d5bd390fa52dd013537b0ae2490dc4e4e49c9f2177bf12cc2ce4c1bd7a00f86977d19e29e6ace6f6f2169c671cd65d533112c92c5265ab4252b57ef98d00025f24052a0a05266b1507ef54a4ac36d78dc29525252788edd63e3667647100ec6428a26d2e1f39348a48048afaa6a4a1201b048ac9f24ad83265851215ba00c6d4e5f772d6b3980400e1f78d83f0625f896b460931f36a3470c211925e0600b3b98a9466bd3e8b88716f4ae25accd2669d79c74265f1deea066ccc336a050030d0f091e88189536da821d8a9c3eefbb4b8a79c77d65f872da3ef28f1bd97009f314a5d2cc4e3340952d4b6a79fddd3574b96936972e54f4d2cddd490954d00ab9b33752a2b52f6342b74ae59391de1d0b03ea079c5fdefbf7e9cbefcfd6d008f4e6f372f3c8b3bf9e41fe6d9f114def4a1fd2bd8ed4af7b545c8281b7dbc0039f80e4f784481fa927e55fede406277875601db23ef3b9b0e96408a54579d002dda9bd77db9f2b79b7b2cfc9f3f3233f323ac2237fcac7a7f7bdb7c37864f00c32cae72cf3fead0f7efca269f226bcb7d8fad479e20ac5aeed6d72eacdd2e00dcda111b72e5ebcbb126df5a627785af9e6f6c9c6ad707c6f08e543bf4ebd300ad0d88b7d69cf11bc38f339166be9d5add4bb423c8dec7cff218c79ca3bc18009fd235e67cbc6f08e78b4e789f330860e789f330860e789f330861bc789f38004313beae3e83ed0861beae3e83ed0861beae3e83ed088a0e83e430df571f4100f684283a0f90c37d5c7d07da10a0e83e430df571f41f684283a0f90c37d5c700d07da10a0e83e430df571f41f684283a0f90c37d5d7c3ecd0850741f2189df003c07afde10a0e83e430df3c07afde10f0a4f21f87e186f9e03d7ef088f027a007d4feb86f9e03d7ef087813d3ea7f5c37cf01ebf7843c09e9f53fae1be780f005fbc21e04f4fa9fd70df3c07afde10f027a7d4feb86f9e03d7ef087813d3ea007f5c4f79cbd610f027a7d4feb8779cbd7f6843c09e9f53fae1de72f5fda11100e04f7c3bce5ebfb421e01d4fd3f4c4f783507e7f6843c03a9fa7e986f8e07d003ef08780753f4fd30df1c0fa7de10f00ea7e9fa61be381f4fbc23c6a1451030095cf7b75fbdb17d0a0500f0e9c4f3fdf84214dcf9790076bd69befb7d0d2ad00f1e1ebf7f7a3877d2208ee6e2d4f5bed51b73f9601434fa1febf5bb3de111400fc6bf4a7e1890ab1376cae34fd5edbe4e61e9d3deb1343f9f90a7ebf8e2028001f7e5e7cbc5a0fefdfca013ca94f9814bfcbada9e741892b05dce4cf6bf0eb006d7ee610fbf3dc7caf86f8e3e2c74bd8f2e2210a77eddb9df975bede986f580016ccdb2f7a3371b6b08b03e3973bfc34a32a69f4244ba87a6712ee669c34d300ab6c2a5f03ef60652cc4212026221e2a3dc8e884b6b51437132865e536a71000c38df98bf11bb45ddd1e07b2f226a82eaa62f19af421652f4f20cca5a197300024b4c97367aaaa6eea894a66d1ca99bbbc9414e0572ec896f9fc4472164f86007e5ca31b78f26c6ba18422aa68ae7d569b6a5e57cd0b714897331c202789f1003810b91cd1260664b5a1b23df98365efde30ecaaa85464143288050143986c000ed22b6536b706c64a88a693522457a5d402b0fab069eaca929237cc99530d004ca417499f265122d17644ceee6a55a3b2bfda6c7cb31cc08ce636a6dd421c006d495b6e212e36e21416871b5a4292b42d24a54952540a54824281040bdfe800ba26226212b96b4ae5cc485a168505216950de4a92438292920820b10c4166008ddc79080682840adcf3e43caf6b826b7b62a73c4f9f03f4f430801524edd300c88a0dedcf6bf4006c60a86bd33e25efea6fccbdac8507226fb545b7e9b9a50069717ad7ae0fa3f26f5bf96bc39421e1f3237dafff0056d7e95b7cb07d43780036b7faf9728408b6c400072adaa2e69d3720dabc8ef838f3fa6708822e37a5000548bdf9f7a52e3953614c48530b1e4efcc5bcc78bb4226836e7b12413f2e500e46d40310ef91f23e2d08e49a52d6f3dfd7efe961626a8050ff682e3993c4f00ed08e58b7be381f4fbc2186f8e07d3ef0861be381f4fbc2186f8e07d3ef086001be381f4fbc2186f8e07d3ef0861be39fbf184313be9e3e87ed0861be9e3e8007ed0861be9e3e87ed0861be9e3e87ed0861be9e3e87ed0861be9e3e87ed086001be9e3e87ed0861be9e3e87ed08627792751f2f9c21838e23cc421838e23cc00421838e23cc421838e23cc421838e23cc421838e23cc421838e23cc421838e0023cc421898430843084308430847050dcd2bb52de5d8d6e36e95ea317907e1001c01721f9fa5a1000016aefcf7a5ae2c4f6a7cfb54e390cc7cf89f0e5d611c007c23a9ed5bed5e74a0a007fdea13bd36c1fefccf1e6798e7d6112126f51bef004e4297a7736b014dc73343e77b6b96972fd3edc0422280d2c01236a74df60200fd40a1bed502b2fefc5edc3faf1843c3b75bd452b4e62829402b6de9bf2ae100bd9b160fa1d785b5f53abc227c229e44dafb7e35e96dc6c7730fcf23ebf78400705a9b690b75d5250db692b5b8e2825084369aad4b52bf9528480a52c9344a004155922f0a9894214b5a928425254a52c84a52901d4a5151012000e5458017002d08c18eb4e7d56a56a5667cd2db8b54b9f8d301234294e94b7239624414b900486dda187318d3466310c04a5288c8d89342a5294af9d3b7db4aadacdacc63001a4a9469675477187a495b230fa448a7a4212bbcb33a5cb15336580009f3e700102e63493a6199314ad1d93fed161e799e64c52cc70e8b50c210c211989e13007523f8f34b60a591d12a7e7f9214d65e98974b3ef9e9721b2acbf1a52d00a200d2e5a812df7efa7e22262a5518f3ca75c5179cf72762db59fda2d909145513004ccc47678a30ca9ef0a4ae65204956193f750df07e593f94df58ef264da39c00b5a94a5152b6f4b33bc94013f123e13c5bf94f959f529262e7bf948dfb57bf002a13e9d6e3a8a63b7828ff005c9afa78e9e51930077fea3f1edd3cf9e049d0009f03627d3eba0166842df3bf424f5a75c4151fa7870b35b802ed971743d0d800d45f7dfafcafb72eb897e76b38b86660fd7570f965a4200d295e67cb7aefe70043d2fc8600ddeee471f0cf361c2f90b9d1014df73e95fa5ba034eddb0de23200b701760fae7e59eb0853f5db9dfeb5e788737b9bf87a74844e2d4c2490f7b0001ea610c5b8430843084308477dd2ac8ffe26ea869be9b7ef4fdc9fe2167ec900f91ff7d7c17ef2fdd1fc599865d21fde9fbbbe2e03e3fe03e3fe2fe0be3e0b00e2bdd7b8f8b86f79ef918589567f0fc3abebfbbefbf23455559dd6ff0077de00fe5a44c9dddf79babdcdfdcdddfdc5eebef6ea998c12c09e009f20f1971f6a000fb1e7ff0006e699e9b6a2ff00b44ff8cdfe2167a8bc95fb9ffc23ff000eff0000747c2c823679fbcff787f89d9ebf787bcf83f85f83f8182f0fbcf7ff001600af07b957597679da97f6f710afa1fe05fc2bf23468abef7f89fe7bbdde9e89003ddee7f0fa3dc6dededfdf5bb36e87718f22a7be514ee6eb25df7b7b501bf4008e31851c76d464c308430845fefb373819ff00c211c43c56827f8a3fe117c30069de65cfbfc57fc13fc7de3fe1d98e5f80fdd3fb8bf8bb25787e33f7efbdf800efdf0af87f85f77f06ff00bff1b3c2b6f76c3fb1181271afe1dfc4f7aba9e800bf2df9bfc9377e89ebef7befcad5fe8ee5b73baf8b79f7c331b53a6f728dfd00ddef880676cdeeec78708f4bed0fe0c7fd82b8959df0f1fe247f8adfb9f2b60053ccbfc5ff00c1ff00c0bf11fc512dfde1f05fc3ff00c539c7dcfc0ffc4fc400fefb77e27fe33dc43ffa317761b6b3fb69804ac73f21fc37bda9a9a7fcafe600bf39bbf975846ff7ff0096a57df77ddee46ee5bcace1266f7a8dfddddb90ce00f97361f28b1cc7308bb0c210c210c210c2119ece197d883fed19c048e37ffd00a77f83ab9175873aff00863fe0b7f10ffe29e6d9d257fbb3f8d3fc5991ff0000faff00f843dffc67f09fff00257ef1f75f0932f84f7913d31b41daf7f02db4003b21fd9efcd356e1749fc43f8b771ff9ca5524cef3f29fc3277f83f9adddcf00ccff0079ddbef4bde64e2aeab727775b8ff1246f6f37ea00bb6e9c9f8de3020078ee78ca8610861086108ce1e8afb33f843987039a29c68714bc67668e1f64009acb9af3864b964aa034666ba8502ce61cb59a73cc9e1601988cb5171d3651008f93e488e9bb9111929848361c2b83110b73dc7beea1c5fb40da897b618b6c009ece6c9d3e393b0aa6a5ab993178b4aa05991514f47354b526a12895f04dac0044a0944d52d4197ba06f6ee32a7ccef572a5ca0b290093be136201d6d996ce003d14db844f63bc2cae65132bf6a9e70994ce1e5f18fcba5cae1535361931f100ccc338e42412a21c9606d84c54425b60bcb210d070ad47c2938bb2b6a3b525004c96999d9bd2cb96a5a04c58da5c3945082a016b091349514a5cb00496610e00f2a7ff00571ffc54fde29b7b29bd9bb22f68fe7ad5ec993bd549b6967f86b9002e4b9a2063a579620f33fef6899c4e1f950838b662e6f28f84618f74978bcc00adf717e228f769a02761da46deced83a3c2eae4e1b2b11fcfd5cea65a2654200e9fba4ca9426efa54995377897218803578aaa27192124242b789172da3f030018dfd54d38ccba3da99a83a519ca14c166bd36ce79972366286295a5289c650069c464963d4cf8c02b8676220d6ec2bc2a87e19c69e6d4a6dc4a8f3cc36be900f15c3e8712a4505d357d253d6485382f2aa65266a1db2504ac050cc281040200088bc921490a1928023a10e22adf077c33e69e307894d27e1db294526591da008b991107359eada4bed65aca92b85899de6fccab875b8c2229724cb52e9a4700c2402a221ccd23d98495b6fb4f46b6a1acda9da0a7d97c0313c76a52662282009cae5c90774d454cc5264d2d385328a44ea8992d0a584abbb4154c2921044500331625a14b3a0b0e24d80f33e02f15e3da75c10493d9fdc4942e834875026b00a9504fe9be57cf4acc73790c265d8a11198a6398a09c97a65d05319a33ee610051246d687cc595baa7d614da02120e9bb3ddaf9db6d802b199d432f0f5a6be00a68c48953953d3bb225c85899de2e5cb2ea338829dd60122e5e289134ce46f0094eefc4433be4dc8718c77e39cc5e8625c8c8910882057722d4b73dfe645c800a5f17d0a3ba352f67cc5f8f0391b8f9c215faed5e7cfcff4c4927470c1b33e00fab374840edcfd37b1a91ded6e5eb4bc83d3223cf91e7760353d210a8b935100f75fe9e6283025ecfa5f9b7f4cf3e2c2c10a8e543e57e7dbbfeb8873c4db28004053cc72a03d7a8bf5a9aef7b5462778e84f9ebadb2fda1027cc56837bdfcb00f2e5d711bc78fd7e7ef86908b61e2c7523f80f4b6365b0314a627f9d94ee5d00977bbf745e6a5ce360e618d0976e96d12d5996fbf6125f858b9ac1bcd16960003cdf51f6d3b587677642a28a9e7197896d095e194db851bf2e914907139ecb00bee0a557e53bc47f792e75649988525490b4e355ccdc94403f12fe11d3f98f0095bc4461db1e1a8d4430843084308457be1c353ffc2fd4b95c746bfeeb2e4f00fc197f31f89484b4c41c63c8f8599ad4e2929684a6383116fbc0fbc12f4c73002d825f293d8dd976d71d90daba3a99d33730bc40a70dc541202134d3d69eee00a945564fe4ea04b9eb58057f974d44b4ff008a41bf4f37ba98092c957c2ae800723e0589e4f19aa17008a50804102b5a81422876e629e7404027dec160804200b781008bd9ae4313620b8bbdf982d1b9852b63cb9915dbcc536a8aedce82b400c4ef6972f978f8bb8e01b8738429f2ed5a52f6de8697adb6340062378e5af300b7c999ded7b1172c610f0d2be7cb90b52e6a6bced6ad76370dff00972b9bbe00b972b13085c0a1140294b9a9f507722b417bf6a5677b817e5901d5ef62ce5c0074ce1022bbdb7def4db6ad4539d7adad5a080bd189e06fc32e2ef6fa96728900a7a1d81d86c696a9aedf64530def2b1b679f10cdc083cb8bc23cada4106b7b00f535d81eb6df6a0f2c589aa3bc2e72d32cce4d9f5bf58473f027a7d4e2db9e0027ccc223c03a9fa7e9839e27ccc21e01d4fd3f4c1cf13e6610f00ea7e9fa6100bc789f38457ae155007141c379bdb5eb47fa7ff785977b634fb46a57f67b1e00bffc8d8a683ff519fca2899642cf042be4636dff00da8048570d1c375796ba004dbfee04ef1e67fc3d9231fc79b5c1e57a56cafbc6be87fc45ff00b3fef08d004cf4d783ce2af58e4ad665d2ae1c75bb5072dc415a61b31653d33cdd3990450029b157130b3a83943b2d89522de34b114e290480a00900fa52bf6ab6770a9a0069f12c7b08a19e18aa454d7d24a9c90722a94b9826241d0a92018d82a64b4900652d20f02408e8ba9fa13acfa271f0d2cd62d25d49d2c8f8df1fc0c36a0e4a00cc79417304b567172e33e96c0a23db410429d8353ed82082ab1c6661f8d617008b215330bc4e8311421b7d545554f5410f96ff0072b59413a05006252b42ee0095057420c52cf00ea7e9fa6363beae3e83ed15467e7f66e9207b43a6841ffe008f1a943fff0022d3fc74bf6eea2761d009ff0096f0fe1ff335b1895bfe08ff00007a7e4a8e97fb4388afb4a73a9aefa5da4fcbff00e5c3df195d87a88d82a40063ff0028e25c3fe7c44d1ff823fdcaf9c60e7ddf7fa7f5c76f6fab8fa0fb460054541d3bd21d54d5e9b9cbfa51a6f9f352e7894a56e4a7216519f66e9832da008f843afc2486063de619aeef3c8434900952c0048c2aec5b0fc2e577d8957d001504936136b6a64d2a091a2553972c28f2049e510a52521d4a091c49007ac50068ce5c0af19da7d267b31674e15b881cb721866cbd173999e92e77665904ca005254a7a3e353265c3c0b4948254ec5ad940d8a81b63554bb67b2d5d3448a4d00a3c0e7ce5164ca978952198b39321067052cf24827945026ca51613104f00a00116aca69685290b050b4a8a54950295254934525492010a0410410082286f800e4626121c3106e08c88e39c5c88f01ea3ebfa61be780f5fbc237d5f6665bd80026074d0be30bfef66b463c69da01ff00facc4fff00de365cff00feae131aa900c5eaff00fdc963c824468dba6da3faafac93b732de9269b67bd4ecc0cb022a00224d90329cfb374ca1a14a8a3e2e2a0e43011efc2c2f8c1498988436c0558b0080db1ebdafc5b0fc2a489f89d7d161f209dd4cdada9954d2d4a67dd4aa74c4000529afba924b5da368a52521d4a091cc81e01f33ca2ad67ce09b8c1d2f91c400e67d43e1835e72765b8160c547e619f6956758091cba1920153f329abb25100012e68545571afb0906c4d6d8d6d16d86cce233934f43b4182d5542ceea244900c46957396a259912c4edf59b6484931409b294584c413c0283f93c5b1780f5001f5fd31c837cf01ebf78b90f01ea3ebfa61be780f5fbc2369980e0f3884e3300bd845c0be9ef0e791d8cf99b32c6bdeaa6729d4b1fcd194f2a22132eb19cf500fe48e4708ecdf3b914044293329d4ba1fe161a25d8b21f2f2582cb4f388f3b00af6ab04d94ed8f6bebf1dab3474d518261d492a6269ea6a37a79a5c166846e0052c99cb4bcb93315bca484d9b79c80703bd44aaa9a56480529018137dd41d2003155aa3ec64f68be8ce9de71d55d45d088291645c83218eccd9aa70deaae9000cd572d92cb5bf7d1916996ca33d474ce34b4d8f1087818389897366da59b6300b1f0eed5f61b15aea5c3687175ceacad9c8a7a69470fc4e589936616427bc900b46896873fccb5a523531909a992b2121772580dd55c9e6cde662f3fd8419c0073069ce53f69bea0e558c32ecd391782bcd79cb2d4c1286dc540cff2c4b736004ee4f1896dd4b8d2cc2cc60619f08750b6d451e15a549241e27db1d348c42a007b3fa2a946fd355ed65352d44b72cb9153329a54d492188de96a525c5c396800b556013241b833002391678a7dedaac8996753a6bc397b4674b611a3a7dc69006964823f382a0a2044c2c875a325c9a0a479a32f4511e110f30848081624b1008da5b69b899d654cc2fa438f0897579bd9357d461d2b1dd85c449fceeca625003d14bbe37553b09ab9cb9d4d3d39ef214b5aa6a0924a64d4484961ba0554c400a42e4abf54a5102d9a49705f9936d588d32ee3ecb0531c1af085c5d7b487300041b4de718d8297f0bdc30b51a3e1622333f6748a837733e6694aa31872166000ce5d5392499abe110eb9110394b3a4a56f42a96b2ac5ed19476af69f66360e004a8fe510b5ed16d0941df09a2a44ac53d3cd08214833c09d2fe220257534930077541a227ff79325c9fe507bc99afc23205af7b8f106ed1d37f6845f898de300832245c4bae4444c570ada2f111310f2cadd7e21f8ace4ebcf3ab512a5b8e3008b52d6b51254a5124d4e32fb11dd97b235a84864a768f1609000002426900000068000c07089a4ff00095ffb457d2305be0574fa8fd71dc3be381f4fbc654300c0ae9f51fae1be381f4fbc23828106fdb6e5f9126a3d3a8a8c5e42c6ed8713007d5bf607c85ef088a0dfb5faef4b1b529ce9b0b114a524a8df4e190d1ef9f80071bb102d08002c6f7f3b52a37e469dfa731882a566e3fa81a16b70b13cdb3400450d4f2a569eb6b01d295e75aec09a62adf16ea39707d7c38677221126f4df00b9b8e836faee0d2a6b7bb78f11972776b8705b982cce5b942208e40daa4505003635aefdf976a81d1bfc41c8b3f85ac3835cf184410120a89f0a4024d4d02600953524ec05c93b037a902b88df0904a8e4092720c2e5dd8066773a702e21180056e23f53c6a86a64d63e0624bf96e445520cb7e1282cbd0506eafe2a64da9b005290ea26d1c5f8c877890eaa5ea8069c092c0427c13da8ed71dafdabaca9910037bcc2f0f7c370a00a4a174f214aef6a92524858ada8336a25cc3f19a7553a0014ddd848d3544def66120fc29f853d0667c4b91c984504c75cc588610861080061086108cbaf091ab49cfb91119566d10179a322c3c34bdc2ead1efa65978000f732798a501b6bc6b8369b4ca2355ff0877c70f0b1918f97e68907da3d8b6d00a0da1d9e182d6ce0716c025caa6f8d49ef2ab0c1f0514f00251bc69d29147300c83314f2e44e9cb332a446da9277788dc27e24003aa7207c323e04e717694b00d3e6081bf4a56e3ee9d7b981d7c5fa73d3df08ca852f4e7f2a73dedd36f4df0012f67d3cf965f5f1844508a547ca9b73ebca87e476c0117fab8bbb7dc79eb00089237debe9617ded73717b8c4020b337adf97cfd7c10a1fa57d2fd76afd6a300b525c75bb789fb67ca10a5f6ebe94b9f3a529cff003c1c7cbd72f9c23c88d800d791a7c80c62ce2778312c520f0d4c239e2d39e27ccc21839e27ccc2186f1e0027ce10c4efab8fa0fb422bd70adff95070dfff00a7ad1fff00de165dc69b6800d6afecf63d7ff91b14d07fea33f945133fc399fec57fd931f444f680f0c1a1009c4049348b3ef1379864d28d03e18b394ff5bb51a553f4ac49f34c14a7294c0065f2e93ce1d4ac78a4889845351d3596a1a8b88cc4d433796d88388fdecb4800f126c5ed0e31824ec4e8f67e4cd998ced052c9c26866496ef69d532a51326400d9408ff18a12512e61294c82a33cad3dd08d4495a905410095ad3b896cc390005c73b5b867668c0b6acfed32c0652cc09ca3c2df0b5970e9765af04a24934c00fd3c89cbcf4ce5500b54343992e48ca12f8781ca52b30ad342590af4da64f30050ea6cc4cbe05c4aa09bee1c37b045d4c9353b43b433ff0088543cd9d2e8a5002670973163795ded5d52d4ba999bc4f78a12e582a7dd5ac32ce5268890f326001de372c1fcc9ccfbbc644b83cf6977071ed7b90cf386bd74d1990e5bd42986005f8c8e774a350e2a559bf2f66d86621dc879a4d74bf363d2f93cc5198e4d0c00f199a1886809266f92437bf9ac922a3a1e4d1f3685e13b4fb07b51d99ce93800f60f8ace9f4489c840c4a85332967d328a82a5cbc42982e6cb3226a8777bca005cda69aadd97352854d44b559992265390b4a890ff00a92e08e1bc2e2f96a000e5ab46a95ed54e00e63ecfee25637224a9c984d34873ec0bd9cb47330cc57e00fe31fcbc620434d72d4d2290db6d3d3cca1345feee8b23fcf8995c448e711000868cd9081e8cecef6d53b6780a2b266e4bc4e8d629714908002533c277a5cf009692e449aa97f1a744cc4ce9409ee898cf91384d4025b7c59432bf10381f9b0088bc5fd9bcff00943a65ff0067ad4bff00bc39071c5fb75513b132c13ff2dd000683fe62b62dd6ff00823fde9f92a3a67ed0d7fca539d7b697e93ffddbafe7008caec4d446c1d231ff0094312e1ff3f1347fe08ff72be716b7ecbce01a71ed0003e24a5fa6f151b31cbfa599425cace1abb9ae5a868c7cb72cb0f0868192c900d51095c389fe6a9aad894cbd6ea1e101086653a54345a254b857f917681b6a008d8bc0575e944b9f88554c14b86534c7dc995046f2e6cddd63dcd34b066ac000237d5ddca0a499bbc2e4f9c24a1f351b2473e2790f5ca36b9e2d7da2dc16fb001c32d48786bd03d1790665d498191c0c5ab4bb2346cbf2ac0e5c828a864997004fb5633c9964f66f1998e76ca513410f1b093acd73a855b13299c4cba0a65200e994479c766f6236afb51a89d8f6338ace9140b9cb40c46b10ba95cf5a547b00c9386d1f792652644953cb2a42a4d34a5054b96998b97325a7025c999524ad006a212e7e22e493c1209161966c326b34580e8dfed3ec74666f87977103c32c009e0321cc625b86899fe9766d9845cf72f42bab08722a272e66783721333b4d00a1557d8869d65f783695ad86e2dcf042af99e29d802114ca9982e3f357592d002549938853213267a807094cfa758553927252a54e0ec0948750baaa160e8500dc7f9830f319457df6a2fb37b876e38386489e3cf82882cb29d4087ca311a9004ebba7f2e621245ae1932590f10f66482984920130c881d4d90b5071eb6e2500301fbfa3a6f288ec9b98605e99bd091726d4767bb798eec8e3e9d8edab5d4100a2554a6800ae98a5cec22aa6292242e5cd59515e1f38a900a77fb944b9a8ab0090b12c293368913d7297dd4d76763bc6e836038b8b0e405c16cf4a820824100041048208a10458820dc107718f56778797afde3671f406f6416498bd4bf62d0069fe9c4046c3cb63b3fe42e26324c14c631b75d8497c5e6ad4ad59914346c50034cff9ce43c2bd1e87df6daff356d36a4b7fcc463c5fda6d62687b55adaf5a000cc451d660156b968202969a6c3f0c9ca4249b052820804d8137b46a27909a00a51cc05215e49493164baa5ed12e11bd8a794e59c1870ada472bd6bd6aca1200c95b7acd9cd73386caf297f3cbb04dc44d2619f33341cb2713ccd59add75e000f2729c02989564f9744c3e5f6a732e7654b9337cb30fd89da6ed5ea666d56d001627330ac2aaa64c38552896aa8982902b765a28e9d5325c9a6a60031a95ef004daa98954e32a6098269ba99332a7fbc98b2949277433d864c1c0039e66e75008f5bc317ed2e64dcf39ba0324715fa192fd3acb998e60ccad5a8f906791b9800b2e48a1a60b10ca733764e9e40ae6a64cc8595cc669289bcd1f44395d32f3800842dcc55b41d835552532eaf673175d74f908333f2359251227ce5206f014d005525625f7a48644b992a5a496fefc16112ba2500f2d7bc468431f020e7c03000eb14ff00db93ecb4d2d674bdee3cb84ccb997e4f2a866e5338d61ca7901a8400464c9ee529f0856259ab193a57256952a842c3d1500f66a1254b12998caa3500cce0e36dc4424ee3e619fd91768b88ff00101b1db493e74d98a3325617535a00566aa4d4c92a3330daa993542629c25629bbdde9b2e6a0528252a9289734b300d5bc254c2f984951b83fe52f73c06ba0d046a498f4a779cbd7f68d846c2baf0079867d977f67e3d9fd1797e773791453dc4eea8433d13269946cb221e87fe2002e23dc2c3af413cc38e325c6db70b4b51478d085787c49491d2983cb933fb6009db313e4ca9c91b3f879099a844c485773818de016950059c380ec488c3400006ae73807e04e601fe54718c0dc66a26a04c6162206619e738c7c14534a662a000e33334ea2a1625958a2da88877e396cbcd2c594db8852542c41c770a2968a005a92b45152a1692e95224494a9246452a12c1047105e32b753fe54f90fb46600dfd8b7ff0089ef6b2ffd80f52ffeebe77c753f6aaadec4fb370cdff8e9879c00ff00fcc52728c6aace47fed47cc47bff00677ca5ee3e3d9fbc527b3b231c33007d56d235278a1e15a163a2617df333597b8a96e68ca5247235e65b81869c4c00e6eb95be56fb3050efea446cc5ea7ba79c4d8db69e9d8ddb5d9edb84244ac300b130767b689484a8832d63bca7aa9a109256a952e509896056a4d0a101dc02009dfdd4e44e164abe099d343e9e8d678f51ed78cc721d0790f071eccfd3d99c0023f23e1672449336eb17eeb79e721a73af1a88db1369a44c62d6969312a97c0014c6673b952cb497e1a133f44413e4aa1db6d9bbd99ca9b8c4fda9dbdae96b0013b68eae6d2e15de04854ac1a84aa54b4a4024a77d72e5c9981c852a892a4d008978900afbd9e7359504f248c9b873e39c759fda06ff00cb5f4f7feca3a25f00ff00bb386323b16504eca5702fff00a498b7ca962aa4ff00095ffb457d230600b8edddf1c0fa7de32a186f8e07d3ef08f0a812a3f76a537e5e751b6f8c8428006ea4f323c5ce9e196b68471a7a77a7ad2bdf9f6bf218adf4f4f47f0844d2d500b73fbbfdedd460f7d74f7eede459114e54dcdbcf7fcc1afeb5c1f9fbbfd01f002843c3f8027615ad37f9d6a3d79d1bdc789033393d8791cfc38943e9e5e77f00bfef838e474b9d740fc470f4845a5f171ab43216445e5494c404668cf4c44c00bd25a7101f9665e00353898290a69d295c6b6b3278227e1dcac4c646c1c48700e56507a5fb69db4fecf6cf7f06a29bbb8aed0226d38521602e970b1f0d6d43006eac8554051a292e65abfbca89f2a60994ac712ae6ee4bdc07e25db984ea7c007f48ea482e231178f17c6aa184218421842184218422a1696ea1cd74bb3b4900b384a8a9c302e9666503e3525a9a49e2a8dcc25ef04ad2957bd6a8ec329cf100b70f1ec4245fbb5ae1d031c8f64f696b764f1ea1c6e88951a799b9532378a500157453593534b3188044c97f14b2a0a12a7a24cf092a9498b92e6196b4ac68006e388d41f79b18ce865acc528cdf97e5199a431488c94cee0589840be953650041b7d00a997d2dad6198b85702a1a32194b2ec2c5b2f433c10f32a09f7f613008ad163586d162b87cd13e8eba9e5d44858209dd5872898015044e92ade953a00513bd2a72172d63792a11ba4a82d21492e141c1f7eb1ef2829d798a5b63bf400a9b548b114db1b07739b3fbcf36e578aa229eb4a035236b2ba5f90a7e1be0f00e1a867cd9b53eb9c224a76bec3622be469ccd3cbf23214cf6ccbbbdf987cd900fdeb088a56c76b5ae3a5796f63d373b57027867a9f12d773a7c85cc214afa9003bd01e74a9b93d011c8d79621fdf0e9717cbc44239014f9dbcb97ddfe5618f0039642835fe1198d5cf3feb99bc410fa91d0b44e2cef9e03d7ef11bbfea579c0030ef0ea07cbef0dd3a295e6ff68627bce5ebfb446e9ff39f5fbc30ef397afe00d0dd3fe73ebf78af5c2b7fe541c37ffe9eb47fff007859771a7da25becfe3a001b3c1b1319f1a29fca289893b8bf889f8156bf03ce372cfda4fd5acc991b8200bc89a7920982a060b59358a5524cdada3c41c99658ca7249ae6efddfe34a920052d2b32c065a8a781f1071109ee949297091e5fec2b0f9155b55595d3a585a00b0bc2e64da6272975153365d36fb105c890b9e94f02a7171181469de984e5b00a97059ee481f278d1671eb9df1c0fa7de364cbe23df8455bd04d579ee856b600694eb1e5b8d88809ce9ae7fcab9c615e865b885b8dc8e71091b1904e869485003b0932816a265f1d0c4f822a0e25f867029b75493adc6686462f84e2385cf40005caafa2a8a5505004033a529095877654b594ad0acd2b4a542e0452a429690052490ca041f1f08dc7bf69ab204ae7dc226896a6061a72739175d21e45051c000254a44833fe4acc4f4e219a58493e08c99651cb310a21610a1029aa547c05001e61ec16ba648da5c5a80922556610a9cb4719d4557204a51048ba1155509c009c6fe978c0a32a13169059d2f7e292330c74278461f7f66f3fe50e997fd9eb0052ff00ef0641c76676e6a0762a580fff009ee83ff915b17eac2bbab9046f8f0091e423a67ed0d7fca539d7be97e93ffddba7e58cbec51606c25202ff00f9c30012ff00e7f589a40aee431006f2be7d0c66dff669f4de5d937833d66d648e830065b9a67fd679c407c7a1285c4bd9434e327e5e4c0b457e10b425a9fcf3371f0073e328551b748aa853aa3b77af5d5ed4617862144cba2c2a52f70b802aabaa00a7ef96b8bc9934d767cc463d6289989492fba97e01c93d3402e789f1d3af8800bd5dcc9af7af1abdacd9b63e2a633dd49d42cd59ae25d8b8a888b54242cce600f14ecaa4f0ae44b8ebad4ae4329f819249a082fdccbe532f8280864370d0cd00369f4e609414f8360f866154c8089341454f4c9084a5214a97292264d504b00033274ddf9b3567e25cc5ad6a254a24e7a10a42529040090001ec67c79c518c006d37c703e9f78afe3e293e7f402374bfd993d689be6bd05d7dd089e4c62265002dd2fcf122cd395a022d45f8794c8b51e026699b4ae112baa5b817e7f9763e006aa8503dd98d9acc5f20aa25caf963b7bc32548c6306c624cb12d788524ea700a85a46eaa6cea05cb32e6288cd69933e5cb0a37dc96848b2436bab504292b20000de0416d48e3cd8fa46aabc6e69cc1e91f18bc51e9acb219b839364dd7cd500692c8215a00370f9718ceb395e5d6929484a5011237200142404a0d502c91800f446ca620712d98d9faf98a2a9b5583e1d36728bbaa7aa96577e49d5e685df005ce3365a96a9682c0ba125f9b07d78c6ed1ec80ce6ee9bfb1734fb50d8436e003d90f21f12f9d196dd495b4e3b95f52b56678da1c4020a9b5ae0405a4104a400915a9ae3ca5da6530afed52b68cb84d6566034a482c5aa2830d92483a165b800e11ae9e0aaa48362a54b05b9a5239c68319c335cf33de6dcd19e333c73d34c00cb9cb314ef35e6199c4acb911319e66199c54de6d1cfad5552de8b8f8c8888007564d54b7144dce3d8d4b269e8e9a9e929d225d3d2c8954d225a410997264400b4ca948486b042129481c046cc6f000048600017d0586b1d7717f7d3c7d0fd00a25d7c07bf18df67d8c93e57157ec8b8bd19ceaebb3b449e0b5a78768e726400f2e29d88cb739838c8d91305d882e16d893c873b41c8e5a8069050927844300096d2cb4078efb5294367bb4a4e2b4804aef57856388081ba04f94a4a27298300399b3a9173661fe75cd592e498d6540289fbd604eead871d75d59cf1731a1300a92a4a949502952494a9241052a0684106e0822841b838f626fa78fa1fb46c00dd7c07bf18b86ccfc54eb8e71e1cf4eb850cc39ba1e3742b4a335cd73b645c009e9cb796616224f992751199e2a651ee6658494319a26687dfce3985620e6900388c8367e3529661db4c2c2258d253e0183d2e395db47229d48c5f11a69749005955dfd4a933644a4d3a65a053aa61a7965229648de97290b3ba5d47794f4000410b54c091bea0c4b9b800016de6190c845bc6377be9e3e87ed171d5fe5ffa00c22e0b43f8a2d6de1ce4babf97f4873643e5994ebbe9e4d34af54219fcb99600a7cacc391e73091f05319432fe6094ccdf9338fc34ca31b132923b2f99b45d000b6631b5b6da91a6c5b01c231c9b864fc4e9cd44cc1eba5e2387a84f9f27b800ab94a42d134a64cd962680a9693ddce0b965ae92097a1690b29de964ee9de400dcd8f1b67e2e23d5f0f1c466b170abaa526d66d09cdeee49d439141cda5d0300394cb24f3b8654be7900f4b667051f25cc1013492cce162219e2a0c4c25f1200db314d4346b096e321619f6ae63782615b4587cdc2f18a74d5d14e5ca5ae51009b324ab7e4ac4c96b44d90b97365a92a19a1692525482e952812c0989295cb0025258b5c645f301c781ca3ab6a7eac6a06b2ea666cd60d4acc7159a75133be006188cd19973146330acbb1f3788712e7bc441c2310f2f838487436cc340cb6000a161e5f0104c43c0c1c33108c34ca3270fc3e870ba0a6c32824a29e8692420069e9e4214a211292198a94a52d6a53952e62d4a98b5a94b5a94a5124964a42001282120335fed9f38ee9c44f131acdc5667996ea3eb9e6a6337e7094e519160045809a43e5ecb99690ce58cb6a8d549e5e6032c4aa4f2f7570aa984595463b000ab8d88f78044443a10df871304c0b09d9da499438448fcad2cca99d58b966007cf9e4d44fdcef57bf51366ac056e27e00a084b7c290e612c2658294a1401200fa9b9ccdfa4506c6e1c711e622bdeff4abca1838e23cc437bfd2af28e2402400f514a72f5069bef415a5be5912d5f006caf7177b9cc3b7a4540bf11d7388f0008bef7b52fbd795af4bd283ad6c6d56f1cf577fa7f5bde11200bf3af5ec08a5007aee6d71b5b6c1cf1e9a33e6d97bb8e3088a0bd46f415ebca94a5b6fcc1da800de3c722fe27abf1f6e5d0205f9dba03715bf73b0a73ef7a4ef7ccd9cdc1d0000cb8f3faa3d1e66cc528ca397e6f99a7d168829449605e8f8d88594821a6535004b2d25453efa2a25cf043414383ef62629e6619a4a9d752856bb15c528f05c003ab315c427264d1d053cca89f30963ba80e112c1237e74d5eec99329277a7400d5a25a015ad2f4a941095289609049f0fa9c871368c17ea8ea14d75433bceb00384d4ad1f1ef96a5b025656dcae4f0ea5225d2e64788a521864f8e214d8422002239d8a8b280e442f1e01dacda4acdacc76bb1aac2a06a266e52c82a74d251004a2534b4a8d00952d8cc524013672a6ce237e6a89d2cc98662d4b3a9b0e0340003de6e629f638e45b8610861086108610861086108bd8e10b5c7f8367a9d3800ccd18a4e58ccd1a81238a7d654d48f30c4a92d258249ff00225d3c51432f11005661a6221e24a596a2a63123bcfb1adbe181d78d9ac5671184e293c7e466cc005128c3f12984252939ee53572b71130fe8955025cd21089953323329676e28004b5164a8d8e80f0be40f1d0df531958a7707d45b714fa77c7ae778d9db8df300cbaea0e603bbdb38d9fdf8fbd74fe903dbef973f3afa731505bd98273763a7002c83e99d8df2d621edc1c81e65ba713ae87287e5d3a13da9d7fb9a61bccf7700cf8f0195c867cbe80b89772dd5d8e5a7d72ead70222f51bedda806e3adefe40079e1bc18b71e84e7d33b657199caebf3bf461f3b8f104f2ca686a6849e7b540000295e56af7f3c37afcb5bdf88b58386e60e44978805ed77cceac067cef90d006f934318f34ba81ff487eae78f0cbc2d6891af5fa0febe30c5b89861086108006108af5c2b7fe541c37ffe9eb47fff007859771a7da1ff00cc18e7ff00a3e2007fff00c53e2899fe1ccff62bfec98dc67f69774d73066be0d74c33f49a5cec0074bf4b75b25f179aa21a1e2fdd321cdd96e75975898bdcc43af31ae412d510002cf4c582aa26a47997b09ae934fb4f5f4735610bc430a5a69c1ff8c9d4d3e5004f281feaee04e9839215ac6ba89404c5027f526c389041f93c68d98f5946d2002a468e69c4df58756f4c74a2410ef454e75273f652c8f2d6584296e18acd1300d819336e5120f85b63e30bef3aa010cb2db8eb852da14a18389d74ac330daf00c467109954347535730a8b0dda792b9a4732add6005c9200b9114a9412952800e49049f00f1ba1fed30e729648782dd22c821d6db99e71d7e92c6c04083e1500993e4dc8f9c1733896d3cda838b9d48a196374aa3d93d71e5dec229664eda900c4ab189974b834d42d6ce3bdaaaba512d24e85499539438ee18d6d1079aa3a000411e2486f40630d7fb379ff002874cbfecf5a97ff0078320e3b3bb71ffd0a0097ff00eb541ffc8ac8c9adff00047fbd3f25474dfda1bff94a33a7fe8bb49f00feee1c65762dff00a0b4bffea188ff00f3c44d1ff823fdcaf9c6743f66e33c0041e76e03f5474bdf7db44c721eb7e6e817186d7e379bcbb9ef276559a4b23900d41a780bf38466a876d23f914997f881f19581d4bdb9522e976bf0faf00945005e134cb04d819f4955532e6201e528d3a89ff5f48c5ad1bb352ae291e6927e008d1a576af69fcf34a355b52f4c332c2ae0b3069de7dcdd926750cb0a05a99600589f47c9a2c24a9292b694f41ad6cba1212eb4a43a8aa16927d4b86d6c9c47000fa1c429d41522ba8e9aae5286b2ea24a26a5db2202c02342083711b3490a400a543250047421e29de336263726fd97fd2a9a4ab4bf89bd668c86898796e7300ce392b20c95e75952188ff00e0795cda73357e19c5001e443446708585714800f1252f25d6ca82db5a4798fb7dc465ccafc070b4a92a994b4d555934020a9100f9b992e54b0a198de14aa5007463918d7572be2427802a3e25be91aca71fd900ee0b5338e0e2d33c4b1d6e22533de21355d7268a697ef1b8b9240e729bcae400d1a85ec4464ae0a122a8925292e94a54a480a3df1b1d48aa0d94d9ca4980a600649c170e13526c533974b2a64d4ffd198b5279b398cd9437654b073084bf90008dd1bd9199362f517d8a591f4fa03c1f1d9eb4eb89cc9b07ef1610dfc5e67d0045d5a9243f8d6a212847be8e47896a2024549200c7973b49aa4d176a9575ab007dca4adc02a94c1ceed3d0e1b354c352c8368d6d41ddaa52b82a59f24a4c68002b9864336cab3f9e6589f413f2d9e65c9c4ce433a974536b6626026d278d7e005f31828869c4a5c69f858c8779879b5a52b438da92a4850207b0a4ce97512600554495899267ca973a54c49052b973501685a48b10a4a82811620c6d810402002e087078831ea317611bebfb0e2429e1d7d94111ac59b2b01039823f5ab5de002c46a1c87f85caf9599899243c43a971293f0f192ec80f4e21df48287e0661000eeb65485249f2076b33bf8df68a9c329be35c84615842770856f545414ce50000dfcc95d6094a1985a140dc18d5557c751ba336427c4dff00ef46854b5adc005adc71456b714a5ad6a355296b254a513cca89249ea71ebfcb28dac71c210c00210c210c210c210c210c210c2111d3f4f4de95a5c5ab4b5791c64cb532078800b1be64e46d7c9fc3ac1cc7dbc3ea3cb94057eb4b917befdbaf2a0a8ec6e6f000e2e7c6c7ea3eb7d6d0efcaff005f9bb020b33c4fdf3fcaff007b6237f8e9c300c6e1ecc7c33d624f0e395c87f2b86d74f3b3bed5af3ec074adc7a5f9ef815300f5cfc9cfa0f12796700f16cdb3f1d6e08e072e70fbfbe5f7dce1bc2fc9cf5300970d5f8d80e113edfd6d6639ff00568c53f179ae5fc653c569be588d52b2b6005a8d5fefc8a61546e7b98a194a696c85a6efcb648a0b659d988a991888af0b00edc2cb6271e46ed976f8e39882b66b0b9e4e1385cf3f9e9b2d442310c4a51200852417fef29684ef4b9764a26d4f7b380988974b36359553f7d5dda4fc093700caea1d3303e6e7268b27c746461c30843084308430843084308430843084650093850d7d19fa4e8c859b2382b3a486107eed8d8a710977334961d344ac28940097e712a69296e3d241888c830dccbc512ea266eb1ebbec8bb42fed051a767b0018a87c6f0f923f2b3e6a805e29432c303bc58ccada440099eef327c909a9260062d354b46c69e795a7bb51752459ff0099239e654323cbe2cc122f331ddafd00357f7a367fb464927460f661c8d8f0e8413967136fbe5b7df3f2c1c72b67fb00c4bdb913edec1dbc6eee0b880ec06e7bfcc5ec076fc307e7cbf6f5f58926ee0035176b1eb7279b902d7bbb911b7afaff006f3f3ef87bf7efe511bc73cfe96600e2e19d9c67c63ca90083500de9d760316262805005f2fa9e315a723d6d776b000b3c72a0e83e4316f7c703e9f78aa141d07c861be381f4fbc2141d07c861be00381f4fbc2141d07c861be9e708af3c2b81fed3dc38d87fe3e748390ffef0b200ee351b42a4ff0000c72fff0023e27a1ffd4a7f28a267f8733fd8affb263e88007c79eb9f0e392a0f4b3873e2aa5908bd20e3123b39e91cd3324d23198392650089c424a65731cbef4e221d6caa56d47cd22d86659995b75b19627ecca26b100059826e263e07c57b2384e3754ac431bd9e98a189ecd22971197225a4aa6d4400a54c9889c25807fbc289692664820fe62499b2c3a8a50bd3ca4acef2e5bef400b6530cc87bb71e6350e391d69358bf666b89096e758f1a09ac3a4b9cf4d6320025c7a431ba8534cc395738cbe5ee2caa1e1e770b24ca73e924c5f658521266007298c61a8e5256f0954bd2a4318ef6c33b77c0d7488fe31866234b5e94813900144893514cb58b29529536a24cd960973ddcc428a3f4f78bce33935a86f8d200a0a6bb310fe25c7ac6507d9b9ec67d39f6784d63b8a8e28b53f24667d4bc9b00229bbf2d983517fb9f48f4965ef43944d3357efdcd30d268c99e6044ac45410022733385944b64f091b1e21a02263570b3386e01b73da7d6edacb46cfe018700d553d0554e942620a7bdc4b125a540cba7ee69d5312892666eafba42a6cc9a00a4a379694ef4b563cfa954ef81092124f5528f0b69cb33e91ae87b637da110001c7bf11d0711905e8a3a13a390132ca7a54b8a878c807f323b33888489cd9900f62e5b18b0f413999a365f010b2c61e87838a6f2dc924663e0a1266e47b29e00ececc7638ec7e06b4d6848c5f135cba9c4424a562409695269a8d331037562004256b52d4952d267cd9bb8b54b08319b4d24ca417fd4a20ab93643c2fe24c500c17ecdf240f686ccc81ffd1eb52b99ff00ed0e41c69fb70503b172d8ff00cb00543c7fe66b228adff047fbd3f25474cfda18483ed27ce848ff00f75fa51d7f00fb398caec5d406c352b9ff0094311e3ff3f1347fe08ff72be71473d90bed07006b804e23d536ceab8f7f42b54e061f2a6ad424be1e22611b276a19c7e232de007795cb5979b3151b96a64fad31ec36d444544e5c984ed881877e62605b3b2e00d2b6386d8e0625d26e2717c3d66a70e52c84226950099f49326149dd4cf96100d0494a533d1294b5046f98aaa24f7a8b7eb4dd3cf88f119738d8a7da21ec7a00d2ef693454bf8c0e11756f24cab50350a4d258a9acc62a611136d27d5480800097225f2e9f89be5e849bccf2ee6a8796c2c049e35c87808d818b4cb596267200b96cdd99847c574a6c4f699886c3257b35b4986d54da2a39b3532d0940978800e1eb5cc2b5c9eee72e5cb9f4ea5a973520ad0b495932e64c945084e1c9a954009feee6025209ff0072797021eff22cd18c5d29fd99be2ae719c6570fac7ab100a3592b20a22585cfa679326d99b38e6b760439ff000887914a23f2a65f951800d5b60a5b8899cda1e19852d2e9662fc061d7cfb11eddf67a552cc385e1b89d005d61491291552e452d305b58ce988a89d3371f312e59516674e717d55a803e0014a896b3b00fceeed1949e3db8c3e1dfd921c1ac1f04bc2ecdd8735c1dc9710079572848e12351389e64385cdaa8d8accfab9a8933877186e5f9a664e4c667003bcbd2f712dc6464fe632b8c8491b3946095ee3afb63f66b1aed1f6a17b55b00412c8c245526a2aa6ad3dd4aac553ee269f0da296a077e9d0112e54f587426004a2625534d4aef6254b5d44def660f85dc9c81dd60129e5903c9eef1a34ba5006fb8e3cf38e3af3cb5baebae2cadc75c71456b71c5aaaa5ad6a254b528952900449249271eb30520000a400000030000b0000b0006423691bff7b21f5091a4007ec5ad3ed547258a9d37a6b917893cf8b93a22441ae6a8ca3a9baad3f54b930016a69f4c2aa3532f30c988532ea592e070b6b09f09f1c769346712ed4ab70f001304a35d57815189a53bc25fe66830e922614b8de09dfde21c3b3388d4540d00ea95272de5212fc1d290f18eee28bd93ba59ed4d974af8f2f67e6a7648cb13001d67874cf752b4e33988c9548139efdd21bccab5c465e94cda3f29e7b4cc9200f23384a661298a964f66fef735404d7e1e7023265cd767fb45c43b3f5ccd9000db2a0aba897859ee682b6977664efc9bbc8613a6cb454d1996c69a6226266400996d4eb96f2b71179150a91fdd4d493bb64919eee99e8d96a059ad149f86bf00d99bd5a88cf92999f157ab390653a6d2e8a662e6996b492673d9ee6fccadb000ea1c54a44de7b95a4729cbb0518949662268ca6711cd34a5a61a05a754889600b658ef6ef870a49b2f67b0dac995cb494cba8c491264d348243099dd49a89b0032729198967ba412dbcb21d26b5d6a5bfbb49278aac07802e78fa45d07b70300da11a53a05c3f39ecf2e1a63a428cd53ccb727c8ba8109949f43b2bd21d279006c343349c901f837d70ec66acd301090b278c944418889976528b99c4cc5b80078f9b4a2271c7fb28d8cc4318c646da63c99bf97933e65651aaa434cc4b119008a51fcdeeac051a7a75a95353352c95d4265a50548973045ba594a5afbe5bb00024827f995c7a0cfab691a63f80753f4fd31ea171c47988d943c03a9fa7e98009843c03a9fa7e984223ddf7fa7f5c210f77dfe9fd70843ddf7fa7f5c210f7700dfe9fd708447bb3c88fc3f5c210f01ea3ebfa6108e2450907f5171f4f3eb8b00c9fd23c7e675f0f7adb3fa8bf973200d03b10747b8d2f11cb96fd2fcfeefd80074c54f97a7bf61fac40259ede81b86618e4f6bdb9087dfdfe3eb87bf9fbe50007d497b5afa9b1cec2c7570e2c5ad0a75fcba7dd7a7d3076e01eccecfcb8c0700a664dcdcea78db8b6ae1ec6ccf8b0d7e19064ee641ca71c139d27d0844ca360015c41772cc96213e152fc42a589c4d5a2a6e0520088838353b330a8675c95b00af74976bbda18d9fa356cf60f3db1bc424ff00c2a7ca524ab0ba19a189de0e00515b5682532006992249555032d6aa55af1aa27940294d96afd4da0e2c6e090006de76b3e26f1e448d6c3084308430843084308430843084308430847b393400e66997a6b2f9dc9639f96cd65714d464047432fc0f43c432a0a42d26e140dd002e36b0a6dd6d4b69d42db5a927268ab2ab0eaba7aea29f329aae926a27d3cf0094add992a6cb2149524f50c52414a924a540a490641292140b105c1f7ecc66006f87dd74956b365cf0bfeea5d9d248cb4d6629478d148901284267b2b4788200dc95c6b87c2e36521d96c695413fef19540c6c77b57b3bdbea5db4c33766ee0048c6e8112d388d192969a182457d225dcd2ce5165a582e967932564cb34f36007ed644d4cd4924eeac7ea193bb7c432ccf500d88b826e100dab6adb9d4f2e7005eb71614db1d8815f373721fcb5b67e7997c829f930b02de7a5f2f2c830a7e00ede95e95af324fa530deceda8c9ac06432f674cde3745afa1177724e6731fd0035c88509dbce94a7adedd0d36bf6c48511e9e3c45c3ebe176cda0520faf41c00ec5acdf27c8c284037a76a907702b41f2bf3bf2c412f987b8cf807b7a8bdae001f3bc480d67e5e2c0b9f2362f639b5a06a2b73cab7dcd0579ee2db57d05b0000435c077cdae1c6795dbabb5b281049b13d01b120e4eee3870d6268aea7bdcd0007a8e845fd46d5021c6a01bdf3cb964df3e8331ead6b0b67ea4f3d35cf27f30075279115bf3b5f636fee0e0e34039580f97c8be99c00b392471b9fae5d6de100a4ff00313b9162373722dcfa9a6c3eb5c1c701993971e2cc3de9a99f537032003eb7723de672f65239dcdf2dcea4f98a41328b944f64333809d4966b02fb9000d1d2c9b4ae2da8e96cc60e21b216c45c14630cc4c33cd90a6de690b490a0310044e972aa254d933a5cb992a72172a6cb5a42913254c4291310a490414ad0a2009503985105f430662e4104106fd416f222e3a5deb7eb47161c4b7119032295006bbeb86a46ac4bb2c4646cc32fc1679ccd309f4349e3a3d8661a3a265edc6b00ab4433f14c30cb4f38df84ad0da013618d4e17b3b80e08b9d3308c26870e5d00425089eaa4a744954d420952428a521d2144900e4496ccc52896896fba9097001a0eac0806ed76e37e022e3346bdabbed09d00c9b2fd3ed31e267384b7274a001a6d893c933149b25e7e664f08c32861897ca62f3fe58ccd31964b219b425b00869540c643cbe1c0019856eb53a3c53b3bd8bc66a975b5f80d2aeaa69266cd009132aa8ccd51254573114751225ae62893bd31482b3aa8c50aa792b249482400ea1441e760c0e599726ee758a47c44f1d1c5c716298787e2075db3b6a1ca61001f6a2a172dc4bd2f90650878b610a6598c6b2765581916574c736db8e2131a0065062ffcc70a9e25c714ad9609b25b37b39bcac1707a4a298b0a4aa780b9d500250a20941a9a85cda8dd700ee8981361f0b000548932e59f812125b477b6770024923882f169801ea4d47536a1bef4aef4a56831c8dc3d801ea0ebaf1f0f9b00dcbb5c9f0171e5fbdefd2a9690eb6eaee8166c5e79d15d45cdba619c5c954600489799726ce22a4938549a60ec2bf1b2d546422db78c1c53f0508e3ec57c0e002e19951ba134c0c4b0ac3318a61478ad0d36214a262670915525136509a80a0009584a9c6f2029612a6b0510cc6295212b0ca008705897167637d78b11d63c001aafac5aa9ae99be273feb1e7dcd1a939da3206065d159a337cda2a733a88800196b261e5f08e4745b8b756c4131fe530d289f768fe548a1270c3b0cc3b08a006145865153d0d2254a5a69e9a52654a0a59256add48037964ba8d9cdf83128004a46ea5922e585ae58e5f4bfde9a9f103726a7957d6dcba814dcda9d33c11600b0b3e9a70e3cbfa022a6e674163ebc381b70e6445d170efc6af157c27c445300dc3deb8676d3786982d4f47c925f130737cad1cfabc09545c664fcc90539ca00d131a52db6811aeca57189425284bc114038fe37b2db3bb46948c6b08a4ae5002032272d2a9550919eea6a69d72aa12977f844d092e7e178b6b952d7fad21500cce633fe60c5bc7319e717419bbdb41ed37ced218fcb938e2af34c24b6630e00a868b732c651d34c9b36532ea1487043e61ca392a4b3e8052d0a20bb0133850072f658a0c6829bb2ed82a49c89f2b6769d4b96ade48a8a9afaa96e2e1e4d4d004cd92bbe4172d432719bdb4d34917dc72c3376cf504f9f8b124db19f379c4e003304d2613c9fcd2673b9d4d629f8e9a4e26f1b153199cc6362145c7e323e3a0031c7e2a2e25e70a96ebefbae3ae28952d64d4e39e4a972a44b449932e5ca95002d2944b972a5a65cb9684860842121294a468121accc445f6019ac3200161700c9b4e807170c447adfe6a7335ee7b77a9e409d86d7b62e38e02dcae490cf960099df90d20732e4dd8f0003f519e5c733ac5cce53e3378afc8ba56e68764ee2000b5572d6903b2acc322734e24f9b66705945729cd8fcc62732cb152769e4c2009849dc44da66ecc9928f0452e3e294e03ef555d0546cbece55e22316aac17000e9f89f792668ae9b4d2d55226d389699133bc29dede9425cb12c8ba7712d9500e832a5956f14a4aac5d8396cb32ee2d7fab37e7e1cf8bee25f84b9e4c67fc300b6b0e6bd318c9c21b6a73092972026597e76964292c2e7395b3040ce32ccd900f860a58848a9849e22220fde2fe19d64a8d6ac7366f01da4948938de174d5e009944994a98152e74a273eeaa24aa5544b4961bc944d48537c40c4ae52163e3001bc06a5dc0b3b10411e0d9c5cc6a47b607da4baad96a2f28e6ee2a73ab523900836b66399ca125c8da791f14c2d1e07597a799072b65a9e961c4d52e30999200587013e342aa29a1a0ecd761b0ea84d4d36cf5219a82e835336aeb50950b820024d654544a70598ee38d08bc5b14f2525d280f6673bcc32b05122fcddf998c006e3ef4444bcec444bcec444443ab79e7de756f3cfbce28adc75e75c5296e3800e28a96b5ad4a52d6a2a59e78e720252025212948000484801206400480036900906b5b28bd901a35f3d00d5dc81c4687ce3c37aefd48a93b0f5e74d8d4daf80093bb6602c380e0dd5f2bb8cac2001bb939f1f968dc98e79922cbdae7ff00c500bd6a076dc1eb620de9838bb81cb911edf5d21d0e46ff006e1c32cafac2f7b900f5371f8d6d7341eb4c492086648b3337ed6cbd7c400639a8df8e9a5bde47a1001f15ea4f5a8ad37a7e7c81f960e380f9b7bb33bb74b035f33a59c07f777667006e21c8f8b7a9df604fc8579dfcc52f7a8c43f21e5f4cb9e46fca258712d99b00e9c5f3bb366033c056d737a5af735d85c723527caa76c1c70e8d66d7831e6e002f11e2c757e76e367d18c3f9b7bf21407734ed6a58d69b6d6df12e3403adce00ba3e5eaf0639925b85869776fd9a1423626f6bd6e48f41e5426e060fc8790d003ef77cb4be906d1ceba9f76b367adb58915bee452c4934e46e7e9514069d2a00443f21e4380e56f0e312d7b9e973c7adf303db47120d2a7d0935a8e95dc9bf003e9cc6d21447937be7cc7cef10520f9f2e9ec1e995a0072bf4f5e86a3af5eb00bdaa5bc7d7db9ccfa6678d9ba35d41f60741617c870ba95e437239df7e42ff0000414aed4a00dec9beed7b66341ec5e000bbf2bb33bf4b104e9e05ed16f7c4000ebacab46b2dd18f7530ce93b65e6b2ec9fc6d910c3c2b42a7b3441256dcaa000dd494b6da51ef265181304c78194c7c64075e7687b7f4bb178634a28a8c7200b92b4e1d460a7fba1baa49afab4e62964aac84b15554e024a375099f3a463d0044d4c90000ea20b07cb2f88f06e1a93c1db0c9399ccd330cd6613c9d46bf32009b4d229d8d8f8e895f8de888879454b5a8d02529164b6da12969a6d2869a42001b42103c535b595588d5d45756cf995357573573ea27cd3bcb9b36612a528900c85ec948012948094809000d512544a944924b9273263d6631a221842184210084218421842184218421842184218423b3e4dce39832166396e6acb11ca80900bcade2e30ef8438cbcdad250fc245b2a211110914ca94cc432aa7890a250a4003894388d9e0d8c62180e254d8ae193cd3d652af7e5ac5d0b491bb324cd438100324cd41289883fa924b10a0142a42d4850524b11e446a08d41d44669344b5b00b2e6b2e5c4c740ad997e6597b2d2731e5c53b58897c42bf93e2a17de785515002a8a5a7c7091690bf0788434496e290b463da9b11b7586ed9e1a27c928a6c400e9909188e1a56eb92b3f0f7b25d9536926aaf2a680771fba9ad3017db499c8009a977650037d2487e16d58922f96873315acdea29f2daf53ce80f7efb1ad0800e6c56d970b75f7d0daf988bdcc5c9cad7e998c99dad95ee5e22a00a81f2af300e42dd4916b0bf3342df1c0bb65cf26f7a443d9ef96432726dd4e848e1a189300734fe9b56c763cf97516a6edf1edf3e76f50fd22722ce4f53a8fe9a026ef6b0013c6a4dfa8de9d81a54d398a8a1b5c9ea1be6f959b9f5d46b910f9819988050059e8da8e0fa9201e441366731c81ad37adf98e55a8e5f80e57c37c7b23ead600f65acf39e7abdb40067991c33b00783df89a545a973caa48a1277db7bd7a8a00ec406fb90cc06af9fd069c4e635b440cc5f5662c340720da019bd88e6227bd002f5b56a2b5e953befceb4005284524283b38d5fc3dbbbe9ced57a677cb276600cdf53af16b888afc811616e74adee39002b623a0c46f8e42fcf2d4e5e422970077c8316be976762183bb5db573685ea6d515068773cea075a7423b8c0afa7a00dee7838be8fd5988839bfcf2367b0670ed9025dee4472ad6d4279134a03bd700f4bd2fd30df039f307af5e4c39dcc351676e197f33b5ada59f360f6788ea2c00391e62c3957a122b502953d2b86f5aecfc3a8395efc7a16ce26fa9cb99e17d0043e85cb33e713e437af2a11dc9bf4bd45ea3722edf1c3e5efaea3866d1cfe2007d74b5ed636cf99e1773100ed6a0f95296afca9d4795313bd9ea0703cc5fa500f9b9e02e472218b5dee5f30e6fa798b9b8ccbad477bd81b9e645398e86a2fd00a37f3cb967f51e197a5e0090fe0ce4e6dc481e5c41ca20122b402e3b1b8f2a008a5391a1a9b1e781531395873bf4cf46b7ab07817ea19d9f3bbf3e22c72d0b003bf226c3ad6d50454fcbbf6af502b86f8f6fe3a7964efa449239e6c05c5fcb003bea730ee0b988adc8a5b6a58927aff356be7c8106dbe05634cafd79741ebc00a235c9f4ff005785c00352d762e43178917a5b9541bfcba91437a9bf4de93b00c395c67e246ad96b971601da43f98771939e03d6e6e49e6c24db9f957e94af00a5ef88df16f7ef8bf86792cfd1ee72c8ea2d91cce9cc988a826e2f704537b700314da95e95a6dc837c7be6dcb4bbf9805d843e9c1ac017b31c9868e34762c000bb05881b9b5397956bb57a5ef5ef86f81d756d3c723edda1d1f3059ec2ed6200ede0e731610a9a6d4a1e96a7e1bdea0f2dc5461bf723cb57f5cdefcc73222600ee6c73b3df8e570daf266d4dc08a117ed6d8f6e86bc89e94dc0c4ef0bf200d00bd7906ea786707eb66cb8f32f9f227cc90d069c81dbbd8f2a8deb7b1ea6b7e0051be2fd09bf1d07bf0ce04bbe796a1af664b665defc74e53bdb702d71415bd003b6f6b7234bee1be037be3c1fee39c38791bb5cbb8773a8195c599f288e66900f31df6b9adcf323d7951bfe1cbabb73e1702cfabd8ef61901935aeeda65964002dcf454dad735a56d4a529cbe76e9422966fdb4be59b74b0d333969d622f6e0024585f3be658aacef986d09b989161b57b0045e97d850d48e829b1a6d86fd900f3b5c7a71e2de05ef0bb5ef666f3ea736cd8d9cf080b72163e4456c2bf85f700b134a61be1cf0f5d34f3eb9676339bddbc4b876cc121b881a8c9b5907734a10015e5bf3a0f3f954d6f5c37c78bf51e76f32d9f0866e2f62ded8877cf402d66001104df615e476af91b6f5a58dbbe277858063c5b97a71ccdb9ead4339d7337006e0e6fd1f997b42b716ed5e87a6dcab4e9f5c46fe5cf997f9372cfca20588e000e0666d663906cfd5f9b2a3a0ff55f9ef5ed5aef4a81bd0546f254388f3cbe00bd0b6b9715b8e4c2ce48e5cdc8b3863ce26d435a501e961e56a57aef7f96010063df8fdbd471b4dae4f3bdf5e6c32c9816079b4513d6ed6dcb9a3397571d1c00b6a619963d97139772e25da444c22127c1f15141155c2ca6197fcd1716a08f001f87e1a1bde452d081c236e36e70ed8cc38ce9c515189d421630ec3829973e00603bbdecdddf8a5d24a379b34eeef37752b7a6a8016674e1240b3a8e497e190065fca2f9f11d230b99c738660cfb98e659ab33c72a3e6f347bdebee91e0659006d0021884846412987838568259876135086d20a94b714b715e2bc6718c43100ec4aab15c4e79a8acab5efcc59b21091697264a1c89726520044a969b252030092492752b5a96a2a51751ccfbc808eb18d64530c210c210c210c210c210c21000c210c210c210c210c210c211d9b27e71cc590e7f0399b2b4c9e95cde0164b004fb7e15b6f34ba07a122d85853315071091e07e1de429b58095001c42169d900e118c6238157c8c4f0aa95d2d6539251310c52b41b2e54d429d1364cc1699200d60a5418b38045495290a0a49623d791e223319a11c4165bd6694fc390d493003a4b5a6ff7c65d71e053109084f8a6b2352d65d8c95b8e7892b6d44c64add400fb88c4a98720e3a3fd87b0bda0e1bb654a65908a1c6a990935987a961a6009001bd5542544aa752a94e1482f3a995f04e0a42a4cf9fb4933d334313baa4b3a006f703322ee78ff00a5b8677083603a123d0771bec06fccd46e31d82e3d8317009ecc1ec736d1d869736666e2f942837a0de97dcf2e7f3ea460481ae5f67f94001c0cdf81243a9eed77b1d416c8676b474a1a0a92697defbd2805ec29717df00071c47d79db96bc2eecd10f7605817739f5b16603a0b718903a577ad2f602c0009a0b036ad6bcc004db12e3dfbf2e3a41f267e05f4c80c803a0e5d4e537e42900cfebb7af3e553504ef8871e8fae5efdda015d5f33ceec6dab8cf810fc6229600e82bdaa0d48b72e9dee6f5c1c65e391d3e7e1025ad7d6d60d767163e9ab98500c6f7a8da83eb4b5b9defd88b9c71e1ebefc35890a3912e7266cdf4d059ef7d001ad995c73eb7a5fa9a5f9d3c8536e427dfbf380240d1dc8d1f9b7cf2cd800500ec3cab5e7d2bb763cc03def622d8871c747f0e30de2f77c9c59acce4e7c011006b873d214b1a83bf4dc56a2dd89fa5fa60e38f2f7c3eba41c31704b1d75b920034b1b9397a5a1b0b1bf3a5aa79edcfcae29e8649033f6f07f85deee4d983dd00ae39df8e44dd9a14bd7bd6fbd3bfc8d2d5b27a61072ef737b6791274d1ce4f00caf63134f43da97a114e54adbb01718805ff00a1e9ec433bb281f9b1d4b66f00968320342f9fd8db7dfb9f2ad4e25fdfbd796712f662e1878b68e5f32cf71c00b5bc0151b52961723627903d79defe98871efc7ec6201b331b1b0b8d726e360037777177d54fe9414e543cc740473a0b57070cfa7bf6786b94403a876e9959008e474cd83b002e2f0a5791bf7a5bf1bdaa3b7615971f3f4fea22732d7c8be7007cbfd2f77b861a5ac1c052bbdcf527c8f4e5ceb6b1ad69887fbdf85bcb3d6000385c3be649666e63263cc30d338a509a54fcb9f2deb7b54907adab63ddbe8700e794058960acf864fa1278daf7b73369a6e282d7dac05c0edd2a2dbe25fefc00fca0ece18f1b87005c3b02337bb599c8b4475009f3009a5fea4d4df7a50f7c001c7bf7973ca1bda827d6c3986624b9cf8664dccd77dfbf5a9bd001cf6dbb9a00d7738f47f088de624107376b659f85bd6ee359fc8f53b7cb96d7e7d77c438e003efd883f1d5435c81e79d9b9640dc4401ce845f9d054927a57ad01f96e6a70003dfbe0626c1c870747619922e4bdc5f32ecfa3c2e29527a6d5d85cdaa6f5b50048e7bed838bf2e36f9fce0e4664db8f467fd2ec5d81f37ca005072a58d0816001f5b8ea286bb923071fd6c786479c0383906170f6d5b987e04df9b5a0763ce0097ad472b9a5c114a1b75b0b581c71f76fb888dec9dfcc68c4bbfa7036be41400bdb607ea77fc7b815e7b24e3d1f9fb1af0d625dfc1df8dec4b3373d59ce790007507ad476e9cb7aedd76bdf0716e6edef873e9c6014e48e26d91673a30cee600ef6e7ac117e7b52bbdae4006d7e7526c40def89ce19dd8e59b1e2481d72bde00e350f134f4bd8dad7db9796c48a917be21c7cbd728383fe6e46dc5800030e300d2f9885fbee4f3ebe66b4de9515e95da5c7bf0f4be7941f8bbbb8cd8e42da9000336b45bd6bbf10796f46655f0e90d4ef3a4c5a5993e5e6de4d21c16d5e09b004f5485a5d8295b6bf006da47863266e92c41252c22363a07af76efb41c376300697bb0115d8d54215f93c392b0d2dd3f0d55794a82a4d2a491ba84b4eaa57f00772775099d5122c4e9e9942d751b80f776cc9d33161c1859c8c3a670ce398f003e6608eccf9a664f4d27130582ebee7850db4d207859858461012cc2c243a300f9198765096d02aa214e2d6b578f317c6311c76be7e278a54aeaab2a0bae6200d825091fa254a425912a4cb1644b40094872c54493ac5ad4b51528b93eec340011d671ac8a618421842184218421842184218421842184218421842184218400218423d949e7335cbd34829d48e61172a9b4b9f444c0c7c13cb622619e456800a6dc41068a49521c42aadbada96d3a95b6b524e4d1d65561f5326b68aa26d200d553ac4c915121665cd96b1aa54920dc38502e952494a814920c824104120800c88ce32a9c3ff16528cfc207296a03b0721ce8a0886829a1288692e677128a0023c2a57859954e5f2920c0ad49838d88294cb16dbd10ccadaf51ec176ad49800ef718563ea93438c9099526a8b4ba2c49412c0ba9914b593082f24912674c20005314ae6229519f26a02d92b60bbdec12ae9c147871fd37200bd2a0e9faf4a00d77f5fae3b95cf1e5f58c8883f9eff009efd0d3cef4c47bebc8c21f3f9dffa0053faee712e73c9b80f7f6e1683c08af5efb73e42febd6c696c4421dba1e77a00faef4e95b8f2b625f90d74e3f6d384224dfcabe963cb634db7afc8e0e7cbdb0074e50853f53f8fca87bf5df11efdfce110053bf9f952fd683eb71cb12e7d8100c5fdf2b65089a50f6ed4a6c47ade80f2b5ab4a13fbf5f7d4c21d79d3fa914b005fa7e270249ce103cfca9d77e7f5bf95c0be2016843e9b0e57fbad3eea4fef00dfbcb843df9445b60294fb35b81d76daa2a0ef8973e7fb7d842269bfdfcb950000f4b1e783fb61ebc7c61022e091b7f4afcc74e46c6f839e3e1f2b70e10882003cf976a7ea05e80f3f960e7d1bfa7083bfca24f9935a7d074ed715fd2c7cac003efd61efdfbd614eff007b5ba8af3bf3dad839f7c3874f7c225f4f7d3a6bd700c2228395c5eb7adcefbdfad796f5dc60e5ddf9c444d39df6b01cfaf31d37e500eb63e7ed9b84223bdfcbefefa6e6b1efdfbf9064295db9fdd7d29ced4dec31002e7dfbf66f9c21f3fa9fc7cb9dafd49c1fa790f7e50f7efdfccc3a577ed5e6004fddab41b9dce2210a7dfcea6db1a13ebe789727fa9f2cf4cc7abc214dfcff00002e54b8fbeb8391ef3ebc7c610236dae7d458df6a1a6c2b7e7df073e8de1e003efc211201ebb75f5e74df98bdc01cea307e9e4df284294e9f7f7be0e7dfbf0066f9c214a7dd37fbf5c1cfbf7cbc3466112e7df3e5165baffc5949f2108eca005a7ee424fb3a00e42c6cd014c4c972c3aa410b2a527c4d4d672c12026050a50041c14402264e38ec33d2b7ba6b6f7b56a4c07bec2b0054aaec64054a9d541a00651e18a6bdc3a2aab25936909264c99808a952972d74abc69b5011f0a18ab80082e127c332386873b868c55ce27335cc3348d9d4f26117359b4c5f5c4c747c006bcb7e2625e5d2aa71c592689484a1b4268db4da50d34943684a47972b2b2a00b10aa9f5b5b5136aaaaa662a6cf9f396573262d599528f00c1290c94a404a40004800601249249726e498f5b8c68886108610861086108610861086108610800610861086108610861086108610861086108bdcd0ae3027992fe0b2c6a418d00ccd9590530f0b3cf12a2731489a242501e5b8af1cea58c7ff52eac4ca1992a0010b1114d310d2d3dd1b11dadd760c24e19b42676258602112ab9ccdc42850500800a2a53d6d34bcc2164544a49225cc9a844aa68c9955053f0add4343a8f3c00c78b8cb2b465072d667cbb9ca510d3fcaf37819e4a231095311b00f879bf12009295a997d167a1629af104c4c1c52598a8472ad4434db89281e96c3715a0c500e8e5576195726b69668744e90b4a9209485196b0fbd2a6a0280992a621135000af826212a04467a4a5402828149bb8d3a8cdf88cc6ad1efc27615dc58d3e7c00b6a1e66b5db19dbff3f31cd88bf836913ba7973b8b676cf3b4296ad2e01e5600bfadef51b107a52986f5f3b1e62d7e9c083a1b66ee20dafbfb783bc4536a8d00ad6ad4f437df9d0588e9414c0af85be973fb1d7858de2774f2cbe8e7fa9b65007b8853980453bd41ada95037bd0509a50ec6f86f68ef6cf51c35bf307d440a00798e7c78bb35837eed9052805cdc8af90dedb9a6f71d30dfb9d1816d5ce9ef00d728863efef97ae861414a0dedc8dfbec696b80296e67600b39966ea1f96b900f1e5a0d64a7e5c72219eecdf47b3daf2520d0806e6fd69d7fb6f5c40590eed00cbed6f639e510d722d6e607ce14dad5bdeb5206c2805796c6fb035b118159f00a59812399bdfa6a5f944b58dc7af51c00717e3a00ee238d3a0b81d09aef53700db6b0eb6b1ad2adfea5c9e01bc8fab886e9e593df51ab74d7d1dc4494db73e005437eb4eb5a0239589b728dff2bb78e4e05ecde475b3c3788e5e77e1f7b0780052f60481b023f1a815f2153414ef890ab5f33cc70b3393cb360e6fac1aff00005d3df0d4dad7852e7715ea0fca94bdfced526a7680b367f1e7f6faf1113bb600370481a37b667cdaeccf0a79d2b5162773f51400fafa16ff002d03dfe5c0df0099b75614f31a8e197c9f47d5f4bc4848e636b6d5e5ce95ea08bf5dac00af2600f1e5cbd8f3ce219f2be4da727be61ede5d223c3fa1dc771ca96b137a77e65b00f9fa7bb67d1c7a035d87cc7c813f530a6dcfadabd48ff77a1ad2e7b002b86f00bbf1c86839ebebd3276896cf467cf3b5b4e24b6bade1422a057e5bd0f317a800a1e56ea2e2b215cf5bbb790cb5e2f9bbd8810411e4fcb5d7c3ecf1341d39d600970799a6c7bedbed7a54d3be7e9c87303efc39da7779e9cac7abb373d1c38b00877840dfcbad2be5cef514045697eb3be7e7d3936bc35e36c9a1ba5f2bf99e0040736e99b295ee6b5da848e7cc75dac6c39528dff07e6ede9f7e8e0883797d00f2e367d723c9e2295b9e77e9734a015df7dac0ff00ceb59be058396d4ddfd700d6fd225897c86adc3c9d9b86632cc87809a8f5a0239fa1a1efe57a73c37d8f001cecfd2ee2de1a17836796bc465d6fc813d33b44d0826e763537b9f3a0e7e700714c37fc2efa64f9666f9e57b657b400f967935b3fb7c9f56b827cea37b54700c88bf5fc3a86fbe4de367f5b7be4099b36bf31caf67e797d2ef0820d01b76f00a6f7e7dee2f4186f9e5c3f7c9ef6d34b0d21ba6d95c3f9f336e3ae91201e9500f9d2d4b9a9fa52f4e430dff659f5cbd2f76d5ed13bbc48167d4db9366de5ce00c5bafe65ccf9772749e2a7d9a67101239441a14a7a36611086105696d6ea6100d86d443b1718f25b5261a0a15b7a2e25ca350ecbce14a4e0e258b61f84524c00aec4aae45152c904ae6cf5a5092c14a08961cae64e504908932d2b9d354022005a14a2c68514a412a500071b780cdcf0e8f68c5feba71813dce898ccb1a6e600372c6575a96c45cf3c6a87cc53d6527c252cada505c925af52ecb2e2a6314c00f85315110cd3b132e3e69db7ed6ebb19efb0dd9e33f0dc2c9289b58fdde21500c804864a905e8e9976f810a351310009b325a57329ce0cda82a74a1d2352ec00a3cad90e3772380244591e3a5e31a18421842184218421842184218421842100842184218421842184218421842184218421842184218422a069eea8677d2e009ba26f93a77112f515a151b2e714a8893cd5b4d8b333962d5f0f149282a42100ea222e18294b8389867bc2ea77d806d2e35b35542ab08ad994e4a819d4ea260065255245b72a69947bb9819c256c99b2dcaa4cc96b650ad13172cef2491cb4003d4647c75bc650b4838c0c879f8434a337aa1f226695fbb69223e240cb9337008a5c2a5404e1ef022014a2d0ff00814e150e4b9110f090719337caa9e93d9300ed6304c704ba4c58cbc1313532477f33ff000754ad94499156b61214777fc100ab32cef2e5ca933aa564b67caa942acaf815d431b1c8916cf23968498bbc0b004a92140a4a4d08208208374a8116208a2811cae31daa149201041058820820008391e845fa718be0ea33b1046bcb4e3a73d188022f7a75ad2bb52ff4da9b5a00a0e25c7cada8bb5ef95c7ef1275e234019af7190fae7ae626a0f7fbdbefb53000de0751e63dfbb440274e9cf37f98f7a7134a6db1a9a8fafaf2ea2a2876c4600f7a66f98fa7adf47897639916661a69a9bdf200f3b34482075b9b1a1f5a529006a5aa6a2879d2d2e3cf2e7d3ddf483f003a01ae43377d0b6bd5cc2a2e6b5b800bd6c2b6b74defcfe988de1c787ab6b9167bb41ed978f0bbb1e3e248f17893400fbdad7f21bf3a5ef7a0c4bfbfbf0f16e50e22c33f6ec4fadc5a1516a5ec69700f2ea77f3db9d307072bd9f4fbbfd20f7d2fc3c80b13a588b5b9de20106dd2d00cf901d472eff009e202874fa3073e597583bf52fa6a6d666cc33f9e71208ef006a83bf637df6e5e74df12e19df2cec73f7eb6ce04e6dab656f4e0467a3f4bc006f43f9f7a581b50f5a56869cc60fc6dd5afcc5c5b9f0d2e221ed6f7e2ef63700038dcf089b505b6a103a52c6ddba7a6f83ff004d79f56d5b5b6712e75e8f6b0082786b704bf1ccc45457d477af23cedb54ed4b79623787bbfc9f2ccf2688f000cad98e7e7d7d72692453d6f6e63aff004e5716151254071cd8d8dacfefce2500dbaf5b0e9e3e07982622c2bca95aefd2bb1dcf3f99eb538b97b0fa07b443b900bfec5b21c46be82cd0045cedccd398a6fcaa2b51cef88de1d75f0e376e0dd500b8c49eb6c98e7f2e432e5785ae6b5bd7af4a6d7b53f1ae277866e3cde1a588006e2465cb5cdce44f83409fc4da9ebcf9f4a1b9bd08c0903e5e3c3c74e39e570088f9e5c49707a8bfd98e6627c43e7b1ef5bfdf3ae20281d73cb31e1f2eb13600b793f9b66c1ae33b86d2d024585efd46d6e5fd6f534a508c4bdc0e3cbdb5d8005eefa5de1c34e40104ddf8710d623260d9c0115a5bafcf9f9f3efbe0e2f71600f2f1390e911ae9f316c86ae222a0edcfcc026a76b72dcd0529627a37869ae5009df3e5a35edc21c2de275d35e0d6c9bd61502a6b6b75b56ff9fca9ea70cefe003d72cfdeb944be5600dafe1cfc0be86fac14a4a6a545200a951240090012540049d8002e4d2d882a02e48005c92400033be797f5ca19790b390e0dfeb7035b00b1631687abfc60643c8298a946515c3e7bcd280b68a602241cb92c7bc0d94a00a3e70c78db8f520bbfcd05285bebf7ac3f0919192d7c050eaadabed6303c0c004da4c28cbc6f134ba5a44c7c3a9d6c920cfab43a6794ef5e4d2159de42e54d009b4eb118d32a108b2594ae5748eba3df21a8d2d18bed43d51cefaa537338ce0053a7a60b41588197b43e1a512a656a24312d97367dc43a424a50b7d41d8d89004a10a8c8a89713ef0f9b31fda5c6769aacd5e2f58b9ea04f73213fddd252a0009fd14f4e93b92c00c0acef4e980033a64c57c5182b98b985d45f9683a0fae600629f6343144308430843084308430843084308430843084308430843084308004308430843084308430843084308430843084308457fd2de25353b4afe1e06005f35fdfd969921272ce6053b1b02d341096c372d89f1a63e5296d29f132c4100c4265e974971e817ca9615ce766bb42da3d99dc914f55f9dc3916fe1d5c55300a4a10c13bb4f33784fa50901d089330480af8972261241ba89d3119171fe55005c786a3c0b7106322fa6bc5ce9567d30b03358d5e459fbe43265f989e6d32d0079ef745d5fc16614a512e2d150532d7ef3fdd31512f10cb106b71d6839dfbb003ddaa6cce39dd48aa9cac16b96427b8c414914cb5eeef1126bc014e52ee94700e67f2b3262d9289454a483988a896b604ee9e072f03979b1e51746dadb79b600dd696871b71295a1c6d49536b6dc48525685a4a92b4ad04292a0485050209b000c764a5495252a4a8292a01495248295254010a490e08218822c431117e3c80041f3a0e62b53534e75ade84d3befb4c4f8fcf8fb3fbc4528458efbd857a50000aedb8f2a6f6c2221426bbed717b904ec6c396f700f2e425cf137cf9c3c7df000f48920923aed7e943e84dc9a124dafcf073c4c2229514a79d2b634a0bec687007de97bd7788412907b73a03cefcb714ea49fa9a2112452fb9a1b93f89e54e400450f71424a272e07df31114edb1f3dfc469416b12280ef5e40d02221e1b585000ef4ad45abcf6db9d6a3b5c942041ad0576b5ec7aa79116f9d2a4f44204740006bcfb9371b1e56ed535d85307cefd7f784082453e77da9d69524d88f3d8505008e78f3f1e30f1e70a13437da848bd77279debb0e5e94aa1134206c4f234e9600af3aec08ee7a513839e3efd81e5088f09b5cd6b4ad474ad77af33df9db628400484d3a8ea2de5b8da95df737a5308445081cc8ad695b52bb117a9deb4b57e800842808207624d6a4d457957c85075209a91842383ab6d969c79e71b659690b0071e75c521b69a69b4953ab71c514a50da1214b5294a094a5254a2082710a520050952d6a0842415294a21294a45c9512400000e492c19cc3c7c74eb16b7a9900c5ce95e42f89809546ab3de60614a68cbf2e3adaa56c3fee83a9f8dcc4b4b9002ef77e25065d32b13689877bc6cbd0adb8db896fad768bb54d99c10cc914b3008e355c82526450281a642f777877b5e42a9f75c84aff002df9a992d6e85cb400a92a09b0ba8968b03bc780c87539793f8463a354b893d4ed542fc14c26bfb80072dbb548cb59794ec0c03ad1416d489944870c74d8389357598d885c0177fc00c62061c8484f41ed2f685b47b4dbf267d57e470f5381875015c990a49052450044cde33aab785d689d30c8def891265d80c39939732c4b27fca32f1e3a676b005808a038e0d16a184218421842184218421842184218421842184218421842001842184218421842184218421842184218421842184218421842184218422a00b69f6b6ea7698a90de52cd51d0d2d4ac297228ef04d244e0f7bef9d4a6591c0097988454428a83f132e10718b4a9548949a11c9b02db0da2d9c2061789ce97004e082aa29cd5144a1bdbca029a705a251597df9923ba9c4123bc117113568f00d2a2dc0dc791c9f8863ce2f772371e92a880c426a2e5088973ca71b6dd9ce50057446c0a7de2fc2a7dd93cc5f6e36198870438e08798cd221680b2cc3adc080065cee1c1bb6ca75ee4ac7b0a5c8515252aaac355dec9f88b15ae96a16274b400205d5ddd4542ca5ca259504a15929aa192d2dcd39791bfa9e9178592f58b4c0035012dff0009e74924ce29d5942258e44feee9c95253e220496669839a29000013475308a65652b08717e0553b5309dadd9dc7377f8662f475130b84d3a966009eac901cff00c12a44aa929e0a128a0b59458919099885fe9503cb23e458fa00454bfbe9faf97ad4539721de2d73cf22083e1623ab3ead15c4d37a5fb8a79500ebb53a91c879e20ababfa1e47c33b67930844115a76a1febcc6e29ebe7890b003cac322c2eda6567d33e1c90a52d4f976f2e8310559dcb599ada5fdb962d63000852bbf98dcd4da83b7e1d6f51890a3a970ceed9756b0beac7d610a11f8f7b000bfe17039f2ad711bfe16d1b870be4798b42007adbaf9fe1bf96f6c48517cf00803a338bbb8670438f2e410a0a76bf4ad697e87d7957e6dec803e39f99278d00ddb85b48400de94b7f41f98c0aee7e97bb5b523a8bf89bc226961d0d69e9f700e5f5c46f13adb91be7aeb60e6c1b9306088a6ff2a83bf2ad773e7e5db13bfc00386bc79b79db98376644d3b6d734b50d79fafcef5ed1be78e990e3f26b66c400876be7088a6e2ffdaa6a08f3bd0d08a624298dcb86f1761e1c9adabdf344d000f4a7d3e96db7b0b56f6c37af9fa72d5cdaf7001d58b42299e73d62d31d3f4b00bfc599d2472d89654942e58dc5098ce42d40100c965a98b9a04dd254e2a11200d3614952d69490a3c7f16dadd9dc0c2bf89e2d494f31040553a6677f56e45b00fe074fded484f15195b81c152859e854c423f5280d1b33e41cfa459e67ae3d002590e5e84d39ca11131750e3adb739cd4efc1c1292dabc0879993cb9f5c64400b1102ae37f1331963edb7e0f7b0c9716b6d9eaac6bb6ca746fcbc030b5cf580052922ab1357752484964ad14b216a9b312bba815d4532c27777e5ef129463a00aa80b2124f355867c03921b983ca2c8f5035bb53b5356b466ccd71f132e5290045123812995c89b4fbc2eb6954b200310f16a64d12d44cc04646a50901512a003527a7b1cdb0da2da22a189e273e64824b51493f97a248dede4834f277513400a324cc9fdece00006618c55cc5aff52891c321e4387137e714a71c6628861000861086108610861086108610861086108610861086108610861086108610860010861086108610861086108610861086108610861086108610861086108610008abb94f5eb5872555120cff3f4439425b103338944fe5eda10544087809eb70031858424abf9d708d30e3802438a525090394619b6bb558438a2c6eb82080900126a660ad90909723bb935a9a89528dee6525054000a240005c4cd989c965b0081b8f00a76f08b8ecb5c79ea04bd30ed668ca59673236ca128762205d8ecbd00308af0a0243af3a153697b6f29c1e373e1a54c30412db70ed0a1c73dc3fb6800c724042711c330faf09002972553a867cc64b6f2d4f55202cabe257774e84500d9284e7179354b0dbc94a9b321c13cce61f8b003908aef22e3c74c6356cb5300dcb39be44a74a12e4432d4b273030dfcb552dd71a8f838f5b6957f282c4b9e00715fcaaf749b84f32a3ed9f67a72909acc3f14a32a202a62114f55265b8b95002933e54e2906c372429445f74651745520fea4a87911f43e9157655c54e82400ddf661d8d41828475f250813595cfa54ca0a4151f7f193095c3c143a4f84d1004f453682a2129512a485728a6ed2762aa968968c6e54a52c903f334d594c80004027e39b3e9e5c940b58ae6004b005c806b13e51b6ff009850f521a2a5416a009e98cc105503a8d9163535093f0d9b640f7849a1f0a83730514280503e05040090390b9c6fe56d26cecf0f271ec1a6819f778a512d89d084cf241b8b163173007d0725a4ff00d21f78ed3073893cc9087a5d3696c7b2b00a1c828f858a428100b85256cbae2555a920a54410450d2b8d94aaba59e90a91534f3926e152a74b0098920e44142882fc8c4820e441e8447b21700836aee0efc8936a120dc1de9d00f19113125200b904022f5a0a0f96c493f99c090332075847ac8d9c49e5cdad00d984d65900d2012b76323a1615b6c0bff3ade75b4a452b535141734a9ae3cd00aba59092b9f534f25290ea54d9d2e5a40cdc95a92006d4988240cc81d4b4750068cd53d30970f147ea2e448304803e2b374819f11153448723d2a5abc35252009aaadb1071ad9bb49b3b203cec7b06940e5de629448721ec02a7824d8d8398008df40cd691ff00487de29a4d78aad0394bcfc3bfa810516f30bf0a84aa553f009ab2b34068cc6404a9f827d14200718895b60d478c10a034153da4ec552ad60085e372a6a9058fe5a9ab6a504b3fc1364d32e4ac5f344c507d6c5a833e502d00be3c0288f3008f6d9c5229df1e3a6304b886a45963384f16d296869f886a5700288189f0d7c0e32e391f1b1c869c3ce225acbc80492cd7f94f17aced9f67a40095a68f0fc52b0a49095ad34f4b266364a4a953a6ce0927fcf4e85017dc394500b354819051e7603e6fe9142732f1e7a83304443396329659cb8dbc8521a88800f763f30cc617c4823deb2f15ca65eb792e1f7883112a7d8b04b90ee5ce386e0021db46393c2d38761987d0254084ae72a7574f96e923792b7a590a5853293b00f4cb40665215169554b3fa5294f57247c87a45b866cd79d60ceb444ff3fcfd00c87085b660a5912890cbdc42ca5444440489b97424590523c0b8b69f71b05400942d295281e0589edaed562ec2b71bae5200293269e60a290a0483fde48a2400d3ca9a41163352b526e12402445954d98acd6785be117e212cfe3148f1c5e200dc30843084308430843084308430843084308430843084308430843084308400308430847fffd900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'; +export default data; diff --git a/ui/block/BlockDetails.pw.tsx b/ui/block/BlockDetails.pw.tsx index a38f551041..a10a06ef5a 100644 --- a/ui/block/BlockDetails.pw.tsx +++ b/ui/block/BlockDetails.pw.tsx @@ -1,10 +1,8 @@ -import { test, expect } from '@playwright/experimental-ct-react'; import React from 'react'; import * as blockMock from 'mocks/blocks/block'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; -import * as configs from 'playwright/utils/configs'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import { test, expect } from 'playwright/lib'; import BlockDetails from './BlockDetails'; import type { BlockQuery } from './useBlockQuery'; @@ -15,59 +13,56 @@ const hooksConfig = { }, }; -test('regular block +@mobile +@dark-mode', async({ mount, page }) => { +test('regular block +@mobile +@dark-mode', async({ render, page }) => { const query = { data: blockMock.base, isPending: false, } as BlockQuery; - const component = await mount( - - - , - { hooksConfig }, - ); + const component = await render(, { hooksConfig }); await page.getByText('View details').click(); await expect(component).toHaveScreenshot(); }); -test('genesis block', async({ mount, page }) => { +test('genesis block', async({ render, page }) => { const query = { data: blockMock.genesis, isPending: false, } as BlockQuery; - const component = await mount( - - - , - { hooksConfig }, - ); + const component = await render(, { hooksConfig }); await page.getByText('View details').click(); await expect(component).toHaveScreenshot(); }); -const customFieldsTest = test.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.viewsEnvs.block.hiddenFields) as any, +test('with blob txs', async({ render, page, mockEnvs }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED', 'true' ], + ]); + const query = { + data: blockMock.withBlobTxs, + isPending: false, + } as BlockQuery; + + const component = await render(, { hooksConfig }); + + await page.getByText('View details').click(); + + await expect(component).toHaveScreenshot(); }); -customFieldsTest('rootstock custom fields', async({ mount, page }) => { +test('rootstock custom fields', async({ render, page, mockEnvs }) => { + await mockEnvs(ENVS_MAP.blockHiddenFields); const query = { data: blockMock.rootstock, isPending: false, } as BlockQuery; - const component = await mount( - - - , - { hooksConfig }, - ); + const component = await render(, { hooksConfig }); await page.getByText('View details').click(); diff --git a/ui/block/BlockDetails.tsx b/ui/block/BlockDetails.tsx index 493c462122..49777319f4 100644 --- a/ui/block/BlockDetails.tsx +++ b/ui/block/BlockDetails.tsx @@ -5,6 +5,8 @@ import { useRouter } from 'next/router'; import React from 'react'; import { scroller, Element } from 'react-scroll'; +import { ZKSYNC_L2_TX_BATCH_STATUSES } from 'types/api/zkSyncL2'; + import { route } from 'nextjs-routes'; import config from 'configs/app'; @@ -19,6 +21,7 @@ import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider'; import DetailsTimestamp from 'ui/shared/DetailsTimestamp'; import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2'; import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import IconSvg from 'ui/shared/IconSvg'; @@ -27,7 +30,10 @@ import PrevNext from 'ui/shared/PrevNext'; import RawDataSnippet from 'ui/shared/RawDataSnippet'; import TextSeparator from 'ui/shared/TextSeparator'; import Utilization from 'ui/shared/Utilization/Utilization'; +import VerificationSteps from 'ui/shared/verificationSteps/VerificationSteps'; +import ZkSyncL2TxnBatchHashesInfo from 'ui/txnBatches/zkSyncL2/ZkSyncL2TxnBatchHashesInfo'; +import BlockDetailsBlobInfo from './details/BlockDetailsBlobInfo'; import type { BlockQuery } from './useBlockQuery'; interface Props { @@ -114,6 +120,31 @@ const BlockDetails = ({ query }: Props) => { return config.chain.verificationType === 'validation' ? 'Validated by' : 'Mined by'; })(); + const txsNum = (() => { + const blockTxsNum = ( + + { data.tx_count } txn{ data.tx_count === 1 ? '' : 's' } + + ); + + const blockBlobTxsNum = (config.features.dataAvailability.isEnabled && data.blob_tx_count) ? ( + <> + including + + { data.blob_tx_count } blob txn{ data.blob_tx_count === 1 ? '' : 's' } + + + ) : null; + + return ( + <> + { blockTxsNum } + { blockBlobTxsNum } + in this block + + ); + })(); + const blockTypeLabel = (() => { switch (data.type) { case 'reorg': @@ -172,9 +203,7 @@ const BlockDetails = ({ query }: Props) => { isLoading={ isPlaceholderData } > - - { data.tx_count } transaction{ data.tx_count === 1 ? '' : 's' } - + { txsNum } { config.features.beaconChain.isEnabled && Boolean(data.withdrawals_count) && ( @@ -190,6 +219,31 @@ const BlockDetails = ({ query }: Props) => { ) } + + { rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && data.zksync && !config.UI.views.block.hiddenFields?.batch && ( + + { data.zksync.batch_number ? ( + + ) : Pending } + + ) } + { rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && data.zksync && !config.UI.views.block.hiddenFields?.L1_status && ( + + + + ) } + { !config.UI.views.block.hiddenFields?.miner && ( { <> + { rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && data.zksync && + } + + { !isPlaceholderData && } + { data.bitcoin_merged_mining_header && ( { + if ( + !data.blob_gas_price || + !data.blob_gas_used || + !data.burnt_blob_fees || + !data.excess_blob_gas + ) { + return null; + } + + const burntBlobFees = BigNumber(data.burnt_blob_fees || 0); + const blobFees = BigNumber(data.blob_gas_price || 0).multipliedBy(BigNumber(data.blob_gas_used || 0)); + + return ( + <> + + { data.blob_gas_price && ( + + { BigNumber(data.blob_gas_price).dividedBy(WEI).toFixed() } { currencyUnits.ether } + + { space }({ BigNumber(data.blob_gas_price).dividedBy(WEI_IN_GWEI).toFixed() } { currencyUnits.gwei }) + + + ) } + { data.blob_gas_used && ( + + { BigNumber(data.blob_gas_used).toFormat() } + + ) } + { !burntBlobFees.isEqualTo(ZERO) && ( + + + { burntBlobFees.dividedBy(WEI).toFixed() } { currencyUnits.ether } + { !blobFees.isEqualTo(ZERO) && ( + +
+ +
+
+ ) } +
+ ) } + { data.excess_blob_gas && ( + + { BigNumber(data.excess_blob_gas).dividedBy(WEI).toFixed() } { currencyUnits.ether } + + { space }({ BigNumber(data.excess_blob_gas).dividedBy(WEI_IN_GWEI).toFixed() } { currencyUnits.gwei }) + + + ) } + + + ); +}; + +export default React.memo(BlockDetailsBlobInfo); diff --git a/ui/block/useBlockBlobTxsQuery.tsx b/ui/block/useBlockBlobTxsQuery.tsx new file mode 100644 index 0000000000..23a6d71ac2 --- /dev/null +++ b/ui/block/useBlockBlobTxsQuery.tsx @@ -0,0 +1,26 @@ +import { TX } from 'stubs/tx'; +import { generateListStub } from 'stubs/utils'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; + +import type { BlockQuery } from './useBlockQuery'; + +interface Params { + heightOrHash: string; + blockQuery: BlockQuery; + tab: string; +} + +export default function useBlockBlobTxsQuery({ heightOrHash, blockQuery, tab }: Params) { + const apiQuery = useQueryWithPages({ + resourceName: 'block_txs', + pathParams: { height_or_hash: heightOrHash }, + filters: { type: 'blob_transaction' }, + options: { + enabled: Boolean(tab === 'blob_txs' && !blockQuery.isPlaceholderData && blockQuery.data?.blob_tx_count), + placeholderData: generateListStub<'block_txs'>(TX, 3, { next_page_params: null }), + refetchOnMount: false, + }, + }); + + return apiQuery; +} diff --git a/ui/block/useBlockTxQuery.tsx b/ui/block/useBlockTxsQuery.tsx similarity index 98% rename from ui/block/useBlockTxQuery.tsx rename to ui/block/useBlockTxsQuery.tsx index 49a637b187..547db2b2e4 100644 --- a/ui/block/useBlockTxQuery.tsx +++ b/ui/block/useBlockTxsQuery.tsx @@ -33,7 +33,7 @@ interface Params { tab: string; } -export default function useBlockTxQuery({ heightOrHash, blockQuery, tab }: Params): BlockTxsQuery { +export default function useBlockTxsQuery({ heightOrHash, blockQuery, tab }: Params): BlockTxsQuery { const [ isRefetchEnabled, setRefetchEnabled ] = React.useState(false); const apiQuery = useQueryWithPages({ diff --git a/ui/blocks/BlocksTable.tsx b/ui/blocks/BlocksTable.tsx index cfd481321d..9a422d0de5 100644 --- a/ui/blocks/BlocksTable.tsx +++ b/ui/blocks/BlocksTable.tsx @@ -33,7 +33,7 @@ const isRollup = config.features.rollup.isEnabled; const BlocksTable = ({ data, isLoading, top, page, showSocketInfo, socketInfoNum, socketInfoAlert }: Props) => { const widthBase = - VALIDATOR_COL_WEIGHT + + (!config.UI.views.block.hiddenFields?.miner ? VALIDATOR_COL_WEIGHT : 0) + GAS_COL_WEIGHT + (!isRollup && !config.UI.views.block.hiddenFields?.total_reward ? REWARD_COL_WEIGHT : 0) + (!isRollup && !config.UI.views.block.hiddenFields?.burnt_fees ? FEES_COL_WEIGHT : 0); diff --git a/ui/contractVerification/ContractVerificationForm.pw.tsx b/ui/contractVerification/ContractVerificationForm.pw.tsx index 140ba75293..44b219f38d 100644 --- a/ui/contractVerification/ContractVerificationForm.pw.tsx +++ b/ui/contractVerification/ContractVerificationForm.pw.tsx @@ -54,6 +54,22 @@ const formConfig: SmartContractVerificationConfig = { 'petersburg', 'istanbul', ], + license_types: { + apache_2_0: 12, + bsd_2_clause: 8, + bsd_3_clause: 9, + bsl_1_1: 14, + gnu_agpl_v3: 13, + gnu_gpl_v2: 4, + gnu_gpl_v3: 5, + gnu_lgpl_v2_1: 6, + gnu_lgpl_v3: 7, + mit: 3, + mpl_2_0: 10, + none: 1, + osl_3_0: 11, + unlicense: 2, + }, }; test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) => { @@ -64,9 +80,16 @@ test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) = { hooksConfig }, ); + // select license + await component.getByLabel(/contract license/i).focus(); + await component.getByLabel(/contract license/i).fill('mit'); + await page.getByRole('button', { name: /mit license/i }).click(); + + // select method await component.getByLabel(/verification method/i).focus(); - await component.getByLabel(/verification method/i).type('solidity'); + await component.getByLabel(/verification method/i).fill('solidity'); await page.getByRole('button', { name: /flattened source code/i }).click(); + await page.getByText(/add contract libraries/i).click(); await page.locator('button[aria-label="add"]').click(); @@ -81,8 +104,9 @@ test('standard input json method', async({ mount, page }) => { { hooksConfig }, ); + // select method await component.getByLabel(/verification method/i).focus(); - await component.getByLabel(/verification method/i).type('solidity'); + await component.getByLabel(/verification method/i).fill('solidity'); await page.getByRole('button', { name: /standard json input/i }).click(); await expect(component).toHaveScreenshot(); @@ -102,8 +126,9 @@ test.describe('sourcify', () => { { hooksConfig }, ); + // select method await component.getByLabel(/verification method/i).focus(); - await component.getByLabel(/verification method/i).type('solidity'); + await component.getByLabel(/verification method/i).fill('solidity'); await page.getByRole('button', { name: /sourcify/i }).click(); await page.getByText(/drop files/i).click(); @@ -129,7 +154,7 @@ test.describe('sourcify', () => { }); await component.getByLabel(/contract name/i).focus(); - await component.getByLabel(/contract name/i).type('e'); + await component.getByLabel(/contract name/i).fill('e'); const contractNameOption = page.getByRole('button', { name: /MockERC20/i }); await expect(contractNameOption).toBeVisible(); @@ -146,8 +171,9 @@ test('multi-part files method', async({ mount, page }) => { { hooksConfig }, ); + // select method await component.getByLabel(/verification method/i).focus(); - await component.getByLabel(/verification method/i).type('solidity'); + await component.getByLabel(/verification method/i).fill('solidity'); await page.getByRole('button', { name: /multi-part files/i }).click(); await expect(component).toHaveScreenshot(); @@ -161,8 +187,9 @@ test('vyper contract method', async({ mount, page }) => { { hooksConfig }, ); + // select method await component.getByLabel(/verification method/i).focus(); - await component.getByLabel(/verification method/i).type('vyper'); + await component.getByLabel(/verification method/i).fill('vyper'); await page.getByRole('button', { name: /contract/i }).click(); await expect(component).toHaveScreenshot(); @@ -176,8 +203,9 @@ test('vyper multi-part method', async({ mount, page }) => { { hooksConfig }, ); + // select method await component.getByLabel(/verification method/i).focus(); - await component.getByLabel(/verification method/i).type('vyper'); + await component.getByLabel(/verification method/i).fill('vyper'); await page.getByRole('button', { name: /multi-part files/i }).click(); await expect(component).toHaveScreenshot(); @@ -191,8 +219,9 @@ test('vyper vyper-standard-input method', async({ mount, page }) => { { hooksConfig }, ); + // select method await component.getByLabel(/verification method/i).focus(); - await component.getByLabel(/verification method/i).type('vyper'); + await component.getByLabel(/verification method/i).fill('vyper'); await page.getByRole('button', { name: /standard json input/i }).click(); await expect(component).toHaveScreenshot(); diff --git a/ui/contractVerification/ContractVerificationForm.tsx b/ui/contractVerification/ContractVerificationForm.tsx index 2a464a017e..8b77e1010c 100644 --- a/ui/contractVerification/ContractVerificationForm.tsx +++ b/ui/contractVerification/ContractVerificationForm.tsx @@ -18,6 +18,7 @@ import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketMessage from 'lib/socket/useSocketMessage'; import ContractVerificationFieldAddress from './fields/ContractVerificationFieldAddress'; +import ContractVerificationFieldLicenseType from './fields/ContractVerificationFieldLicenseType'; import ContractVerificationFieldMethod from './fields/ContractVerificationFieldMethod'; import ContractVerificationFlattenSourceCode from './methods/ContractVerificationFlattenSourceCode'; import ContractVerificationMultiPartFile from './methods/ContractVerificationMultiPartFile'; @@ -37,7 +38,7 @@ interface Props { const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Props) => { const formApi = useForm({ mode: 'onBlur', - defaultValues: methodFromQuery ? getDefaultValues(methodFromQuery, config, hash) : undefined, + defaultValues: methodFromQuery ? getDefaultValues(methodFromQuery, config, hash, null) : undefined, }); const { control, handleSubmit, watch, formState, setError, reset, getFieldState } = formApi; const submitPromiseResolver = React.useRef<(value: unknown) => void>(); @@ -161,12 +162,13 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro }; }, [ config ]); const method = watch('method'); + const licenseType = watch('license_type'); const content = methods[method?.value] || null; const methodValue = method?.value; useUpdateEffect(() => { if (methodValue) { - reset(getDefaultValues(methodValue, config, address || hash)); + reset(getDefaultValues(methodValue, config, hash || address, licenseType)); const methodName = METHOD_LABELS[methodValue]; mixpanel.logEvent(mixpanel.EventTypes.CONTRACT_VERIFICATION, { Status: 'Method selected', Method: methodName }); @@ -183,6 +185,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro > { !hash && } + { Contract address to verify - + ({ label: `${ title } (${ label })`, value: type })); + +import ContractVerificationFormRow from '../ContractVerificationFormRow'; + +const ContractVerificationFieldLicenseType = () => { + const { formState, control } = useFormContext(); + const isMobile = useIsMobile(); + + const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps}) => { + const error = 'license_type' in formState.errors ? formState.errors.license_type : undefined; + + return ( + + ); + }, [ formState.errors, formState.isSubmitting, isMobile ]); + + return ( + + + + For best practices, all contract source code holders, publishers and authors are encouraged to also + specify the accompanying license for their verified contract source code provided. + + + ); +}; + +export default React.memo(ContractVerificationFieldLicenseType); diff --git a/ui/contractVerification/fields/ContractVerificationFieldMethod.tsx b/ui/contractVerification/fields/ContractVerificationFieldMethod.tsx index 7e9c5088d2..83d4090c0e 100644 --- a/ui/contractVerification/fields/ContractVerificationFieldMethod.tsx +++ b/ui/contractVerification/fields/ContractVerificationFieldMethod.tsx @@ -11,6 +11,7 @@ import { DarkMode, ListItem, OrderedList, + Box, } from '@chakra-ui/react'; import React from 'react'; import type { ControllerRenderProps, Control } from 'react-hook-form'; @@ -97,7 +98,7 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props return ( <> -
+ Currently, Blockscout supports { methods.length } contract verification methods @@ -121,8 +122,7 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props -
-
+ ; + license_type: LicenseOption | null; } export interface FormFieldsStandardInput { @@ -34,6 +40,7 @@ export interface FormFieldsStandardInput { sources: Array; autodetect_constructor_args: boolean; constructor_args: string; + license_type: LicenseOption | null; } export interface FormFieldsSourcify { @@ -41,6 +48,7 @@ export interface FormFieldsSourcify { method: MethodOption; sources: Array; contract_index?: Option; + license_type: LicenseOption | null; } export interface FormFieldsMultiPartFile { @@ -52,6 +60,7 @@ export interface FormFieldsMultiPartFile { optimization_runs: string; sources: Array; libraries: Array; + license_type: LicenseOption | null; } export interface FormFieldsVyperContract { @@ -62,6 +71,7 @@ export interface FormFieldsVyperContract { compiler: Option | null; code: string; constructor_args: string | undefined; + license_type: LicenseOption | null; } export interface FormFieldsVyperMultiPartFile { @@ -71,6 +81,7 @@ export interface FormFieldsVyperMultiPartFile { evm_version: Option | null; sources: Array; interfaces: Array; + license_type: LicenseOption | null; } export interface FormFieldsVyperStandardInput { @@ -78,6 +89,7 @@ export interface FormFieldsVyperStandardInput { method: MethodOption; compiler: Option | null; sources: Array; + license_type: LicenseOption | null; } export type FormFields = FormFieldsFlattenSourceCode | FormFieldsStandardInput | FormFieldsSourcify | diff --git a/ui/contractVerification/utils.ts b/ui/contractVerification/utils.ts index 1a60089eea..5791b12566 100644 --- a/ui/contractVerification/utils.ts +++ b/ui/contractVerification/utils.ts @@ -11,7 +11,12 @@ import type { FormFieldsVyperMultiPartFile, FormFieldsVyperStandardInput, } from './types'; -import type { SmartContractVerificationMethod, SmartContractVerificationError, SmartContractVerificationConfig } from 'types/api/contract'; +import type { + SmartContractVerificationMethod, + SmartContractVerificationError, + SmartContractVerificationConfig, + SmartContractLicenseType, +} from 'types/api/contract'; import type { Params as FetchParams } from 'lib/hooks/useFetch'; @@ -52,6 +57,7 @@ export const DEFAULT_VALUES: Record autodetect_constructor_args: true, constructor_args: '', libraries: [], + license_type: null, }, 'standard-input': { address: '', @@ -64,6 +70,7 @@ export const DEFAULT_VALUES: Record sources: [], autodetect_constructor_args: true, constructor_args: '', + license_type: null, }, sourcify: { address: '', @@ -72,6 +79,7 @@ export const DEFAULT_VALUES: Record label: METHOD_LABELS.sourcify, }, sources: [], + license_type: null, }, 'multi-part': { address: '', @@ -85,6 +93,7 @@ export const DEFAULT_VALUES: Record optimization_runs: '200', sources: [], libraries: [], + license_type: null, }, 'vyper-code': { address: '', @@ -97,6 +106,7 @@ export const DEFAULT_VALUES: Record evm_version: null, code: '', constructor_args: '', + license_type: null, }, 'vyper-multi-part': { address: '', @@ -107,6 +117,7 @@ export const DEFAULT_VALUES: Record compiler: null, evm_version: null, sources: [], + license_type: null, }, 'vyper-standard-input': { address: '', @@ -116,11 +127,17 @@ export const DEFAULT_VALUES: Record }, compiler: null, sources: [], + license_type: null, }, }; -export function getDefaultValues(method: SmartContractVerificationMethod, config: SmartContractVerificationConfig, hash?: string) { - const defaultValues = { ...DEFAULT_VALUES[method], address: hash }; +export function getDefaultValues( + method: SmartContractVerificationMethod, + config: SmartContractVerificationConfig, + hash: string | undefined, + licenseType: FormFields['license_type'], +) { + const defaultValues: FormFields = { ...DEFAULT_VALUES[method], address: hash || '', license_type: licenseType }; if ('evm_version' in defaultValues) { if (method === 'flattened-code' || method === 'multi-part') { @@ -162,6 +179,8 @@ export function sortVerificationMethods(methodA: SmartContractVerificationMethod } export function prepareRequestBody(data: FormFields): FetchParams['body'] { + const defaultLicenseType: SmartContractLicenseType = 'none'; + switch (data.method.value) { case 'flattened-code': { const _data = data as FormFieldsFlattenSourceCode; @@ -176,6 +195,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] { evm_version: _data.evm_version?.value, autodetect_constructor_args: _data.autodetect_constructor_args, constructor_args: _data.constructor_args, + license_type: _data.license_type?.value ?? defaultLicenseType, }; } @@ -184,6 +204,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] { const body = new FormData(); _data.compiler && body.set('compiler_version', _data.compiler.value); + body.set('license_type', _data.license_type?.value ?? defaultLicenseType); body.set('contract_name', _data.name); body.set('autodetect_constructor_args', String(Boolean(_data.autodetect_constructor_args))); body.set('constructor_args', _data.constructor_args); @@ -196,7 +217,8 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] { const _data = data as FormFieldsSourcify; const body = new FormData(); addFilesToFormData(body, _data.sources, 'files'); - _data.contract_index && body.set('chosen_contract_index', _data.contract_index.value); + body.set('chosen_contract_index', _data.contract_index?.value ?? defaultLicenseType); + _data.license_type && body.set('license_type', _data.license_type.value); return body; } @@ -207,6 +229,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] { const body = new FormData(); _data.compiler && body.set('compiler_version', _data.compiler.value); _data.evm_version && body.set('evm_version', _data.evm_version.value); + body.set('license_type', _data.license_type?.value ?? defaultLicenseType); body.set('is_optimization_enabled', String(Boolean(_data.is_optimization_enabled))); _data.is_optimization_enabled && body.set('optimization_runs', _data.optimization_runs); @@ -226,6 +249,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] { source_code: _data.code, contract_name: _data.name, constructor_args: _data.constructor_args, + license_type: _data.license_type?.value ?? defaultLicenseType, }; } @@ -235,6 +259,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] { const body = new FormData(); _data.compiler && body.set('compiler_version', _data.compiler.value); _data.evm_version && body.set('evm_version', _data.evm_version.value); + body.set('license_type', _data.license_type?.value ?? defaultLicenseType); addFilesToFormData(body, _data.sources, 'files'); addFilesToFormData(body, _data.interfaces, 'interfaces'); @@ -246,6 +271,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] { const body = new FormData(); _data.compiler && body.set('compiler_version', _data.compiler.value); + body.set('license_type', _data.license_type?.value ?? defaultLicenseType); addFilesToFormData(body, _data.sources, 'files'); return body; diff --git a/ui/csvExport/CsvExportForm.tsx b/ui/csvExport/CsvExportForm.tsx index 1105757b2c..917d706e8f 100644 --- a/ui/csvExport/CsvExportForm.tsx +++ b/ui/csvExport/CsvExportForm.tsx @@ -21,9 +21,10 @@ interface Props { filterType?: CsvExportParams['filterType'] | null; filterValue?: CsvExportParams['filterValue'] | null; fileNameTemplate: string; + exportType: CsvExportParams['type'] | undefined; } -const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTemplate }: Props) => { +const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTemplate, exportType }: Props) => { const formApi = useForm({ mode: 'onBlur', defaultValues: { @@ -36,10 +37,10 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla const onFormSubmit: SubmitHandler = React.useCallback(async(data) => { try { - const url = buildUrl(resource, undefined, { + const url = buildUrl(resource, { hash } as never, { address_id: hash, - from_period: data.from, - to_period: data.to, + from_period: exportType !== 'holders' ? data.from : null, + to_period: exportType !== 'holders' ? data.to : null, filter_type: filterType, filter_value: filterValue, recaptcha_response: data.reCaptcha, @@ -56,11 +57,11 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla } const blob = await response.blob(); - downloadBlob( - blob, - `${ fileNameTemplate }_${ hash }_${ data.from }_${ data.to } - ${ filterType && filterValue ? '_with_filter_type_' + filterType + '_value_' + filterValue : '' }.csv`, - ); + const fileName = exportType === 'holders' ? + `${ fileNameTemplate }_${ hash }.csv` : + // eslint-disable-next-line max-len + `${ fileNameTemplate }_${ hash }_${ data.from }_${ data.to }${ filterType && filterValue ? '_with_filter_type_' + filterType + '_value_' + filterValue : '' }.csv`; + downloadBlob(blob, fileName); } catch (error) { toast({ @@ -73,7 +74,7 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla }); } - }, [ fileNameTemplate, hash, resource, filterType, filterValue, toast ]); + }, [ resource, hash, exportType, filterType, filterValue, fileNameTemplate, toast ]); return ( @@ -82,8 +83,8 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla onSubmit={ handleSubmit(onFormSubmit) } > - - + { exportType !== 'holders' && } + { exportType !== 'holders' && } + ); +}; + +export default React.forwardRef(TriggerButton); diff --git a/ui/marketplace/MarketplaceAppInfo/WebsiteLink.tsx b/ui/marketplace/MarketplaceAppInfo/WebsiteLink.tsx new file mode 100644 index 0000000000..2a3dea9cca --- /dev/null +++ b/ui/marketplace/MarketplaceAppInfo/WebsiteLink.tsx @@ -0,0 +1,36 @@ +import { Link } from '@chakra-ui/react'; +import React from 'react'; + +import IconSvg from 'ui/shared/IconSvg'; + +interface Props { + url?: string | undefined; +} + +const WebsiteLink = ({ url }: Props) => { + if (!url) { + return null; + } + + function getHostname(url: string) { + try { + return new URL(url).hostname; + } catch (err) {} + } + + return ( + + + { getHostname(url) } + + ); +}; + +export default WebsiteLink; diff --git a/ui/marketplace/MarketplaceAppIntegrationIcon.tsx b/ui/marketplace/MarketplaceAppIntegrationIcon.tsx new file mode 100644 index 0000000000..f884c11ca2 --- /dev/null +++ b/ui/marketplace/MarketplaceAppIntegrationIcon.tsx @@ -0,0 +1,51 @@ +import { Tooltip } from '@chakra-ui/react'; +import React from 'react'; + +import type { IconName } from 'ui/shared/IconSvg'; +import IconSvg from 'ui/shared/IconSvg'; + +type Props = { + internalWallet: boolean | undefined; + external: boolean | undefined; +} + +const MarketplaceAppIntegrationIcon = ({ external, internalWallet }: Props) => { + const [ icon, iconColor, text ] = React.useMemo(() => { + let icon: IconName = 'integration/partial'; + let color = 'gray.400'; + let text = 'This app opens in Blockscout without Blockscout wallet functionality. Use your external web3 wallet to connect directly to this application'; + + if (external) { + icon = 'arrows/north-east'; + text = 'This app opens in a separate tab'; + } else if (internalWallet) { + icon = 'integration/full'; + color = 'green.500'; + text = 'This app opens in Blockscout and your Blockscout wallet connects automatically'; + } + + return [ icon, color, text ]; + }, [ external, internalWallet ]); + + return ( + + + + ); +}; + +export default MarketplaceAppIntegrationIcon; diff --git a/ui/marketplace/MarketplaceAppModal.pw.tsx b/ui/marketplace/MarketplaceAppModal.pw.tsx new file mode 100644 index 0000000000..ba6aca1233 --- /dev/null +++ b/ui/marketplace/MarketplaceAppModal.pw.tsx @@ -0,0 +1,41 @@ +import { test, expect, devices } from '@playwright/experimental-ct-react'; +import React from 'react'; + +import type { MarketplaceAppWithSecurityReport } from 'types/client/marketplace'; + +import { apps as appsMock } from 'mocks/apps/apps'; +import TestApp from 'playwright/TestApp'; + +import MarketplaceAppModal from './MarketplaceAppModal'; + +const props = { + onClose: () => {}, + onFavoriteClick: () => {}, + showContractList: () => {}, + data: appsMock[0] as MarketplaceAppWithSecurityReport, + isFavorite: false, +}; + +const testFn: Parameters[1] = async({ mount, page }) => { + await page.route(appsMock[0].logo, (route) => + route.fulfill({ + status: 200, + path: './playwright/mocks/image_s.jpg', + }), + ); + + await mount( + + + , + ); + + await expect(page).toHaveScreenshot(); +}; + +test('base view +@dark-mode', testFn); + +test.describe('mobile', () => { + test.use({ viewport: devices['iPhone 13 Pro'].viewport }); + test('base view', testFn); +}); diff --git a/ui/marketplace/MarketplaceAppModal.tsx b/ui/marketplace/MarketplaceAppModal.tsx index d997621f8a..ff318722b8 100644 --- a/ui/marketplace/MarketplaceAppModal.tsx +++ b/ui/marketplace/MarketplaceAppModal.tsx @@ -1,23 +1,28 @@ import { Box, Flex, Heading, IconButton, Image, Link, List, Modal, ModalBody, - ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Tag, Text, useColorModeValue, + ModalCloseButton, ModalContent, ModalFooter, ModalOverlay, Tag, Text, useColorModeValue, } from '@chakra-ui/react'; import React, { useCallback } from 'react'; -import type { MarketplaceAppOverview } from 'types/client/marketplace'; +import type { MarketplaceAppWithSecurityReport } from 'types/client/marketplace'; +import { ContractListTypes } from 'types/client/marketplace'; import useIsMobile from 'lib/hooks/useIsMobile'; import { nbsp } from 'lib/html-entities'; +import * as mixpanel from 'lib/mixpanel/index'; import type { IconName } from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg'; +import AppSecurityReport from './AppSecurityReport'; +import ContractListButton, { ContractListButtonVariants } from './ContractListButton'; import MarketplaceAppModalLink from './MarketplaceAppModalLink'; type Props = { onClose: () => void; isFavorite: boolean; - onFavoriteClick: (id: string, isFavorite: boolean) => void; - data: MarketplaceAppOverview; + onFavoriteClick: (id: string, isFavorite: boolean, source: 'App modal') => void; + data: MarketplaceAppWithSecurityReport; + showContractList: (id: string, type: ContractListTypes, hasPreviousStep: boolean) => void; } const MarketplaceAppModal = ({ @@ -25,8 +30,12 @@ const MarketplaceAppModal = ({ isFavorite, onFavoriteClick, data, + showContractList: showContractListProp, }: Props) => { + const starOutlineIconColor = useColorModeValue('gray.600', 'gray.300'); + const { + id, title, url, external, @@ -36,33 +45,68 @@ const MarketplaceAppModal = ({ github, telegram, twitter, + discord, logo, logoDarkMode, categories, + securityReport, } = data; const socialLinks = [ telegram ? { - icon: 'social/telega' as IconName, + icon: 'social/telegram_filled' as IconName, url: telegram, } : null, twitter ? { - icon: 'social/tweet' as IconName, + icon: 'social/twitter_filled' as IconName, url: twitter, } : null, - github ? { - icon: 'social/git' as IconName, - url: github, + discord ? { + icon: 'social/discord_filled' as IconName, + url: discord, } : null, ].filter(Boolean); + if (github) { + if (Array.isArray(github)) { + github.forEach((url) => socialLinks.push({ icon: 'social/github_filled', url })); + } else { + socialLinks.push({ icon: 'social/github_filled', url: github }); + } + } + const handleFavoriteClick = useCallback(() => { - onFavoriteClick(data.id, isFavorite); - }, [ onFavoriteClick, data.id, isFavorite ]); + onFavoriteClick(id, isFavorite, 'App modal'); + }, [ onFavoriteClick, id, isFavorite ]); + + const showContractList = useCallback((type: ContractListTypes) => { + onClose(); + showContractListProp(id, type, true); + }, [ onClose, showContractListProp, id ]); + + const showAllContracts = React.useCallback(() => { + mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Total contracts', Info: id, Source: 'App modal' }); + showContractList(ContractListTypes.ALL); + }, [ showContractList, id ]); + + const showVerifiedContracts = React.useCallback(() => { + mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Verified contracts', Info: id, Source: 'App modal' }); + showContractList(ContractListTypes.VERIFIED); + }, [ showContractList, id ]); + + const showAnalyzedContracts = React.useCallback(() => { + showContractList(ContractListTypes.ANALYZED); + }, [ showContractList ]); const isMobile = useIsMobile(); const logoUrl = useColorModeValue(logo, logoDarkMode || logo); + function getHostname(url: string | undefined) { + try { + return new URL(url || '').hostname; + } catch (err) {} + } + return ( - @@ -117,29 +163,54 @@ const MarketplaceAppModal = ({ gridColumn={{ base: '1 / 3', sm: 2 }} marginTop={{ base: 6, sm: 0 }} > - - + + + - : - } - /> - + : + } + /> + + + { securityReport && ( + + + + { securityReport.overallInfo.totalContractsNumber } + + + { securityReport.overallInfo.verifiedNumber } + + + ) } + - + @@ -198,7 +269,7 @@ const MarketplaceAppModal = ({ overflow="hidden" textOverflow="ellipsis" > - { site } + { getHostname(site) } ) } @@ -228,6 +299,7 @@ const MarketplaceAppModal = ({ w="20px" h="20px" display="block" + color="text_secondary" /> )) } diff --git a/ui/marketplace/MarketplaceAppTopBar.tsx b/ui/marketplace/MarketplaceAppTopBar.tsx new file mode 100644 index 0000000000..f342b05866 --- /dev/null +++ b/ui/marketplace/MarketplaceAppTopBar.tsx @@ -0,0 +1,101 @@ +import { chakra, Flex, Tooltip, Skeleton, useBoolean } from '@chakra-ui/react'; +import React from 'react'; + +import type { MarketplaceAppOverview, MarketplaceAppSecurityReport } from 'types/client/marketplace'; +import { ContractListTypes } from 'types/client/marketplace'; + +import { route } from 'nextjs-routes'; + +import config from 'configs/app'; +import { useAppContext } from 'lib/contexts/app'; +import useIsMobile from 'lib/hooks/useIsMobile'; +import IconSvg from 'ui/shared/IconSvg'; +import LinkExternal from 'ui/shared/LinkExternal'; +import LinkInternal from 'ui/shared/LinkInternal'; +import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo'; +import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop'; +import WalletMenuDesktop from 'ui/snippets/walletMenu/WalletMenuDesktop'; + +import AppSecurityReport from './AppSecurityReport'; +import ContractListModal from './ContractListModal'; +import MarketplaceAppInfo from './MarketplaceAppInfo'; + +type Props = { + data: MarketplaceAppOverview | undefined; + isLoading: boolean; + securityReport?: MarketplaceAppSecurityReport; +} + +const MarketplaceAppTopBar = ({ data, isLoading, securityReport }: Props) => { + const [ showContractList, setShowContractList ] = useBoolean(false); + const appProps = useAppContext(); + const isMobile = useIsMobile(); + + const goBackUrl = React.useMemo(() => { + if (appProps.referrer && appProps.referrer.includes('/apps') && !appProps.referrer.includes('/apps/')) { + return appProps.referrer; + } + return route({ pathname: '/apps' }); + }, [ appProps.referrer ]); + + function getHostname(url: string | undefined) { + try { + return new URL(url || '').hostname; + } catch (err) {} + } + + return ( + <> + + { !isMobile && } + + + + + + + + { getHostname(data?.url) } + + + + + + { (securityReport || isLoading) && ( + + ) } + { !isMobile && ( + + { config.features.account.isEnabled && } + { config.features.blockchainInteraction.isEnabled && } + + ) } + + { showContractList && ( + + ) } + + ); +}; + +export default MarketplaceAppTopBar; diff --git a/ui/marketplace/MarketplaceCategoriesMenu.tsx b/ui/marketplace/MarketplaceCategoriesMenu.tsx new file mode 100644 index 0000000000..ad55bb7582 --- /dev/null +++ b/ui/marketplace/MarketplaceCategoriesMenu.tsx @@ -0,0 +1,70 @@ +import { Box, Button, Icon, Menu, MenuButton, MenuList, Skeleton } from '@chakra-ui/react'; +import React from 'react'; + +import { MarketplaceCategory } from 'types/client/marketplace'; + +import eastMiniArrowIcon from 'icons/arrows/east-mini.svg'; + +import MarketplaceCategoriesMenuItem from './MarketplaceCategoriesMenuItem'; + +type Props = { + categories: Array; + selectedCategoryId: string; + onSelect: (category: string) => void; + isLoading: boolean; +} + +const MarketplaceCategoriesMenu = ({ selectedCategoryId, onSelect, categories, isLoading }: Props) => { + const options = React.useMemo(() => ([ + MarketplaceCategory.FAVORITES, + MarketplaceCategory.ALL, + ...categories, + ]), [ categories ]); + + if (isLoading) { + return ( + + ); + } + + return ( + + + + { selectedCategoryId } + + + + + + { options.map((category: string) => ( + + )) } + + + ); +}; + +export default React.memo(MarketplaceCategoriesMenu); diff --git a/ui/marketplace/MarketplaceCategoriesMenuItem.tsx b/ui/marketplace/MarketplaceCategoriesMenuItem.tsx new file mode 100644 index 0000000000..b36374a39b --- /dev/null +++ b/ui/marketplace/MarketplaceCategoriesMenuItem.tsx @@ -0,0 +1,36 @@ +import { Icon, MenuItem } from '@chakra-ui/react'; +import type { FunctionComponent, SVGAttributes } from 'react'; +import React, { useCallback } from 'react'; + +import { MarketplaceCategory } from 'types/client/marketplace'; + +import starFilledIcon from 'icons/star_filled.svg'; + +type Props = { + id: string; + onClick: (category: string) => void; +} + +const ICONS: Record>> = { + [MarketplaceCategory.FAVORITES]: starFilledIcon, +}; + +const MarketplaceCategoriesMenuItem = ({ id, onClick }: Props) => { + const handleSelection = useCallback(() => { + onClick(id); + }, [ id, onClick ]); + + return ( + + { id in ICONS && } + { id } + + ); +}; + +export default MarketplaceCategoriesMenuItem; diff --git a/ui/marketplace/MarketplaceList.tsx b/ui/marketplace/MarketplaceList.tsx index 2d491383e2..ec4eea487a 100644 --- a/ui/marketplace/MarketplaceList.tsx +++ b/ui/marketplace/MarketplaceList.tsx @@ -1,26 +1,34 @@ import { Grid } from '@chakra-ui/react'; -import React from 'react'; +import React, { useCallback } from 'react'; +import type { MouseEvent } from 'react'; import type { MarketplaceAppPreview } from 'types/client/marketplace'; -import { MarketplaceCategory } from 'types/client/marketplace'; -import { apos } from 'lib/html-entities'; -import EmptySearchResult from 'ui/shared/EmptySearchResult'; -import IconSvg from 'ui/shared/IconSvg'; +import * as mixpanel from 'lib/mixpanel/index'; +import EmptySearchResult from './EmptySearchResult'; import MarketplaceAppCard from './MarketplaceAppCard'; type Props = { apps: Array; - onAppClick: (id: string) => void; + showAppInfo: (id: string) => void; favoriteApps: Array; - onFavoriteClick: (id: string, isFavorite: boolean) => void; + onFavoriteClick: (id: string, isFavorite: boolean, source: 'Discovery view') => void; isLoading: boolean; - showDisclaimer: (id: string) => void; selectedCategoryId?: string; + onAppClick: (event: MouseEvent, id: string) => void; } -const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick, isLoading, showDisclaimer, selectedCategoryId }: Props) => { +const MarketplaceList = ({ apps, showAppInfo, favoriteApps, onFavoriteClick, isLoading, selectedCategoryId, onAppClick }: Props) => { + const handleInfoClick = useCallback((id: string) => { + mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'More button', Info: id, Source: 'Discovery view' }); + showAppInfo(id); + }, [ showAppInfo ]); + + const handleFavoriteClick = useCallback((id: string, isFavorite: boolean) => { + onFavoriteClick(id, isFavorite, 'Discovery view'); + }, [ onFavoriteClick ]); + return apps.length > 0 ? ( ( )) } ) : ( - - You don{ apos }t have any favorite apps. - Click on the icon on the app{ apos }s card to add it to Favorites. - - ) : ( - `Couldn${ apos }t find an app that matches your filter query.` - ) - } - /> + ); }; diff --git a/ui/marketplace/MarketplaceListWithScores.tsx b/ui/marketplace/MarketplaceListWithScores.tsx new file mode 100644 index 0000000000..2ed58dafec --- /dev/null +++ b/ui/marketplace/MarketplaceListWithScores.tsx @@ -0,0 +1,86 @@ +import { Hide, Show } from '@chakra-ui/react'; +import React from 'react'; +import type { MouseEvent } from 'react'; + +import type { MarketplaceAppWithSecurityReport, ContractListTypes } from 'types/client/marketplace'; + +import DataListDisplay from 'ui/shared/DataListDisplay'; + +import EmptySearchResult from './EmptySearchResult'; +import ListItem from './MarketplaceListWithScores/ListItem'; +import Table from './MarketplaceListWithScores/Table'; + +interface Props { + apps: Array; + showAppInfo: (id: string) => void; + favoriteApps: Array; + onFavoriteClick: (id: string, isFavorite: boolean, source: 'Security view') => void; + isLoading: boolean; + selectedCategoryId?: string; + onAppClick: (event: MouseEvent, id: string) => void; + showContractList: (id: string, type: ContractListTypes) => void; +} + +const MarketplaceListWithScores = ({ + apps, + showAppInfo, + favoriteApps, + onFavoriteClick, + isLoading, + selectedCategoryId, + onAppClick, + showContractList, +}: Props) => { + + const displayedApps = React.useMemo(() => [ ...apps ].sort((a, b) => { + if (!a.securityReport) { + return 1; + } else if (!b.securityReport) { + return -1; + } + return b.securityReport.overallInfo.securityScore - a.securityReport.overallInfo.securityScore; + }), [ apps ]); + + const content = apps.length > 0 ? ( + <> + + { displayedApps.map((app, index) => ( + + )) } + + + + + + ) : null; + + return apps.length > 0 ? ( + + ) : ( + + ); +}; + +export default MarketplaceListWithScores; diff --git a/ui/marketplace/MarketplaceListWithScores/AppLink.tsx b/ui/marketplace/MarketplaceListWithScores/AppLink.tsx new file mode 100644 index 0000000000..bf1b1d6af7 --- /dev/null +++ b/ui/marketplace/MarketplaceListWithScores/AppLink.tsx @@ -0,0 +1,73 @@ +import { Flex, Skeleton, LinkBox, Image, useColorModeValue } from '@chakra-ui/react'; +import React from 'react'; +import type { MouseEvent } from 'react'; + +import type { MarketplaceAppPreview } from 'types/client/marketplace'; + +import MarketplaceAppCardLink from '../MarketplaceAppCardLink'; +import MarketplaceAppIntegrationIcon from '../MarketplaceAppIntegrationIcon'; + +interface Props { + app: MarketplaceAppPreview; + isLoading: boolean | undefined; + onAppClick: (event: MouseEvent, id: string) => void; + isLarge?: boolean; +} + +const AppLink = ({ app, isLoading, onAppClick, isLarge = false }: Props) => { + const { id, url, external, title, logo, logoDarkMode, internalWallet, categories } = app; + + const logoUrl = useColorModeValue(logo, logoDarkMode || logo); + + const categoriesLabel = categories.join(', '); + + return ( + + + + + + + + + + + + { categoriesLabel } + + + + ); +}; + +export default AppLink; diff --git a/ui/marketplace/MarketplaceListWithScores/ListItem.tsx b/ui/marketplace/MarketplaceListWithScores/ListItem.tsx new file mode 100644 index 0000000000..4b5a8988f4 --- /dev/null +++ b/ui/marketplace/MarketplaceListWithScores/ListItem.tsx @@ -0,0 +1,126 @@ +import { Flex, IconButton, Text } from '@chakra-ui/react'; +import React from 'react'; +import type { MouseEvent } from 'react'; + +import type { MarketplaceAppWithSecurityReport } from 'types/client/marketplace'; +import { ContractListTypes } from 'types/client/marketplace'; + +import * as mixpanel from 'lib/mixpanel/index'; +import IconSvg from 'ui/shared/IconSvg'; +import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; + +import AppSecurityReport from '../AppSecurityReport'; +import ContractListButton, { ContractListButtonVariants } from '../ContractListButton'; +import AppLink from './AppLink'; +import MoreInfoButton from './MoreInfoButton'; + +type Props = { + app: MarketplaceAppWithSecurityReport; + onInfoClick: (id: string) => void; + isFavorite: boolean; + onFavoriteClick: (id: string, isFavorite: boolean, source: 'Security view') => void; + isLoading: boolean; + onAppClick: (event: MouseEvent, id: string) => void; + showContractList: (id: string, type: ContractListTypes) => void; +} + +const ListItem = ({ app, onInfoClick, isFavorite, onFavoriteClick, isLoading, onAppClick, showContractList }: Props) => { + const { id, securityReport } = app; + + const handleInfoClick = React.useCallback((event: MouseEvent) => { + event.preventDefault(); + mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'More button', Info: id, Source: 'Security view' }); + onInfoClick(id); + }, [ onInfoClick, id ]); + + const handleFavoriteClick = React.useCallback(() => { + onFavoriteClick(id, isFavorite, 'Security view'); + }, [ onFavoriteClick, id, isFavorite ]); + + const showAllContracts = React.useCallback(() => { + mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Total contracts', Info: id, Source: 'Security view' }); + showContractList(id, ContractListTypes.ALL); + }, [ showContractList, id ]); + + const showVerifiedContracts = React.useCallback(() => { + mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Verified contracts', Info: id, Source: 'Security view' }); + showContractList(id, ContractListTypes.VERIFIED); + }, [ showContractList, id ]); + + const showAnalyzedContracts = React.useCallback(() => { + showContractList(id, ContractListTypes.ANALYZED); + }, [ showContractList, id ]); + + return ( + + + + + { !isLoading && ( + : + + } + /> + ) } + + + + { (securityReport || isLoading) ? ( + <> + + + { securityReport?.overallInfo.totalContractsNumber ?? 0 } + + + { securityReport?.overallInfo.verifiedNumber ?? 0 } + + + ) : ( + Data will be available soon + ) } + + + + + + ); +}; + +export default ListItem; diff --git a/ui/marketplace/MarketplaceListWithScores/MoreInfoButton.tsx b/ui/marketplace/MarketplaceListWithScores/MoreInfoButton.tsx new file mode 100644 index 0000000000..d96bddb66c --- /dev/null +++ b/ui/marketplace/MarketplaceListWithScores/MoreInfoButton.tsx @@ -0,0 +1,29 @@ +import { Link, Skeleton } from '@chakra-ui/react'; +import React from 'react'; +import type { MouseEvent } from 'react'; + +interface Props { + onClick: (event: MouseEvent) => void; + isLoading?: boolean; +} + +const MoreInfoButton = ({ onClick, isLoading }: Props) => ( + + + More info + + +); + +export default MoreInfoButton; diff --git a/ui/marketplace/MarketplaceListWithScores/Table.tsx b/ui/marketplace/MarketplaceListWithScores/Table.tsx new file mode 100644 index 0000000000..1e8cbddf56 --- /dev/null +++ b/ui/marketplace/MarketplaceListWithScores/Table.tsx @@ -0,0 +1,52 @@ +import { Table as ChakraTable, Tbody, Th, Tr } from '@chakra-ui/react'; +import React from 'react'; +import type { MouseEvent } from 'react'; + +import type { MarketplaceAppWithSecurityReport, ContractListTypes } from 'types/client/marketplace'; + +import { default as Thead } from 'ui/shared/TheadSticky'; + +import TableItem from './TableItem'; + +type Props = { + apps: Array; + isLoading?: boolean; + favoriteApps: Array; + onFavoriteClick: (id: string, isFavorite: boolean, source: 'Security view') => void; + onAppClick: (event: MouseEvent, id: string) => void; + onInfoClick: (id: string) => void; + showContractList: (id: string, type: ContractListTypes) => void; +} + +const Table = ({ apps, isLoading, favoriteApps, onFavoriteClick, onAppClick, onInfoClick, showContractList }: Props) => { + return ( + + + + + + + + + + + + + { apps.map((app, index) => ( + + )) } + + + ); +}; + +export default Table; diff --git a/ui/marketplace/MarketplaceListWithScores/TableItem.tsx b/ui/marketplace/MarketplaceListWithScores/TableItem.tsx new file mode 100644 index 0000000000..c77b5c5c78 --- /dev/null +++ b/ui/marketplace/MarketplaceListWithScores/TableItem.tsx @@ -0,0 +1,126 @@ +import { Td, Tr, IconButton, Skeleton, Text } from '@chakra-ui/react'; +import React from 'react'; +import type { MouseEvent } from 'react'; + +import type { MarketplaceAppWithSecurityReport } from 'types/client/marketplace'; +import { ContractListTypes } from 'types/client/marketplace'; + +import * as mixpanel from 'lib/mixpanel/index'; +import IconSvg from 'ui/shared/IconSvg'; + +import AppSecurityReport from '../AppSecurityReport'; +import ContractListButton, { ContractListButtonVariants } from '../ContractListButton'; +import AppLink from './AppLink'; +import MoreInfoButton from './MoreInfoButton'; + +type Props = { + app: MarketplaceAppWithSecurityReport; + isLoading?: boolean; + isFavorite: boolean; + onFavoriteClick: (id: string, isFavorite: boolean, source: 'Security view') => void; + onAppClick: (event: MouseEvent, id: string) => void; + onInfoClick: (id: string) => void; + showContractList: (id: string, type: ContractListTypes) => void; +} + +const TableItem = ({ + app, + isLoading, + isFavorite, + onFavoriteClick, + onAppClick, + onInfoClick, + showContractList, +}: Props) => { + + const { id, securityReport } = app; + + const handleInfoClick = React.useCallback((event: MouseEvent) => { + event.preventDefault(); + mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'More button', Info: id, Source: 'Security view' }); + onInfoClick(id); + }, [ onInfoClick, id ]); + + const handleFavoriteClick = React.useCallback(() => { + onFavoriteClick(id, isFavorite, 'Security view'); + }, [ onFavoriteClick, id, isFavorite ]); + + const showAllContracts = React.useCallback(() => { + mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Total contracts', Info: id, Source: 'Security view' }); + showContractList(id, ContractListTypes.ALL); + }, [ showContractList, id ]); + + const showVerifiedContracts = React.useCallback(() => { + mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Verified contracts', Info: id, Source: 'Security view' }); + showContractList(id, ContractListTypes.VERIFIED); + }, [ showContractList, id ]); + + const showAnalyzedContracts = React.useCallback(() => { + showContractList(id, ContractListTypes.ANALYZED); + }, [ showContractList, id ]); + + return ( + + + + { (securityReport || isLoading) ? ( + <> + + + + + ) : ( + + ) } + + + ); +}; + +export default TableItem; diff --git a/ui/marketplace/__screenshots__/MarketplaceAppInfo.pw.tsx_dark-color-mode_base-view-dark-mode-1.png b/ui/marketplace/__screenshots__/MarketplaceAppInfo.pw.tsx_dark-color-mode_base-view-dark-mode-1.png new file mode 100644 index 0000000000..99eb6c1d23 Binary files /dev/null and b/ui/marketplace/__screenshots__/MarketplaceAppInfo.pw.tsx_dark-color-mode_base-view-dark-mode-1.png differ diff --git a/ui/marketplace/__screenshots__/MarketplaceAppInfo.pw.tsx_default_base-view-dark-mode-1.png b/ui/marketplace/__screenshots__/MarketplaceAppInfo.pw.tsx_default_base-view-dark-mode-1.png new file mode 100644 index 0000000000..03091af10b Binary files /dev/null and b/ui/marketplace/__screenshots__/MarketplaceAppInfo.pw.tsx_default_base-view-dark-mode-1.png differ diff --git a/ui/marketplace/__screenshots__/MarketplaceAppInfo.pw.tsx_default_mobile-base-view-1.png b/ui/marketplace/__screenshots__/MarketplaceAppInfo.pw.tsx_default_mobile-base-view-1.png new file mode 100644 index 0000000000..7bb7f5597c Binary files /dev/null and b/ui/marketplace/__screenshots__/MarketplaceAppInfo.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_dark-color-mode_base-view-dark-mode-1.png b/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_dark-color-mode_base-view-dark-mode-1.png new file mode 100644 index 0000000000..d5fba4f1f9 Binary files /dev/null and b/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_dark-color-mode_base-view-dark-mode-1.png differ diff --git a/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_default_base-view-dark-mode-1.png b/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_default_base-view-dark-mode-1.png new file mode 100644 index 0000000000..25dffc6800 Binary files /dev/null and b/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_default_base-view-dark-mode-1.png differ diff --git a/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_default_mobile-base-view-1.png b/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_default_mobile-base-view-1.png new file mode 100644 index 0000000000..63ab8f1099 Binary files /dev/null and b/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/marketplace/useMarketplace.tsx b/ui/marketplace/useMarketplace.tsx index 80221c65a2..99ba708e3a 100644 --- a/ui/marketplace/useMarketplace.tsx +++ b/ui/marketplace/useMarketplace.tsx @@ -2,7 +2,8 @@ import _pickBy from 'lodash/pickBy'; import { useRouter } from 'next/router'; import React from 'react'; -import { MarketplaceCategory } from 'types/client/marketplace'; +import type { ContractListTypes } from 'types/client/marketplace'; +import { MarketplaceCategory, MarketplaceDisplayType } from 'types/client/marketplace'; import useDebounce from 'lib/hooks/useDebounce'; import * as mixpanel from 'lib/mixpanel/index'; @@ -25,16 +26,25 @@ export default function useMarketplace() { const router = useRouter(); const defaultCategoryId = getQueryParamString(router.query.category); const defaultFilterQuery = getQueryParamString(router.query.filter); + const defaultDisplayType = getQueryParamString(router.query.tab); const [ selectedAppId, setSelectedAppId ] = React.useState(null); const [ selectedCategoryId, setSelectedCategoryId ] = React.useState(MarketplaceCategory.ALL); + const [ selectedDisplayType, setSelectedDisplayType ] = React.useState( + Object.values(MarketplaceDisplayType).includes(defaultDisplayType as MarketplaceDisplayType) ? + defaultDisplayType : + MarketplaceDisplayType.DEFAULT, + ); const [ filterQuery, setFilterQuery ] = React.useState(defaultFilterQuery); const [ favoriteApps, setFavoriteApps ] = React.useState>([]); + const [ isFavoriteAppsLoaded, setIsFavoriteAppsLoaded ] = React.useState(false); const [ isAppInfoModalOpen, setIsAppInfoModalOpen ] = React.useState(false); const [ isDisclaimerModalOpen, setIsDisclaimerModalOpen ] = React.useState(false); + const [ contractListModalType, setContractListModalType ] = React.useState(null); + const [ hasPreviousStep, setHasPreviousStep ] = React.useState(false); - const handleFavoriteClick = React.useCallback((id: string, isFavorite: boolean) => { - mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Favorite app', Info: id }); + const handleFavoriteClick = React.useCallback((id: string, isFavorite: boolean, source: 'Discovery view' | 'Security view' | 'App modal' | 'Banner') => { + mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Favorite app', Info: id, Source: source }); const favoriteApps = getFavoriteApps(); @@ -59,11 +69,21 @@ export default function useMarketplace() { setIsDisclaimerModalOpen(true); }, []); + const showContractList = React.useCallback((id: string, type: ContractListTypes, hasPreviousStep?: boolean) => { + setSelectedAppId(id); + setContractListModalType(type); + if (hasPreviousStep) { + setHasPreviousStep(true); + } + }, []); + const debouncedFilterQuery = useDebounce(filterQuery, 500); const clearSelectedAppId = React.useCallback(() => { setSelectedAppId(null); setIsAppInfoModalOpen(false); setIsDisclaimerModalOpen(false); + setContractListModalType(null); + setHasPreviousStep(false); }, []); const handleCategoryChange = React.useCallback((newCategory: string) => { @@ -71,11 +91,20 @@ export default function useMarketplace() { setSelectedCategoryId(newCategory); }, []); - const { isPlaceholderData, isError, error, data, displayedApps } = useMarketplaceApps(debouncedFilterQuery, selectedCategoryId, favoriteApps); - const { isPlaceholderData: isCategoriesPlaceholderData, data: categories } = useMarketplaceCategories(data, isPlaceholderData); + const handleDisplayTypeChange = React.useCallback((newDisplayType: MarketplaceDisplayType) => { + setSelectedDisplayType(newDisplayType); + }, []); + + const { + isPlaceholderData, isError, error, data, displayedApps, + } = useMarketplaceApps(debouncedFilterQuery, selectedCategoryId, favoriteApps, isFavoriteAppsLoaded); + const { + isPlaceholderData: isCategoriesPlaceholderData, data: categories, + } = useMarketplaceCategories(data, isPlaceholderData); React.useEffect(() => { setFavoriteApps(getFavoriteApps()); + setIsFavoriteAppsLoaded(true); }, [ ]); React.useEffect(() => { @@ -91,6 +120,7 @@ export default function useMarketplace() { const query = _pickBy({ category: selectedCategoryId === MarketplaceCategory.ALL ? undefined : selectedCategoryId, filter: debouncedFilterQuery, + tab: selectedDisplayType === MarketplaceDisplayType.DEFAULT ? undefined : selectedDisplayType, }, Boolean); if (debouncedFilterQuery.length > 0) { @@ -105,7 +135,7 @@ export default function useMarketplace() { // omit router in the deps because router.push() somehow modifies it // and we get infinite re-renders then // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ debouncedFilterQuery, selectedCategoryId ]); + }, [ debouncedFilterQuery, selectedCategoryId, selectedDisplayType ]); return React.useMemo(() => ({ selectedCategoryId, @@ -116,6 +146,7 @@ export default function useMarketplace() { isError, error, categories, + apps: data, displayedApps, showAppInfo, selectedAppId, @@ -127,11 +158,17 @@ export default function useMarketplace() { showDisclaimer, appsTotal: data?.length || 0, isCategoriesPlaceholderData, + showContractList, + contractListModalType, + selectedDisplayType, + onDisplayTypeChange: handleDisplayTypeChange, + hasPreviousStep, }), [ selectedCategoryId, categories, clearSelectedAppId, selectedAppId, + data, displayedApps, error, favoriteApps, @@ -144,7 +181,11 @@ export default function useMarketplace() { isAppInfoModalOpen, isDisclaimerModalOpen, showDisclaimer, - data?.length, isCategoriesPlaceholderData, + showContractList, + contractListModalType, + selectedDisplayType, + handleDisplayTypeChange, + hasPreviousStep, ]); } diff --git a/ui/marketplace/useMarketplaceApps.tsx b/ui/marketplace/useMarketplaceApps.tsx index 360b5f0bca..96f09f379e 100644 --- a/ui/marketplace/useMarketplaceApps.tsx +++ b/ui/marketplace/useMarketplaceApps.tsx @@ -1,7 +1,7 @@ import { useQuery } from '@tanstack/react-query'; import React from 'react'; -import type { MarketplaceAppOverview } from 'types/client/marketplace'; +import type { MarketplaceAppWithSecurityReport } from 'types/client/marketplace'; import { MarketplaceCategory } from 'types/client/marketplace'; import config from 'configs/app'; @@ -10,19 +10,21 @@ import useApiFetch from 'lib/api/useApiFetch'; import useFetch from 'lib/hooks/useFetch'; import { MARKETPLACE_APP } from 'stubs/marketplace'; +import useSecurityReports from './useSecurityReports'; + const feature = config.features.marketplace; -function isAppNameMatches(q: string, app: MarketplaceAppOverview) { +function isAppNameMatches(q: string, app: MarketplaceAppWithSecurityReport) { return app.title.toLowerCase().includes(q.toLowerCase()); } -function isAppCategoryMatches(category: string, app: MarketplaceAppOverview, favoriteApps: Array) { +function isAppCategoryMatches(category: string, app: MarketplaceAppWithSecurityReport, favoriteApps: Array = []) { return category === MarketplaceCategory.ALL || (category === MarketplaceCategory.FAVORITES && favoriteApps.includes(app.id)) || app.categories.includes(category); } -function sortApps(apps: Array, favoriteApps: Array) { +function sortApps(apps: Array, favoriteApps: Array = []) { return apps.sort((a, b) => { const priorityA = a.priority || 0; const priorityB = b.priority || 0; @@ -47,48 +49,65 @@ function sortApps(apps: Array, favoriteApps: Array = []) { +export default function useMarketplaceApps( + filter: string, + selectedCategoryId: string = MarketplaceCategory.ALL, + favoriteApps: Array | undefined = undefined, + isFavoriteAppsLoaded: boolean = false, // eslint-disable-line @typescript-eslint/no-inferrable-types +) { const fetch = useFetch(); const apiFetch = useApiFetch(); - // Update favorite apps only when selectedCategoryId changes to avoid sortApps to be called on each favorite app click - const lastFavoriteAppsRef = React.useRef(favoriteApps); + const { data: securityReports, isPlaceholderData: isSecurityReportsPlaceholderData } = useSecurityReports(); + + // Set the value only 1 time to avoid unnecessary useQuery calls and re-rendering of all applications + const [ snapshotFavoriteApps, setSnapshotFavoriteApps ] = React.useState | undefined>(); + const isInitialSetup = React.useRef(true); + React.useEffect(() => { - lastFavoriteAppsRef.current = favoriteApps; - }, [ selectedCategoryId ]); // eslint-disable-line react-hooks/exhaustive-deps + if (isInitialSetup.current && (isFavoriteAppsLoaded || favoriteApps === undefined)) { + setSnapshotFavoriteApps(favoriteApps || []); + isInitialSetup.current = false; + } + }, [ isFavoriteAppsLoaded, favoriteApps ]); - const { isPlaceholderData, isError, error, data } = useQuery, Array>({ - queryKey: [ 'marketplace-dapps' ], + const { isPlaceholderData, isError, error, data } = useQuery, Array>({ + queryKey: [ 'marketplace-dapps', snapshotFavoriteApps ], queryFn: async() => { if (!feature.isEnabled) { return []; } else if ('configUrl' in feature) { - return fetch, unknown>(feature.configUrl, undefined, { resource: 'marketplace-dapps' }); + return fetch, unknown>(feature.configUrl, undefined, { resource: 'marketplace-dapps' }); } else { return apiFetch('marketplace_dapps', { pathParams: { chainId: config.chain.id } }); } }, - select: (data) => sortApps(data as Array, lastFavoriteAppsRef.current), + select: (data) => sortApps(data as Array, snapshotFavoriteApps), placeholderData: feature.isEnabled ? Array(9).fill(MARKETPLACE_APP) : undefined, staleTime: Infinity, - enabled: feature.isEnabled, + enabled: feature.isEnabled && Boolean(snapshotFavoriteApps), }); + const appsWithSecurityReports = React.useMemo(() => + data?.map((app) => ({ ...app, securityReport: securityReports?.[app.id] })), + [ data, securityReports ]); + const displayedApps = React.useMemo(() => { - return data?.filter(app => isAppNameMatches(filter, app) && isAppCategoryMatches(selectedCategoryId, app, favoriteApps)) || []; - }, [ selectedCategoryId, data, filter, favoriteApps ]); + return appsWithSecurityReports?.filter(app => isAppNameMatches(filter, app) && isAppCategoryMatches(selectedCategoryId, app, favoriteApps)) || []; + }, [ selectedCategoryId, appsWithSecurityReports, filter, favoriteApps ]); return React.useMemo(() => ({ data, displayedApps, error, isError, - isPlaceholderData, + isPlaceholderData: isPlaceholderData || isSecurityReportsPlaceholderData, }), [ data, displayedApps, error, isError, isPlaceholderData, + isSecurityReportsPlaceholderData, ]); } diff --git a/ui/marketplace/useMarketplaceWallet.tsx b/ui/marketplace/useMarketplaceWallet.tsx index 17370ea6bd..24e2f6858c 100644 --- a/ui/marketplace/useMarketplaceWallet.tsx +++ b/ui/marketplace/useMarketplaceWallet.tsx @@ -1,14 +1,15 @@ import type { TypedData } from 'abitype'; import { useCallback } from 'react'; import type { Account, SignTypedDataParameters } from 'viem'; -import { useAccount, useSendTransaction, useSwitchNetwork, useNetwork, useSignMessage, useSignTypedData } from 'wagmi'; +import { useAccount, useSendTransaction, useSwitchChain, useSignMessage, useSignTypedData } from 'wagmi'; import config from 'configs/app'; +import * as mixpanel from 'lib/mixpanel/index'; type SendTransactionArgs = { chainId?: number; mode?: 'prepared'; - to: string; + to: `0x${ string }`; }; export type SignTypedDataArgs< @@ -20,31 +21,39 @@ export type SignTypedDataArgs< TPrimaryType extends string = string, > = SignTypedDataParameters; -export default function useMarketplaceWallet() { - const { address } = useAccount(); - const { chain } = useNetwork(); +export default function useMarketplaceWallet(appId: string) { + const { address, chainId } = useAccount(); const { sendTransactionAsync } = useSendTransaction(); const { signMessageAsync } = useSignMessage(); const { signTypedDataAsync } = useSignTypedData(); - const { switchNetworkAsync } = useSwitchNetwork({ chainId: Number(config.chain.id) }); + const { switchChainAsync } = useSwitchChain(); + + const logEvent = useCallback((event: mixpanel.EventPayload['Action']) => { + mixpanel.logEvent( + mixpanel.EventTypes.WALLET_ACTION, + { Action: event, Address: address, AppId: appId }, + ); + }, [ address, appId ]); const switchNetwork = useCallback(async() => { - if (Number(config.chain.id) !== chain?.id) { - await switchNetworkAsync?.(); + if (Number(config.chain.id) !== chainId) { + await switchChainAsync?.({ chainId: Number(config.chain.id) }); } - }, [ chain, switchNetworkAsync ]); + }, [ chainId, switchChainAsync ]); const sendTransaction = useCallback(async(transaction: SendTransactionArgs) => { await switchNetwork(); const tx = await sendTransactionAsync(transaction); - return tx.hash; - }, [ sendTransactionAsync, switchNetwork ]); + logEvent('Send Transaction'); + return tx; + }, [ sendTransactionAsync, switchNetwork, logEvent ]); const signMessage = useCallback(async(message: string) => { await switchNetwork(); const signature = await signMessageAsync({ message }); + logEvent('Sign Message'); return signature; - }, [ signMessageAsync, switchNetwork ]); + }, [ signMessageAsync, switchNetwork, logEvent ]); const signTypedData = useCallback(async(typedData: SignTypedDataArgs) => { await switchNetwork(); @@ -52,8 +61,9 @@ export default function useMarketplaceWallet() { typedData.domain.chainId = Number(typedData.domain.chainId); } const signature = await signTypedDataAsync(typedData); + logEvent('Sign Typed Data'); return signature; - }, [ signTypedDataAsync, switchNetwork ]); + }, [ signTypedDataAsync, switchNetwork, logEvent ]); return { address, diff --git a/ui/marketplace/useSecurityReports.tsx b/ui/marketplace/useSecurityReports.tsx new file mode 100644 index 0000000000..c15aef2d8a --- /dev/null +++ b/ui/marketplace/useSecurityReports.tsx @@ -0,0 +1,35 @@ +import { useQuery } from '@tanstack/react-query'; + +import type { MarketplaceAppSecurityReport, MarketplaceAppSecurityReportRaw } from 'types/client/marketplace'; + +import config from 'configs/app'; +import type { ResourceError } from 'lib/api/resources'; +import useApiFetch from 'lib/hooks/useFetch'; + +const feature = config.features.marketplace; +const securityReportsUrl = (feature.isEnabled && feature.securityReportsUrl) || ''; + +export default function useSecurityReports() { + const apiFetch = useApiFetch(); + + return useQuery, Record>({ + queryKey: [ 'marketplace-security-reports' ], + queryFn: async() => apiFetch(securityReportsUrl, undefined, { resource: 'marketplace-security-reports' }), + select: (data) => { + const securityReports: Record = {}; + (data as Array).forEach((item) => { + const report = item.chainsData[config.chain.id || '']; + if (report) { + const issues: Record = report.overallInfo.issueSeverityDistribution; + report.overallInfo.totalIssues = Object.values(issues).reduce((acc, val) => acc + val, 0); + report.overallInfo.securityScore = Number(report.overallInfo.securityScore.toFixed(2)); + } + securityReports[item.appName] = report; + }); + return securityReports; + }, + placeholderData: securityReportsUrl ? {} : undefined, + staleTime: Infinity, + enabled: Boolean(securityReportsUrl), + }); +} diff --git a/ui/pages/Address.pw.tsx b/ui/pages/Address.pw.tsx new file mode 100644 index 0000000000..b58cdd7841 --- /dev/null +++ b/ui/pages/Address.pw.tsx @@ -0,0 +1,33 @@ +import React from 'react'; + +import * as addressMock from 'mocks/address/address'; +import * as addressCountersMock from 'mocks/address/counters'; +import * as addressTabCountersMock from 'mocks/address/tabCounters'; +import * as socketServer from 'playwright/fixtures/socketServer'; +import { test, expect } from 'playwright/lib'; + +import Address from './Address'; + +const hooksConfig = { + router: { + query: { hash: addressMock.hash }, + }, +}; + +test.describe('fetched bytecode', () => { + test('should refetch address query', async({ render, mockApiResponse, createSocket, page }) => { + const addressApiUrl = await mockApiResponse('address', addressMock.validator, { pathParams: { hash: addressMock.hash } }); + await mockApiResponse('address_counters', addressCountersMock.forValidator, { pathParams: { hash: addressMock.hash } }); + await mockApiResponse('address_tabs_counters', addressTabCountersMock.base, { pathParams: { hash: addressMock.hash } }); + await mockApiResponse('address_txs', { items: [], next_page_params: null }, { pathParams: { hash: addressMock.hash } }); + await render(
, { hooksConfig }, { withSocket: true }); + + const socket = await createSocket(); + const channel = await socketServer.joinChannel(socket, `addresses:${ addressMock.hash.toLowerCase() }`); + socketServer.sendMessage(socket, channel, 'fetched_bytecode', { fetched_bytecode: '0x0123' }); + + const request = await page.waitForRequest(addressApiUrl); + + expect(request).toBeTruthy(); + }); +}); diff --git a/ui/pages/Address.tsx b/ui/pages/Address.tsx index 2043a5d80f..6d4c829a96 100644 --- a/ui/pages/Address.tsx +++ b/ui/pages/Address.tsx @@ -1,17 +1,22 @@ -import { Box, Flex, HStack } from '@chakra-ui/react'; +import { Box, Flex, HStack, useColorModeValue } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import React from 'react'; +import type { EntityTag } from 'ui/shared/EntityTags/types'; import type { RoutedTab } from 'ui/shared/Tabs/types'; import config from 'configs/app'; +import useAddressMetadataInfoQuery from 'lib/address/useAddressMetadataInfoQuery'; import useApiQuery from 'lib/api/useApiQuery'; import { useAppContext } from 'lib/contexts/app'; import useContractTabs from 'lib/hooks/useContractTabs'; import useIsSafeAddress from 'lib/hooks/useIsSafeAddress'; import getQueryParamString from 'lib/router/getQueryParamString'; +import useSocketChannel from 'lib/socket/useSocketChannel'; +import useSocketMessage from 'lib/socket/useSocketMessage'; import { ADDRESS_TABS_COUNTERS } from 'stubs/address'; import { USER_OPS_ACCOUNT } from 'stubs/userOps'; +import AddressAccountHistory from 'ui/address/AddressAccountHistory'; import AddressBlocksValidated from 'ui/address/AddressBlocksValidated'; import AddressCoinBalance from 'ui/address/AddressCoinBalance'; import AddressContract from 'ui/address/AddressContract'; @@ -33,15 +38,18 @@ import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu' import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet'; import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import EnsEntity from 'ui/shared/entities/ens/EnsEntity'; -import EntityTags from 'ui/shared/EntityTags'; +import EntityTags from 'ui/shared/EntityTags/EntityTags'; +import formatUserTags from 'ui/shared/EntityTags/formatUserTags'; +import sortEntityTags from 'ui/shared/EntityTags/sortEntityTags'; import IconSvg from 'ui/shared/IconSvg'; import NetworkExplorers from 'ui/shared/NetworkExplorers'; import PageTitle from 'ui/shared/Page/PageTitle'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; -import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton'; const TOKEN_TABS = [ 'tokens_erc20', 'tokens_nfts', 'tokens_nfts_collection', 'tokens_nfts_list' ]; +const txInterpretation = config.features.txInterpretation; + const AddressPageContent = () => { const router = useRouter(); const appProps = useAppContext(); @@ -67,9 +75,30 @@ const AddressPageContent = () => { }, }); + const addressesForMetadataQuery = React.useMemo(() => ([ hash ].filter(Boolean)), [ hash ]); + const addressMetadataQuery = useAddressMetadataInfoQuery(addressesForMetadataQuery); + + const isLoading = addressQuery.isPlaceholderData || (config.features.userOps.isEnabled && userOpsAccountQuery.isPlaceholderData); + const isTabsLoading = isLoading || addressTabsCountersQuery.isPlaceholderData; + + const handleFetchedBytecodeMessage = React.useCallback(() => { + addressQuery.refetch(); + }, [ addressQuery ]); + + const channel = useSocketChannel({ + topic: `addresses:${ hash?.toLowerCase() }`, + isDisabled: isTabsLoading || addressQuery.isDegradedData || Boolean(addressQuery.data?.is_contract), + }); + useSocketMessage({ + channel, + event: 'fetched_bytecode', + handler: handleFetchedBytecodeMessage, + }); + const isSafeAddress = useIsSafeAddress(!addressQuery.isPlaceholderData && addressQuery.data?.is_contract ? hash : undefined); + const safeIconColor = useColorModeValue('black', 'white'); - const contractTabs = useContractTabs(addressQuery.data); + const contractTabs = useContractTabs(addressQuery.data, addressQuery.isPlaceholderData); const tabs: Array = React.useMemo(() => { return [ @@ -77,14 +106,21 @@ const AddressPageContent = () => { id: 'txs', title: 'Transactions', count: addressTabsCountersQuery.data?.transactions_count, - component: , + component: , }, + txInterpretation.isEnabled && txInterpretation.provider === 'noves' ? + { + id: 'account_history', + title: 'Account history', + component: , + } : + undefined, config.features.userOps.isEnabled && Boolean(userOpsAccountQuery.data?.total_ops) ? { id: 'user_ops', title: 'User operations', count: userOpsAccountQuery.data?.total_ops, - component: , + component: , } : undefined, config.features.beaconChain.isEnabled && addressTabsCountersQuery.data?.withdrawals_count ? @@ -92,39 +128,39 @@ const AddressPageContent = () => { id: 'withdrawals', title: 'Withdrawals', count: addressTabsCountersQuery.data?.withdrawals_count, - component: , + component: , } : undefined, { id: 'token_transfers', title: 'Token transfers', count: addressTabsCountersQuery.data?.token_transfers_count, - component: , + component: , }, { id: 'tokens', title: 'Tokens', count: addressTabsCountersQuery.data?.token_balances_count, - component: , + component: , subTabs: TOKEN_TABS, }, { id: 'internal_txns', title: 'Internal txns', count: addressTabsCountersQuery.data?.internal_txs_count, - component: , + component: , }, { id: 'coin_balance_history', title: 'Coin balance history', - component: , + component: , }, config.chain.verificationType === 'validation' && addressTabsCountersQuery.data?.validations_count ? { id: 'blocks_validated', title: 'Blocks validated', count: addressTabsCountersQuery.data?.validations_count, - component: , + component: , } : undefined, addressTabsCountersQuery.data?.logs_count ? @@ -132,9 +168,10 @@ const AddressPageContent = () => { id: 'logs', title: 'Logs', count: addressTabsCountersQuery.data?.logs_count, - component: , + component: , } : undefined, + addressQuery.data?.is_contract ? { id: 'contract', title: () => { @@ -149,30 +186,39 @@ const AddressPageContent = () => { return 'Contract'; }, - component: , - subTabs: contractTabs.map(tab => tab.id), + component: , + subTabs: contractTabs.tabs.map(tab => tab.id), } : undefined, ].filter(Boolean); - }, [ addressQuery.data, contractTabs, addressTabsCountersQuery.data, userOpsAccountQuery.data ]); + }, [ addressQuery.data, contractTabs, addressTabsCountersQuery.data, userOpsAccountQuery.data, isTabsLoading ]); - const isLoading = addressQuery.isPlaceholderData || (config.features.userOps.isEnabled && userOpsAccountQuery.isPlaceholderData); + const tags: Array = React.useMemo(() => { + return [ + !addressQuery.data?.is_contract ? { slug: 'eoa', name: 'EOA', tagType: 'custom' as const, ordinal: -1 } : undefined, + config.features.validators.isEnabled && addressQuery.data?.has_validated_blocks ? + { slug: 'validator', name: 'Validator', tagType: 'custom' as const, ordinal: 10 } : + undefined, + addressQuery.data?.implementation_address ? { slug: 'proxy', name: 'Proxy', tagType: 'custom' as const, ordinal: -1 } : undefined, + addressQuery.data?.token ? { slug: 'token', name: 'Token', tagType: 'custom' as const, ordinal: -1 } : undefined, + isSafeAddress ? { slug: 'safe', name: 'Multisig: Safe', tagType: 'custom' as const, ordinal: -10 } : undefined, + config.features.userOps.isEnabled && userOpsAccountQuery.data ? + { slug: 'user_ops_acc', name: 'Smart contract wallet', tagType: 'custom' as const, ordinal: -10 } : + undefined, + ...formatUserTags(addressQuery.data), + ...(addressMetadataQuery.data?.addresses?.[hash.toLowerCase()]?.tags || []), + ].filter(Boolean).sort(sortEntityTags); + }, [ addressMetadataQuery.data, addressQuery.data, hash, isSafeAddress, userOpsAccountQuery.data ]); - const tags = ( + const titleContentAfter = ( ); - const content = (addressQuery.isError || addressQuery.isDegradedData) ? null : ; + const content = (addressQuery.isError || addressQuery.isDegradedData) ? + null : + ; const backLink = React.useMemo(() => { const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/accounts'); @@ -207,6 +253,7 @@ const AddressPageContent = () => { fontWeight={ 500 } noLink isSafeAddress={ isSafeAddress } + iconColor={ isSafeAddress ? safeIconColor : undefined } mr={ 4 } /> { !isLoading && addressQuery.data?.is_contract && addressQuery.data.token && @@ -229,17 +276,15 @@ const AddressPageContent = () => { + { config.features.metasuites.isEnabled && } { /* should stay before tabs to scroll up with pagination */ } - { (isLoading || addressTabsCountersQuery.isPlaceholderData) ? - : - content - } + { content } ); }; diff --git a/ui/pages/BeaconChainWithdrawals.pw.tsx b/ui/pages/BeaconChainWithdrawals.pw.tsx index ab4cc8fa7a..a445ee1720 100644 --- a/ui/pages/BeaconChainWithdrawals.pw.tsx +++ b/ui/pages/BeaconChainWithdrawals.pw.tsx @@ -1,43 +1,16 @@ -import { test as base, expect } from '@playwright/experimental-ct-react'; import React from 'react'; import { data as withdrawalsData } from 'mocks/withdrawals/withdrawals'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; -import buildApiUrl from 'playwright/utils/buildApiUrl'; -import * as configs from 'playwright/utils/configs'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import { test, expect } from 'playwright/lib'; import BeaconChainWithdrawals from './BeaconChainWithdrawals'; -const test = base.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.featureEnvs.beaconChain) as any, -}); - -const WITHDRAWALS_API_URL = buildApiUrl('withdrawals'); -const WITHDRAWALS_COUNTERS_API_URL = buildApiUrl('withdrawals_counters'); - -test('base view +@mobile', async({ mount, page }) => { - await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ - status: 200, - body: '', - })); - - await page.route(WITHDRAWALS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(withdrawalsData), - })); - - await page.route(WITHDRAWALS_COUNTERS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify({ withdrawal_count: '111111', withdrawal_sum: '1010101010110101001101010' }), - })); - - const component = await mount( - - - , - ); - +test('base view +@mobile', async({ render, mockEnvs, mockTextAd, mockApiResponse }) => { + await mockEnvs(ENVS_MAP.beaconChain); + await mockTextAd(); + await mockApiResponse('withdrawals', withdrawalsData); + await mockApiResponse('withdrawals_counters', { withdrawal_count: '111111', withdrawal_sum: '1010101010110101001101010' }); + const component = await render(); await expect(component).toHaveScreenshot(); }); diff --git a/ui/pages/Blob.pw.tsx b/ui/pages/Blob.pw.tsx new file mode 100644 index 0000000000..1f0f56227e --- /dev/null +++ b/ui/pages/Blob.pw.tsx @@ -0,0 +1,68 @@ +import { test, expect } from '@playwright/experimental-ct-react'; +import React from 'react'; + +import * as textAdMock from 'mocks/ad/textAd'; +import * as blobsMock from 'mocks/blobs/blobs'; +import TestApp from 'playwright/TestApp'; +import buildApiUrl from 'playwright/utils/buildApiUrl'; +import * as configs from 'playwright/utils/configs'; + +import Blob from './Blob'; + +const BLOB_API_URL = buildApiUrl('blob', { hash: blobsMock.base1.hash }); +const hooksConfig = { + router: { + query: { hash: blobsMock.base1.hash }, + }, +}; + +test.beforeEach(async({ page }) => { + await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ + status: 200, + body: JSON.stringify(textAdMock.duck), + })); + await page.route(textAdMock.duck.ad.thumbnail, (route) => { + return route.fulfill({ + status: 200, + path: './playwright/mocks/image_s.jpg', + }); + }); +}); + +test('base view +@mobile +@dark-mode', async({ mount, page }) => { + await page.route(BLOB_API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(blobsMock.base1), + })); + + const component = await mount( + + + , + { hooksConfig }, + ); + + await expect(component).toHaveScreenshot({ + mask: [ page.locator(configs.adsBannerSelector) ], + maskColor: configs.maskColor, + }); +}); + +test('without data', async({ mount, page }) => { + await page.route(BLOB_API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(blobsMock.withoutData), + })); + + const component = await mount( + + + , + { hooksConfig }, + ); + + await expect(component).toHaveScreenshot({ + mask: [ page.locator(configs.adsBannerSelector) ], + maskColor: configs.maskColor, + }); +}); diff --git a/ui/pages/Blob.tsx b/ui/pages/Blob.tsx new file mode 100644 index 0000000000..4df70d1ba9 --- /dev/null +++ b/ui/pages/Blob.tsx @@ -0,0 +1,59 @@ +import { useRouter } from 'next/router'; +import React from 'react'; + +import useApiQuery from 'lib/api/useApiQuery'; +import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import { BLOB } from 'stubs/blobs'; +import BlobInfo from 'ui/blob/BlobInfo'; +import TextAd from 'ui/shared/ad/TextAd'; +import isCustomAppError from 'ui/shared/AppError/isCustomAppError'; +import DataFetchAlert from 'ui/shared/DataFetchAlert'; +import BlobEntity from 'ui/shared/entities/blob/BlobEntity'; +import PageTitle from 'ui/shared/Page/PageTitle'; + +const BlobPageContent = () => { + const router = useRouter(); + const hash = getQueryParamString(router.query.hash); + + const { data, isPlaceholderData, isError, error } = useApiQuery('blob', { + pathParams: { hash }, + queryOptions: { + placeholderData: BLOB, + refetchOnMount: false, + }, + }); + + const content = (() => { + if (isError) { + if (isCustomAppError(error)) { + throwOnResourceLoadError({ resource: 'blob', error, isError: true }); + } + + return ; + } + + if (!data) { + return null; + } + + return ; + })(); + + const titleSecondRow = ( + + ); + + return ( + <> + + + { content } + + ); +}; + +export default BlobPageContent; diff --git a/ui/pages/Block.tsx b/ui/pages/Block.tsx index 00adcf4af2..6cb3408860 100644 --- a/ui/pages/Block.tsx +++ b/ui/pages/Block.tsx @@ -13,8 +13,9 @@ import useIsMobile from 'lib/hooks/useIsMobile'; import getQueryParamString from 'lib/router/getQueryParamString'; import BlockDetails from 'ui/block/BlockDetails'; import BlockWithdrawals from 'ui/block/BlockWithdrawals'; +import useBlockBlobTxsQuery from 'ui/block/useBlockBlobTxsQuery'; import useBlockQuery from 'ui/block/useBlockQuery'; -import useBlockTxQuery from 'ui/block/useBlockTxQuery'; +import useBlockTxsQuery from 'ui/block/useBlockTxsQuery'; import useBlockWithdrawalsQuery from 'ui/block/useBlockWithdrawalsQuery'; //import TextAd from 'ui/shared/ad/TextAd'; import ServiceDegradationWarning from 'ui/shared/alerts/ServiceDegradationWarning'; @@ -40,8 +41,9 @@ const BlockPageContent = () => { const tab = getQueryParamString(router.query.tab); const blockQuery = useBlockQuery({ heightOrHash }); - const blockTxsQuery = useBlockTxQuery({ heightOrHash, blockQuery, tab }); + const blockTxsQuery = useBlockTxsQuery({ heightOrHash, blockQuery, tab }); const blockWithdrawalsQuery = useBlockWithdrawalsQuery({ heightOrHash, blockQuery, tab }); + const blockBlobTxsQuery = useBlockBlobTxsQuery({ heightOrHash, blockQuery, tab }); const tabs: Array = React.useMemo(() => ([ { @@ -64,6 +66,14 @@ const BlockPageContent = () => { ), }, + config.features.dataAvailability.isEnabled && blockQuery.data?.blob_tx_count ? + { + id: 'blob_txs', + title: 'Blob txns', + component: ( + + ), + } : null, config.features.beaconChain.isEnabled && Boolean(blockQuery.data?.withdrawals_count) ? { id: 'withdrawals', @@ -75,7 +85,7 @@ const BlockPageContent = () => { ), } : null, - ].filter(Boolean)), [ blockQuery, blockTxsQuery, blockWithdrawalsQuery ]); + ].filter(Boolean)), [ blockBlobTxsQuery, blockQuery, blockTxsQuery, blockWithdrawalsQuery ]); const hasPagination = !isMobile && ( (tab === 'txs' && blockTxsQuery.pagination.isVisible) || diff --git a/ui/pages/Blocks.pw.tsx b/ui/pages/Blocks.pw.tsx index 5b3776045c..2a334c3b21 100644 --- a/ui/pages/Blocks.pw.tsx +++ b/ui/pages/Blocks.pw.tsx @@ -1,19 +1,13 @@ -import { test as base, expect, devices } from '@playwright/experimental-ct-react'; import React from 'react'; -import * as textAdMock from 'mocks/ad/textAd'; import * as blockMock from 'mocks/blocks/block'; import * as statsMock from 'mocks/stats/index'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; 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 { test, expect, devices } from 'playwright/lib'; import Blocks from './Blocks'; -const BLOCKS_API_URL = buildApiUrl('blocks') + '?type=block'; -const STATS_API_URL = buildApiUrl('stats'); const hooksConfig = { router: { query: { tab: 'blocks' }, @@ -21,70 +15,29 @@ const hooksConfig = { }, }; -const test = base.extend({ - createSocket: socketServer.createSocket, -}); - // FIXME // test cases which use socket cannot run in parallel since the socket server always run on the same port test.describe.configure({ mode: 'serial' }); -test.beforeEach(async({ page }) => { - await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ - status: 200, - body: JSON.stringify(textAdMock.duck), - })); - await page.route(textAdMock.duck.ad.thumbnail, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); +test.beforeEach(async({ mockTextAd }) => { + await mockTextAd(); }); -test('base view +@dark-mode', async({ mount, page }) => { - await page.route(BLOCKS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(blockMock.baseListResponse), - })); - await page.route(STATS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(statsMock.base), - })); - - const component = await mount( - - - , - { hooksConfig }, - ); - await page.waitForResponse(BLOCKS_API_URL); +test('base view +@dark-mode', async({ render, mockApiResponse }) => { + await mockApiResponse('blocks', blockMock.baseListResponse, { queryParams: { type: 'block' } }); + await mockApiResponse('stats', statsMock.base); + + const component = await render(, { hooksConfig }); await expect(component).toHaveScreenshot(); }); -const hiddenFieldsTest = test.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.viewsEnvs.block.hiddenFields) as any, -}); +test('hidden fields', async({ render, mockApiResponse, mockEnvs }) => { + await mockEnvs(ENVS_MAP.blockHiddenFields); + await mockApiResponse('blocks', blockMock.baseListResponse, { queryParams: { type: 'block' } }); + await mockApiResponse('stats', statsMock.base); -hiddenFieldsTest('hidden fields', async({ mount, page }) => { - await page.route(BLOCKS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(blockMock.baseListResponse), - })); - await page.route(STATS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(statsMock.base), - })); - - const component = await mount( - - - , - { hooksConfig }, - ); - await page.waitForResponse(BLOCKS_API_URL); + const component = await render(, { hooksConfig }); await expect(component).toHaveScreenshot(); }); @@ -92,66 +45,30 @@ hiddenFieldsTest('hidden fields', async({ mount, page }) => { test.describe('mobile', () => { test.use({ viewport: devices['iPhone 13 Pro'].viewport }); - test(' base view', async({ mount, page }) => { - await page.route(BLOCKS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(blockMock.baseListResponse), - })); - await page.route(STATS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(statsMock.base), - })); - - const component = await mount( - - - , - { hooksConfig }, - ); - await page.waitForResponse(BLOCKS_API_URL); + test(' base view', async({ render, mockApiResponse }) => { + await mockApiResponse('blocks', blockMock.baseListResponse, { queryParams: { type: 'block' } }); + await mockApiResponse('stats', statsMock.base); + + const component = await render(, { hooksConfig }); await expect(component).toHaveScreenshot(); }); - const hiddenFieldsTest = test.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.viewsEnvs.block.hiddenFields) as any, - }); + test('hidden fields', async({ render, mockApiResponse, mockEnvs }) => { + await mockEnvs(ENVS_MAP.blockHiddenFields); + await mockApiResponse('blocks', blockMock.baseListResponse, { queryParams: { type: 'block' } }); + await mockApiResponse('stats', statsMock.base); - hiddenFieldsTest('hidden fields', async({ mount, page }) => { - await page.route(BLOCKS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(blockMock.baseListResponse), - })); - await page.route(STATS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(statsMock.base), - })); - - const component = await mount( - - - , - { hooksConfig }, - ); - await page.waitForResponse(BLOCKS_API_URL); + const component = await render(, { hooksConfig }); await expect(component).toHaveScreenshot(); }); }); -test('new item from socket', async({ mount, page, createSocket }) => { - await page.route(BLOCKS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(blockMock.baseListResponse), - })); +test('new item from socket', async({ render, mockApiResponse, createSocket }) => { + await mockApiResponse('blocks', blockMock.baseListResponse, { queryParams: { type: 'block' } }); - const component = await mount( - - - , - { hooksConfig }, - ); + const component = await render(, { hooksConfig }, { withSocket: true }); const socket = await createSocket(); const channel = await socketServer.joinChannel(socket, 'blocks:new_block'); @@ -167,18 +84,10 @@ test('new item from socket', async({ mount, page, createSocket }) => { await expect(component).toHaveScreenshot(); }); -test('socket error', async({ mount, page, createSocket }) => { - await page.route(BLOCKS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(blockMock.baseListResponse), - })); - - const component = await mount( - - - , - { hooksConfig }, - ); +test('socket error', async({ render, mockApiResponse, createSocket }) => { + await mockApiResponse('blocks', blockMock.baseListResponse, { queryParams: { type: 'block' } }); + + const component = await render(, { hooksConfig }, { withSocket: true }); const socket = await createSocket(); await socketServer.joinChannel(socket, 'blocks:new_block'); diff --git a/ui/pages/CsvExport.pw.tsx b/ui/pages/CsvExport.pw.tsx index 15e3ead1ac..8bf9d48264 100644 --- a/ui/pages/CsvExport.pw.tsx +++ b/ui/pages/CsvExport.pw.tsx @@ -1,38 +1,39 @@ -import { test, expect } from '@playwright/experimental-ct-react'; +import { Box } from '@chakra-ui/react'; import React from 'react'; import * as addressMock from 'mocks/address/address'; -import TestApp from 'playwright/TestApp'; -import buildApiUrl from 'playwright/utils/buildApiUrl'; +import * as tokenMock from 'mocks/tokens/tokenInfo'; +import { test, expect } from 'playwright/lib'; import * as configs from 'playwright/utils/configs'; import CsvExport from './CsvExport'; -const ADDRESS_API_URL = buildApiUrl('address', { hash: addressMock.hash }); -const hooksConfig = { - router: { - query: { address: addressMock.hash, type: 'transactions', filterType: 'address', filterValue: 'from' }, - isReady: true, - }, -}; - -test.beforeEach(async({ page }) => { - await page.route(ADDRESS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(addressMock.withName), - })); -}); +test('base view +@mobile +@dark-mode', async({ render, page, mockApiResponse }) => { + const hooksConfig = { + router: { + query: { address: addressMock.hash, type: 'transactions', filterType: 'address', filterValue: 'from' }, + }, + }; + await mockApiResponse('address', addressMock.validator, { pathParams: { hash: addressMock.hash } }); + + const component = await render(, { hooksConfig }); -test('base view +@mobile +@dark-mode', async({ mount, page }) => { + await expect(component).toHaveScreenshot({ + mask: [ page.locator('.recaptcha') ], + maskColor: configs.maskColor, + }); +}); - const component = await mount( - - - , - { hooksConfig }, - ); +test('token holders', async({ render, page, mockApiResponse }) => { + const hooksConfig = { + router: { + query: { address: addressMock.hash, type: 'holders' }, + }, + }; + await mockApiResponse('address', addressMock.token, { pathParams: { hash: addressMock.hash } }); + await mockApiResponse('token', tokenMock.tokenInfo, { pathParams: { hash: addressMock.hash } }); - await page.waitForResponse('https://www.google.com/recaptcha/api2/**'); + const component = await render(, { hooksConfig }); await expect(component).toHaveScreenshot({ mask: [ page.locator('.recaptcha') ], diff --git a/ui/pages/CsvExport.tsx b/ui/pages/CsvExport.tsx index 38a9b66a7b..23b6fe2cf1 100644 --- a/ui/pages/CsvExport.tsx +++ b/ui/pages/CsvExport.tsx @@ -15,6 +15,7 @@ import { nbsp } from 'lib/html-entities'; import CsvExportForm from 'ui/csvExport/CsvExportForm'; import ContentLoader from 'ui/shared/ContentLoader'; import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import TokenEntity from 'ui/shared/entities/token/TokenEntity'; import PageTitle from 'ui/shared/Page/PageTitle'; interface ExportTypeEntity { @@ -53,6 +54,11 @@ const EXPORT_TYPES: Record = { fileNameTemplate: 'logs', filterType: 'topic', }, + holders: { + text: 'holders', + resource: 'csv_export_token_holders', + fileNameTemplate: 'holders', + }, }; const isCorrectExportType = (type: string): type is CsvExportParams['type'] => Object.keys(EXPORT_TYPES).includes(type); @@ -75,6 +81,15 @@ const CsvExport = () => { }, }); + const tokenQuery = useApiQuery('token', { + pathParams: { hash: addressHash }, + queryOptions: { + enabled: Boolean(addressHash) && exportTypeParam === 'holders', + }, + }); + + const isLoading = addressQuery.isPending || (exportTypeParam === 'holders' && tokenQuery.isPending); + const backLink = React.useMemo(() => { const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/address'); @@ -111,7 +126,7 @@ const CsvExport = () => { const content = (() => { throwOnResourceLoadError(addressQuery); - if (addressQuery.isPending) { + if (isLoading) { return ; } @@ -119,6 +134,7 @@ const CsvExport = () => { { ); })(); - return ( - <> - + const description = (() => { + if (isLoading) { + return null; + } + + if (exportTypeParam === 'holders') { + return ( + + Export { exportType.text } for token + + to CSV file. + Exports are limited to the last 10K { exportType.text }. + + ); + } + + return ( Export { exportType.text } for address @@ -144,6 +177,16 @@ const CsvExport = () => { to CSV file. Exports are limited to the last 10K { exportType.text }. + ); + })(); + + return ( + <> + + { description } { content } ); diff --git a/ui/pages/GasTracker.tsx b/ui/pages/GasTracker.tsx index ecb4928257..b3212adce2 100644 --- a/ui/pages/GasTracker.tsx +++ b/ui/pages/GasTracker.tsx @@ -36,7 +36,8 @@ const GasTracker = () => { rowGap={ 1 } flexDir={{ base: 'column', lg: 'row' }} > - { data?.network_utilization_percentage && } + { typeof data?.network_utilization_percentage === 'number' && + } { data?.gas_price_updated_at && ( Last updated diff --git a/ui/pages/L2Deposits.pw.tsx b/ui/pages/L2Deposits.pw.tsx new file mode 100644 index 0000000000..4b9c5c7ed5 --- /dev/null +++ b/ui/pages/L2Deposits.pw.tsx @@ -0,0 +1,43 @@ +import { test as base, expect } from '@playwright/experimental-ct-react'; +import React from 'react'; + +import { data as depositsData } from 'mocks/l2deposits/deposits'; +import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; +import TestApp from 'playwright/TestApp'; +import buildApiUrl from 'playwright/utils/buildApiUrl'; +import * as configs from 'playwright/utils/configs'; + +import L2Deposits from './L2Deposits'; + +const DEPOSITS_API_URL = buildApiUrl('l2_deposits'); +const DEPOSITS_COUNT_API_URL = buildApiUrl('l2_deposits_count'); + +const test = base.extend({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + context: contextWithEnvs(configs.featureEnvs.rollup) as any, +}); + +test('base view +@mobile', async({ mount, page }) => { + await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ + status: 200, + body: '', + })); + + await page.route(DEPOSITS_API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(depositsData), + })); + + await page.route(DEPOSITS_COUNT_API_URL, (route) => route.fulfill({ + status: 200, + body: '3971111', + })); + + const component = await mount( + + + , + ); + + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/pages/L2Deposits.tsx b/ui/pages/L2Deposits.tsx new file mode 100644 index 0000000000..179efcdb8b --- /dev/null +++ b/ui/pages/L2Deposits.tsx @@ -0,0 +1,100 @@ +import { Box, Hide, Show, Skeleton } from '@chakra-ui/react'; +import React from 'react'; + +import useApiQuery from 'lib/api/useApiQuery'; +import { rightLineArrow, nbsp } from 'lib/html-entities'; +import { L2_DEPOSIT_ITEM } from 'stubs/L2'; +import { generateListStub } from 'stubs/utils'; +import DepositsListItem from 'ui/l2Deposits/DepositsListItem'; +import DepositsTable from 'ui/l2Deposits/DepositsTable'; +import ActionBar from 'ui/shared/ActionBar'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import PageTitle from 'ui/shared/Page/PageTitle'; +import Pagination from 'ui/shared/pagination/Pagination'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; + +const L2Deposits = () => { + const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({ + resourceName: 'l2_deposits', + options: { + placeholderData: generateListStub<'l2_deposits'>( + L2_DEPOSIT_ITEM, + 50, + { + next_page_params: { + items_count: 50, + l1_block_number: 9045200, + tx_hash: '', + }, + }, + ), + }, + }); + + const countersQuery = useApiQuery('l2_deposits_count', { + queryOptions: { + placeholderData: 1927029, + }, + }); + + const content = data?.items ? ( + <> + + { data.items.map(((item, index) => ( + + ))) } + + + + + + ) : null; + + const text = (() => { + if (countersQuery.isError) { + return null; + } + + return ( + + A total of { countersQuery.data?.toLocaleString() } deposits found + + ); + })(); + + const actionBar = ( + <> + + { text } + + + + { text } + + { pagination.isVisible && } + + + ); + + return ( + <> + + + + ); +}; + +export default L2Deposits; diff --git a/ui/pages/L2OutputRoots.pw.tsx b/ui/pages/L2OutputRoots.pw.tsx new file mode 100644 index 0000000000..b799a54f54 --- /dev/null +++ b/ui/pages/L2OutputRoots.pw.tsx @@ -0,0 +1,43 @@ +import { test as base, expect } from '@playwright/experimental-ct-react'; +import React from 'react'; + +import { outputRootsData } from 'mocks/l2outputRoots/outputRoots'; +import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; +import TestApp from 'playwright/TestApp'; +import buildApiUrl from 'playwright/utils/buildApiUrl'; +import * as configs from 'playwright/utils/configs'; + +import OutputRoots from './L2OutputRoots'; + +const test = base.extend({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + context: contextWithEnvs(configs.featureEnvs.rollup) as any, +}); + +const OUTPUT_ROOTS_API_URL = buildApiUrl('l2_output_roots'); +const OUTPUT_ROOTS_COUNT_API_URL = buildApiUrl('l2_output_roots_count'); + +test('base view +@mobile', async({ mount, page }) => { + await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ + status: 200, + body: '', + })); + + await page.route(OUTPUT_ROOTS_API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(outputRootsData), + })); + + await page.route(OUTPUT_ROOTS_COUNT_API_URL, (route) => route.fulfill({ + status: 200, + body: '9927', + })); + + const component = await mount( + + + , + ); + + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/pages/L2OutputRoots.tsx b/ui/pages/L2OutputRoots.tsx new file mode 100644 index 0000000000..624682e195 --- /dev/null +++ b/ui/pages/L2OutputRoots.tsx @@ -0,0 +1,98 @@ +import { Box, Hide, Show, Skeleton, Text } from '@chakra-ui/react'; +import React from 'react'; + +import useApiQuery from 'lib/api/useApiQuery'; +import { L2_OUTPUT_ROOTS_ITEM } from 'stubs/L2'; +import { generateListStub } from 'stubs/utils'; +import OutputRootsListItem from 'ui/l2OutputRoots/OutputRootsListItem'; +import OutputRootsTable from 'ui/l2OutputRoots/OutputRootsTable'; +import ActionBar from 'ui/shared/ActionBar'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import PageTitle from 'ui/shared/Page/PageTitle'; +import Pagination from 'ui/shared/pagination/Pagination'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; + +const L2OutputRoots = () => { + const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({ + resourceName: 'l2_output_roots', + options: { + placeholderData: generateListStub<'l2_output_roots'>( + L2_OUTPUT_ROOTS_ITEM, + 50, + { + next_page_params: { + items_count: 50, + index: 9045200, + }, + }, + ), + }, + }); + + const countersQuery = useApiQuery('l2_output_roots_count', { + queryOptions: { + placeholderData: 50617, + }, + }); + + const content = data?.items ? ( + <> + + { data.items.map(((item, index) => ( + + ))) } + + + + + + ) : null; + + const text = (() => { + if (countersQuery.isError || isError || !data?.items.length) { + return null; + } + + return ( + + L2 output index + #{ data.items[0].l2_output_index } to + #{ data.items[data.items.length - 1].l2_output_index } + (total of { countersQuery.data?.toLocaleString() } roots) + + ); + })(); + + const actionBar = ( + <> + + { text } + + + + { text } + + { pagination.isVisible && } + + + ); + + return ( + <> + + + + ); +}; + +export default L2OutputRoots; diff --git a/ui/pages/L2TxnBatches.pw.tsx b/ui/pages/L2TxnBatches.pw.tsx new file mode 100644 index 0000000000..6b2336f492 --- /dev/null +++ b/ui/pages/L2TxnBatches.pw.tsx @@ -0,0 +1,43 @@ +import { test as base, expect } from '@playwright/experimental-ct-react'; +import React from 'react'; + +import { txnBatchesData } from 'mocks/l2txnBatches/txnBatches'; +import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; +import TestApp from 'playwright/TestApp'; +import buildApiUrl from 'playwright/utils/buildApiUrl'; +import * as configs from 'playwright/utils/configs'; + +import L2TxnBatches from './L2TxnBatches'; + +const test = base.extend({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + context: contextWithEnvs(configs.featureEnvs.rollup) as any, +}); + +const TXN_BATCHES_API_URL = buildApiUrl('l2_txn_batches'); +const TXN_BATCHES_COUNT_API_URL = buildApiUrl('l2_txn_batches_count'); + +test('base view +@mobile', async({ mount, page }) => { + await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ + status: 200, + body: '', + })); + + await page.route(TXN_BATCHES_API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(txnBatchesData), + })); + + await page.route(TXN_BATCHES_COUNT_API_URL, (route) => route.fulfill({ + status: 200, + body: '1235016', + })); + + const component = await mount( + + + , + ); + + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/pages/L2TxnBatches.tsx b/ui/pages/L2TxnBatches.tsx new file mode 100644 index 0000000000..ab4b1b02f2 --- /dev/null +++ b/ui/pages/L2TxnBatches.tsx @@ -0,0 +1,97 @@ +import { Box, Hide, Show, Skeleton, Text } from '@chakra-ui/react'; +import React from 'react'; + +import useApiQuery from 'lib/api/useApiQuery'; +import { nbsp } from 'lib/html-entities'; +import { L2_TXN_BATCHES_ITEM } from 'stubs/L2'; +import { generateListStub } from 'stubs/utils'; +import TxnBatchesListItem from 'ui/l2TxnBatches/TxnBatchesListItem'; +import TxnBatchesTable from 'ui/l2TxnBatches/TxnBatchesTable'; +import ActionBar from 'ui/shared/ActionBar'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import PageTitle from 'ui/shared/Page/PageTitle'; +import Pagination from 'ui/shared/pagination/Pagination'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; + +const L2TxnBatches = () => { + const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({ + resourceName: 'l2_txn_batches', + options: { + placeholderData: generateListStub<'l2_txn_batches'>( + L2_TXN_BATCHES_ITEM, + 50, + { + next_page_params: { + items_count: 50, + block_number: 9045200, + }, + }, + ), + }, + }); + + const countersQuery = useApiQuery('l2_txn_batches_count', { + queryOptions: { + placeholderData: 5231746, + }, + }); + + const content = data?.items ? ( + <> + + { data.items.map(((item, index) => ( + + ))) } + + + + ) : null; + + const text = (() => { + if (countersQuery.isError || isError || !data?.items.length) { + return null; + } + + return ( + + Tx batch (L2 block) + #{ data.items[0].l2_block_number } to + #{ data.items[data.items.length - 1].l2_block_number } + (total of { countersQuery.data?.toLocaleString() } batches) + + ); + })(); + + const actionBar = ( + <> + + { text } + + + + { text } + + { pagination.isVisible && } + + + ); + + return ( + <> + + + + ); +}; + +export default L2TxnBatches; diff --git a/ui/pages/L2Withdrawals.pw.tsx b/ui/pages/L2Withdrawals.pw.tsx new file mode 100644 index 0000000000..b5e8d9b8d0 --- /dev/null +++ b/ui/pages/L2Withdrawals.pw.tsx @@ -0,0 +1,43 @@ +import { test as base, expect } from '@playwright/experimental-ct-react'; +import React from 'react'; + +import { data as withdrawalsData } from 'mocks/l2withdrawals/withdrawals'; +import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; +import TestApp from 'playwright/TestApp'; +import buildApiUrl from 'playwright/utils/buildApiUrl'; +import * as configs from 'playwright/utils/configs'; + +import L2Withdrawals from './L2Withdrawals'; + +const test = base.extend({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + context: contextWithEnvs(configs.featureEnvs.rollup) as any, +}); + +const WITHDRAWALS_API_URL = buildApiUrl('l2_withdrawals'); +const WITHDRAWALS_COUNT_API_URL = buildApiUrl('l2_withdrawals_count'); + +test('base view +@mobile', async({ mount, page }) => { + await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ + status: 200, + body: '', + })); + + await page.route(WITHDRAWALS_API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(withdrawalsData), + })); + + await page.route(WITHDRAWALS_COUNT_API_URL, (route) => route.fulfill({ + status: 200, + body: '397', + })); + + const component = await mount( + + + , + ); + + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/pages/L2Withdrawals.tsx b/ui/pages/L2Withdrawals.tsx new file mode 100644 index 0000000000..b5610c2f47 --- /dev/null +++ b/ui/pages/L2Withdrawals.tsx @@ -0,0 +1,97 @@ +import { Box, Hide, Show, Skeleton } from '@chakra-ui/react'; +import React from 'react'; + +import useApiQuery from 'lib/api/useApiQuery'; +import { rightLineArrow, nbsp } from 'lib/html-entities'; +import { L2_WITHDRAWAL_ITEM } from 'stubs/L2'; +import { generateListStub } from 'stubs/utils'; +import WithdrawalsListItem from 'ui/l2Withdrawals/WithdrawalsListItem'; +import WithdrawalsTable from 'ui/l2Withdrawals/WithdrawalsTable'; +import ActionBar from 'ui/shared/ActionBar'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import PageTitle from 'ui/shared/Page/PageTitle'; +import Pagination from 'ui/shared/pagination/Pagination'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; + +const L2Withdrawals = () => { + const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({ + resourceName: 'l2_withdrawals', + options: { + placeholderData: generateListStub<'l2_withdrawals'>( + L2_WITHDRAWAL_ITEM, + 50, + { + next_page_params: { + items_count: 50, + nonce: '', + }, + }, + ), + }, + }); + + const countersQuery = useApiQuery('l2_withdrawals_count', { + queryOptions: { + placeholderData: 23700, + }, + }); + + const content = data?.items ? ( + <> + { data.items.map(((item, index) => ( + + ))) } + + + + + ) : null; + + const text = (() => { + if (countersQuery.isError) { + return null; + } + + return ( + + A total of { countersQuery.data?.toLocaleString() } withdrawals found + + ); + })(); + + const actionBar = ( + <> + + { text } + + + + { text } + + { pagination.isVisible && } + + + ); + + return ( + <> + + + + ); +}; + +export default L2Withdrawals; diff --git a/ui/pages/Marketplace.pw.tsx b/ui/pages/Marketplace.pw.tsx new file mode 100644 index 0000000000..004b1c6f95 --- /dev/null +++ b/ui/pages/Marketplace.pw.tsx @@ -0,0 +1,106 @@ +import React from 'react'; + +import { apps as appsMock } from 'mocks/apps/apps'; +import { securityReports as securityReportsMock } from 'mocks/apps/securityReports'; +import { test, expect, devices } from 'playwright/lib'; + +import Marketplace from './Marketplace'; + +const MARKETPLACE_CONFIG_URL = 'http://localhost/marketplace-config.json'; + +test.beforeEach(async({ mockConfigResponse, mockEnvs, mockAssetResponse }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_MARKETPLACE_ENABLED', 'true' ], + [ 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL ], + ]); + await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL, JSON.stringify(appsMock)); + await Promise.all(appsMock.map(app => mockAssetResponse(app.logo, './playwright/mocks/image_s.jpg'))); +}); + +test('base view +@dark-mode', async({ render }) => { + const component = await render(); + + await expect(component).toHaveScreenshot(); +}); + +test('with featured app +@dark-mode', async({ render, mockEnvs }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_MARKETPLACE_FEATURED_APP', 'hop-exchange' ], + ]); + const component = await render(); + + await expect(component).toHaveScreenshot(); +}); + +test('with banner +@dark-mode', async({ render, mockEnvs, mockConfigResponse }) => { + const MARKETPLACE_BANNER_CONTENT_URL = 'https://localhost/marketplace-banner.html'; + const MARKETPLACE_BANNER_LINK_URL = 'https://example.com'; + + await mockEnvs([ + [ 'NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL', MARKETPLACE_BANNER_CONTENT_URL ], + [ 'NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL', MARKETPLACE_BANNER_LINK_URL ], + ]); + await mockConfigResponse('MARKETPLACE_BANNER_CONTENT_URL', MARKETPLACE_BANNER_CONTENT_URL, './playwright/mocks/page.html', true); + const component = await render(); + + await expect(component).toHaveScreenshot(); +}); + +test('with scores +@dark-mode', async({ render, mockConfigResponse, mockEnvs }) => { + const MARKETPLACE_SECURITY_REPORTS_URL = 'https://marketplace-security-reports.json'; + await mockEnvs([ + [ 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL ], + ]); + await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL, JSON.stringify(securityReportsMock)); + const component = await render(); + await component.getByText('Apps scores').click(); + + await expect(component).toHaveScreenshot(); +}); + +// I had a memory error while running tests in GH actions +// separate run for mobile tests fixes it +test.describe('mobile', () => { + test.use({ viewport: devices['iPhone 13 Pro'].viewport }); + + test('base view', async({ render }) => { + const component = await render(); + + await expect(component).toHaveScreenshot(); + }); + + test('with featured app', async({ render, mockEnvs }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_MARKETPLACE_FEATURED_APP', 'hop-exchange' ], + ]); + const component = await render(); + + await expect(component).toHaveScreenshot(); + }); + + test('with banner', async({ render, mockEnvs, mockConfigResponse }) => { + const MARKETPLACE_BANNER_CONTENT_URL = 'https://localhost/marketplace-banner.html'; + const MARKETPLACE_BANNER_LINK_URL = 'https://example.com'; + + await mockEnvs([ + [ 'NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL', MARKETPLACE_BANNER_CONTENT_URL ], + [ 'NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL', MARKETPLACE_BANNER_LINK_URL ], + ]); + await mockConfigResponse('MARKETPLACE_BANNER_CONTENT_URL', MARKETPLACE_BANNER_CONTENT_URL, './playwright/mocks/page.html', true); + const component = await render(); + + await expect(component).toHaveScreenshot(); + }); + + test('with scores', async({ render, mockConfigResponse, mockEnvs }) => { + const MARKETPLACE_SECURITY_REPORTS_URL = 'https://marketplace-security-reports.json'; + await mockEnvs([ + [ 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL ], + ]); + await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL, JSON.stringify(securityReportsMock)); + const component = await render(); + await component.getByText('Apps scores').click(); + + await expect(component).toHaveScreenshot(); + }); +}); diff --git a/ui/pages/Marketplace.tsx b/ui/pages/Marketplace.tsx index 1962559368..11c8776db8 100644 --- a/ui/pages/Marketplace.tsx +++ b/ui/pages/Marketplace.tsx @@ -1,21 +1,25 @@ -import { Box, Menu, MenuButton, MenuItem, MenuList, Flex, IconButton } from '@chakra-ui/react'; +import { Box, Menu, MenuButton, MenuItem, MenuList, Flex, IconButton, Skeleton } from '@chakra-ui/react'; import React from 'react'; +import type { MouseEvent } from 'react'; -import { MarketplaceCategory } from 'types/client/marketplace'; +import { MarketplaceCategory, MarketplaceDisplayType } from 'types/client/marketplace'; import type { TabItem } from 'ui/shared/Tabs/types'; import config from 'configs/app'; import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; import useIsMobile from 'lib/hooks/useIsMobile'; +import Banner from 'ui/marketplace/Banner'; +import ContractListModal from 'ui/marketplace/ContractListModal'; import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal'; import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal'; import MarketplaceList from 'ui/marketplace/MarketplaceList'; +import MarketplaceListWithScores from 'ui/marketplace/MarketplaceListWithScores'; import FilterInput from 'ui/shared/filters/FilterInput'; import IconSvg from 'ui/shared/IconSvg'; import type { IconName } from 'ui/shared/IconSvg'; import LinkExternal from 'ui/shared/LinkExternal'; import PageTitle from 'ui/shared/Page/PageTitle'; -import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton'; +import RadioButtonGroup from 'ui/shared/radioButtonGroup/RadioButtonGroup'; import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll'; import useMarketplace from '../marketplace/useMarketplace'; @@ -50,6 +54,7 @@ const Marketplace = () => { filterQuery, onSearchInputChange, showAppInfo, + apps, displayedApps, selectedAppId, clearSelectedAppId, @@ -60,7 +65,13 @@ const Marketplace = () => { showDisclaimer, appsTotal, isCategoriesPlaceholderData, + showContractList, + contractListModalType, + selectedDisplayType, + onDisplayTypeChange, + hasPreviousStep, } = useMarketplace(); + const isMobile = useIsMobile(); const categoryTabs = React.useMemo(() => { @@ -80,7 +91,7 @@ const Marketplace = () => { tabs.unshift({ id: MarketplaceCategory.FAVORITES, - title: () => , + title: () => , count: null, component: null, }); @@ -93,18 +104,33 @@ const Marketplace = () => { return index === -1 ? 0 : index; }, [ categoryTabs, selectedCategoryId ]); + const selectedApp = displayedApps.find(app => app.id === selectedAppId); + const handleCategoryChange = React.useCallback((index: number) => { onCategoryChange(categoryTabs[index].id); }, [ categoryTabs, onCategoryChange ]); + const handleAppClick = React.useCallback((event: MouseEvent, id: string) => { + const isShown = window.localStorage.getItem('marketplace-disclaimer-shown'); + if (!isShown) { + event.preventDefault(); + showDisclaimer(id); + } + }, [ showDisclaimer ]); + + const handleGoBackInContractListModal = React.useCallback(() => { + clearSelectedAppId(); + if (selectedApp) { + showAppInfo(selectedApp.id); + } + }, [ clearSelectedAppId, showAppInfo, selectedApp ]); + throwOnResourceLoadError(isError && error ? { isError, error } : { isError: false, error: null }); if (!feature.isEnabled) { return null; } - const selectedApp = displayedApps.find(app => app.id === selectedAppId); - return ( <> { ) } /> - - { (isCategoriesPlaceholderData) ? ( - - ) : ( - - ) } - - - - + + + + + + { feature.securityReportsUrl && ( + + + onChange={ onDisplayTypeChange } + defaultValue={ selectedDisplayType } + name="type" + options={ [ + { + title: 'Discovery', + value: MarketplaceDisplayType.DEFAULT, + icon: 'apps_xs', + onlyIcon: false, + }, + { + title: 'Apps scores', + value: MarketplaceDisplayType.SCORES, + icon: 'apps_list', + onlyIcon: false, + contentAfter: ( + + ), + }, + ] } + autoWidth + /> + + ) } + + + + { (selectedDisplayType === MarketplaceDisplayType.SCORES && feature.securityReportsUrl) ? ( + + ) : ( + + ) } + { (selectedApp && isAppInfoModalOpen) && ( ) } @@ -188,6 +269,15 @@ const Marketplace = () => { appId={ selectedApp.id } /> ) } + + { (selectedApp && contractListModalType) && ( + + ) } ); }; diff --git a/ui/pages/MarketplaceApp.pw.tsx b/ui/pages/MarketplaceApp.pw.tsx new file mode 100644 index 0000000000..08d1be9b6d --- /dev/null +++ b/ui/pages/MarketplaceApp.pw.tsx @@ -0,0 +1,41 @@ +import { Flex } from '@chakra-ui/react'; +import React from 'react'; + +import { apps as appsMock } from 'mocks/apps/apps'; +import { test, expect, devices } from 'playwright/lib'; + +import MarketplaceApp from './MarketplaceApp'; + +const hooksConfig = { + router: { + query: { id: appsMock[0].id }, + isReady: true, + }, +}; + +const MARKETPLACE_CONFIG_URL = 'https://marketplace-config.json'; + +const testFn: Parameters[1] = async({ render, mockConfigResponse, mockAssetResponse, mockEnvs }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_MARKETPLACE_ENABLED', 'true' ], + [ 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL ], + ]); + await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL, JSON.stringify(appsMock)); + await mockAssetResponse(appsMock[0].url, './mocks/apps/app.html'); + + const component = await render( + + + , + { hooksConfig }, + ); + + await expect(component).toHaveScreenshot(); +}; + +test('base view +@dark-mode', testFn); + +test.describe('mobile', () => { + test.use({ viewport: devices['iPhone 13 Pro'].viewport }); + test('base view', testFn); +}); diff --git a/ui/pages/MarketplaceApp.tsx b/ui/pages/MarketplaceApp.tsx index 98e65dcf57..44f98d9de2 100644 --- a/ui/pages/MarketplaceApp.tsx +++ b/ui/pages/MarketplaceApp.tsx @@ -1,4 +1,4 @@ -import { Box, Center, useColorMode } from '@chakra-ui/react'; +import { Box, Center, useColorMode, Flex } from '@chakra-ui/react'; import { useQuery } from '@tanstack/react-query'; import { DappscoutIframeProvider, useDappscoutIframe } from 'dappscout-iframe'; import { useRouter } from 'next/router'; @@ -11,14 +11,17 @@ import { route } from 'nextjs-routes'; import config from 'configs/app'; import type { ResourceError } from 'lib/api/resources'; import useApiFetch from 'lib/api/useApiFetch'; +import { useMarketplaceContext } from 'lib/contexts/marketplace'; import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; import useFetch from 'lib/hooks/useFetch'; import * as metadata from 'lib/metadata'; import getQueryParamString from 'lib/router/getQueryParamString'; import ContentLoader from 'ui/shared/ContentLoader'; +import MarketplaceAppTopBar from '../marketplace/MarketplaceAppTopBar'; import useAutoConnectWallet from '../marketplace/useAutoConnectWallet'; import useMarketplaceWallet from '../marketplace/useMarketplaceWallet'; +import useSecurityReports from '../marketplace/useSecurityReports'; const feature = config.features.marketplace; @@ -69,7 +72,7 @@ const MarketplaceAppContent = ({ address, data, isPending }: Props) => { return (
{ (isFrameLoading) && ( @@ -96,13 +99,14 @@ const MarketplaceAppContent = ({ address, data, isPending }: Props) => { }; const MarketplaceApp = () => { - const { address, sendTransaction, signMessage, signTypedData } = useMarketplaceWallet(); - useAutoConnectWallet(); - const fetch = useFetch(); const apiFetch = useApiFetch(); const router = useRouter(); const id = getQueryParamString(router.query.id); + const { address, sendTransaction, signMessage, signTypedData } = useMarketplaceWallet(id); + useAutoConnectWallet(); + + const { data: securityReports, isLoading: isSecurityReportsLoading } = useSecurityReports(); const query = useQuery, MarketplaceAppOverview>({ queryKey: [ 'marketplace-dapps', id ], @@ -126,6 +130,7 @@ const MarketplaceApp = () => { enabled: feature.isEnabled, }); const { data, isPending } = query; + const { setIsAutoConnectDisabled } = useMarketplaceContext(); useEffect(() => { if (data) { @@ -133,22 +138,30 @@ const MarketplaceApp = () => { { pathname: '/apps/[id]', query: { id: data.id } }, { app_name: data.title }, ); + setIsAutoConnectDisabled(!data.internalWallet); } - }, [ data ]); + }, [ data, setIsAutoConnectDisabled ]); throwOnResourceLoadError(query); return ( - - - + + + + + + ); }; diff --git a/ui/pages/MyProfile.tsx b/ui/pages/MyProfile.tsx index 91fe658d80..bd3972ef6d 100644 --- a/ui/pages/MyProfile.tsx +++ b/ui/pages/MyProfile.tsx @@ -44,7 +44,7 @@ const MyProfile = () => { Email diff --git a/ui/pages/OptimisticL2Deposits.pw.tsx b/ui/pages/OptimisticL2Deposits.pw.tsx index b1737edd05..68360ce79c 100644 --- a/ui/pages/OptimisticL2Deposits.pw.tsx +++ b/ui/pages/OptimisticL2Deposits.pw.tsx @@ -1,48 +1,22 @@ -import { test as base, expect } from '@playwright/experimental-ct-react'; import React from 'react'; import { data as depositsData } from 'mocks/l2deposits/deposits'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; -import buildApiUrl from 'playwright/utils/buildApiUrl'; -import * as configs from 'playwright/utils/configs'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import { test, expect } from 'playwright/lib'; import OptimisticL2Deposits from './OptimisticL2Deposits'; -const DEPOSITS_API_URL = buildApiUrl('l2_deposits'); -const DEPOSITS_COUNT_API_URL = buildApiUrl('l2_deposits_count'); - -const test = base.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.featureEnvs.optimisticRollup) as any, -}); - -test('base view +@mobile', async({ mount, page }) => { +test('base view +@mobile', async({ render, mockEnvs, mockTextAd, mockApiResponse }) => { // test on mobile is flaky // my assumption is there is not enough time to calculate hashes truncation so component is unstable // so I raised the test timeout to check if it helps test.slow(); + await mockEnvs(ENVS_MAP.optimisticRollup); + await mockTextAd(); + await mockApiResponse('optimistic_l2_deposits', depositsData); + await mockApiResponse('optimistic_l2_deposits_count', 3971111); - await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ - status: 200, - body: '', - })); - - await page.route(DEPOSITS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(depositsData), - })); - - await page.route(DEPOSITS_COUNT_API_URL, (route) => route.fulfill({ - status: 200, - body: '3971111', - })); - - const component = await mount( - - - , - ); + const component = await render(); await expect(component).toHaveScreenshot({ timeout: 10_000 }); }); diff --git a/ui/pages/OptimisticL2Deposits.tsx b/ui/pages/OptimisticL2Deposits.tsx index aa71178691..0b2335af74 100644 --- a/ui/pages/OptimisticL2Deposits.tsx +++ b/ui/pages/OptimisticL2Deposits.tsx @@ -14,9 +14,9 @@ import StickyPaginationWithText from 'ui/shared/StickyPaginationWithText'; const OptimisticL2Deposits = () => { const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({ - resourceName: 'l2_deposits', + resourceName: 'optimistic_l2_deposits', options: { - placeholderData: generateListStub<'l2_deposits'>( + placeholderData: generateListStub<'optimistic_l2_deposits'>( L2_DEPOSIT_ITEM, 50, { @@ -30,7 +30,7 @@ const OptimisticL2Deposits = () => { }, }); - const countersQuery = useApiQuery('l2_deposits_count', { + const countersQuery = useApiQuery('optimistic_l2_deposits_count', { queryOptions: { placeholderData: 1927029, }, @@ -76,7 +76,7 @@ const OptimisticL2Deposits = () => { diff --git a/ui/pages/OptimisticL2OutputRoots.pw.tsx b/ui/pages/OptimisticL2OutputRoots.pw.tsx index 8c19f64377..bf9878b0f5 100644 --- a/ui/pages/OptimisticL2OutputRoots.pw.tsx +++ b/ui/pages/OptimisticL2OutputRoots.pw.tsx @@ -1,48 +1,20 @@ -import { test as base, expect } from '@playwright/experimental-ct-react'; import React from 'react'; import { outputRootsData } from 'mocks/l2outputRoots/outputRoots'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; -import buildApiUrl from 'playwright/utils/buildApiUrl'; -import * as configs from 'playwright/utils/configs'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import { test, expect } from 'playwright/lib'; import OptimisticL2OutputRoots from './OptimisticL2OutputRoots'; -const test = base.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.featureEnvs.optimisticRollup) as any, -}); - -const OUTPUT_ROOTS_API_URL = buildApiUrl('l2_output_roots'); -const OUTPUT_ROOTS_COUNT_API_URL = buildApiUrl('l2_output_roots_count'); - -test('base view +@mobile', async({ mount, page }) => { +test('base view +@mobile', async({ render, mockEnvs, mockTextAd, mockApiResponse }) => { // test on mobile is flaky // my assumption is there is not enough time to calculate hashes truncation so component is unstable // so I raised the test timeout to check if it helps test.slow(); - - await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ - status: 200, - body: '', - })); - - await page.route(OUTPUT_ROOTS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(outputRootsData), - })); - - await page.route(OUTPUT_ROOTS_COUNT_API_URL, (route) => route.fulfill({ - status: 200, - body: '9927', - })); - - const component = await mount( - - - , - ); - + await mockEnvs(ENVS_MAP.optimisticRollup); + await mockTextAd(); + await mockApiResponse('optimistic_l2_output_roots', outputRootsData); + await mockApiResponse('optimistic_l2_output_roots_count', 9927); + const component = await render(); await expect(component).toHaveScreenshot({ timeout: 10_000 }); }); diff --git a/ui/pages/OptimisticL2OutputRoots.tsx b/ui/pages/OptimisticL2OutputRoots.tsx index dac63f79cf..ef72bc84f9 100644 --- a/ui/pages/OptimisticL2OutputRoots.tsx +++ b/ui/pages/OptimisticL2OutputRoots.tsx @@ -13,9 +13,9 @@ import StickyPaginationWithText from 'ui/shared/StickyPaginationWithText'; const OptimisticL2OutputRoots = () => { const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({ - resourceName: 'l2_output_roots', + resourceName: 'optimistic_l2_output_roots', options: { - placeholderData: generateListStub<'l2_output_roots'>( + placeholderData: generateListStub<'optimistic_l2_output_roots'>( L2_OUTPUT_ROOTS_ITEM, 50, { @@ -28,7 +28,7 @@ const OptimisticL2OutputRoots = () => { }, }); - const countersQuery = useApiQuery('l2_output_roots_count', { + const countersQuery = useApiQuery('optimistic_l2_output_roots_count', { queryOptions: { placeholderData: 50617, }, diff --git a/ui/pages/OptimisticL2TxnBatches.pw.tsx b/ui/pages/OptimisticL2TxnBatches.pw.tsx index fb6c40a307..f98922fdc6 100644 --- a/ui/pages/OptimisticL2TxnBatches.pw.tsx +++ b/ui/pages/OptimisticL2TxnBatches.pw.tsx @@ -1,48 +1,20 @@ -import { test as base, expect } from '@playwright/experimental-ct-react'; import React from 'react'; import { txnBatchesData } from 'mocks/l2txnBatches/txnBatches'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; -import buildApiUrl from 'playwright/utils/buildApiUrl'; -import * as configs from 'playwright/utils/configs'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import { test, expect } from 'playwright/lib'; import OptimisticL2TxnBatches from './OptimisticL2TxnBatches'; -const test = base.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.featureEnvs.optimisticRollup) as any, -}); - -const TXN_BATCHES_API_URL = buildApiUrl('l2_txn_batches'); -const TXN_BATCHES_COUNT_API_URL = buildApiUrl('l2_txn_batches_count'); - -test('base view +@mobile', async({ mount, page }) => { +test('base view +@mobile', async({ render, mockTextAd, mockEnvs, mockApiResponse }) => { // test on mobile is flaky // my assumption is there is not enough time to calculate hashes truncation so component is unstable // so I raised the test timeout to check if it helps test.slow(); - - await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ - status: 200, - body: '', - })); - - await page.route(TXN_BATCHES_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(txnBatchesData), - })); - - await page.route(TXN_BATCHES_COUNT_API_URL, (route) => route.fulfill({ - status: 200, - body: '1235016', - })); - - const component = await mount( - - - , - ); - + await mockTextAd(); + await mockEnvs(ENVS_MAP.optimisticRollup); + await mockApiResponse('optimistic_l2_txn_batches', txnBatchesData); + await mockApiResponse('optimistic_l2_txn_batches_count', 1235016); + const component = await render(); await expect(component).toHaveScreenshot({ timeout: 10_000 }); }); diff --git a/ui/pages/OptimisticL2TxnBatches.tsx b/ui/pages/OptimisticL2TxnBatches.tsx index 6d8decf462..9578642300 100644 --- a/ui/pages/OptimisticL2TxnBatches.tsx +++ b/ui/pages/OptimisticL2TxnBatches.tsx @@ -14,9 +14,9 @@ import OptimisticL2TxnBatchesTable from 'ui/txnBatches/optimisticL2/OptimisticL2 const OptimisticL2TxnBatches = () => { const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({ - resourceName: 'l2_txn_batches', + resourceName: 'optimistic_l2_txn_batches', options: { - placeholderData: generateListStub<'l2_txn_batches'>( + placeholderData: generateListStub<'optimistic_l2_txn_batches'>( L2_TXN_BATCHES_ITEM, 50, { @@ -29,7 +29,7 @@ const OptimisticL2TxnBatches = () => { }, }); - const countersQuery = useApiQuery('l2_txn_batches_count', { + const countersQuery = useApiQuery('optimistic_l2_txn_batches_count', { queryOptions: { placeholderData: 5231746, }, diff --git a/ui/pages/OptimisticL2Withdrawals.pw.tsx b/ui/pages/OptimisticL2Withdrawals.pw.tsx index 70742da848..98cbeea711 100644 --- a/ui/pages/OptimisticL2Withdrawals.pw.tsx +++ b/ui/pages/OptimisticL2Withdrawals.pw.tsx @@ -1,48 +1,20 @@ -import { test as base, expect } from '@playwright/experimental-ct-react'; import React from 'react'; import { data as withdrawalsData } from 'mocks/l2withdrawals/withdrawals'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; -import buildApiUrl from 'playwright/utils/buildApiUrl'; -import * as configs from 'playwright/utils/configs'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import { test, expect } from 'playwright/lib'; import OptimisticL2Withdrawals from './OptimisticL2Withdrawals'; -const test = base.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.featureEnvs.optimisticRollup) as any, -}); - -const WITHDRAWALS_API_URL = buildApiUrl('l2_withdrawals'); -const WITHDRAWALS_COUNT_API_URL = buildApiUrl('l2_withdrawals_count'); - -test('base view +@mobile', async({ mount, page }) => { +test('base view +@mobile', async({ render, mockTextAd, mockEnvs, mockApiResponse }) => { // test on mobile is flaky // my assumption is there is not enough time to calculate hashes truncation so component is unstable // so I raised the test timeout to check if it helps test.slow(); - - await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ - status: 200, - body: '', - })); - - await page.route(WITHDRAWALS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(withdrawalsData), - })); - - await page.route(WITHDRAWALS_COUNT_API_URL, (route) => route.fulfill({ - status: 200, - body: '397', - })); - - const component = await mount( - - - , - ); - + await mockTextAd(); + await mockEnvs(ENVS_MAP.optimisticRollup); + await mockApiResponse('optimistic_l2_withdrawals', withdrawalsData); + await mockApiResponse('optimistic_l2_withdrawals_count', 397); + const component = await render(); await expect(component).toHaveScreenshot({ timeout: 10_000 }); }); diff --git a/ui/pages/OptimisticL2Withdrawals.tsx b/ui/pages/OptimisticL2Withdrawals.tsx index 43ec837ad2..d249970e58 100644 --- a/ui/pages/OptimisticL2Withdrawals.tsx +++ b/ui/pages/OptimisticL2Withdrawals.tsx @@ -14,9 +14,9 @@ import OptimisticL2WithdrawalsTable from 'ui/withdrawals/optimisticL2/Optimistic const OptimisticL2Withdrawals = () => { const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({ - resourceName: 'l2_withdrawals', + resourceName: 'optimistic_l2_withdrawals', options: { - placeholderData: generateListStub<'l2_withdrawals'>( + placeholderData: generateListStub<'optimistic_l2_withdrawals'>( L2_WITHDRAWAL_ITEM, 50, { @@ -29,7 +29,7 @@ const OptimisticL2Withdrawals = () => { }, }); - const countersQuery = useApiQuery('l2_withdrawals_count', { + const countersQuery = useApiQuery('optimistic_l2_withdrawals_count', { queryOptions: { placeholderData: 23700, }, @@ -39,7 +39,7 @@ const OptimisticL2Withdrawals = () => { <> { data.items.map(((item, index) => ( diff --git a/ui/pages/SearchResults.pw.tsx b/ui/pages/SearchResults.pw.tsx index f66ed35bf2..3a492ad3ce 100644 --- a/ui/pages/SearchResults.pw.tsx +++ b/ui/pages/SearchResults.pw.tsx @@ -1,243 +1,184 @@ -import { test, expect } from '@playwright/experimental-ct-react'; import React from 'react'; -import { buildExternalAssetFilePath } from 'configs/app/utils'; import { apps as appsMock } from 'mocks/apps/apps'; import * as searchMock from 'mocks/search/index'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; -import * as app from 'playwright/utils/app'; -import buildApiUrl from 'playwright/utils/buildApiUrl'; -import * as configs from 'playwright/utils/configs'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import { test, expect } from 'playwright/lib'; import SearchResults from './SearchResults'; test.describe('search by name ', () => { - const extendedTest = test.extend({ - context: contextWithEnvs([ - { name: 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', value: '' }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ]) as any, - }); - - extendedTest('+@mobile +@dark-mode', async({ mount, page }) => { + test('+@mobile +@dark-mode', async({ render, mockApiResponse, mockAssetResponse, mockEnvs }) => { const hooksConfig = { router: { query: { q: 'o' }, }, }; - await page.route(buildApiUrl('search') + '?q=o', (route) => route.fulfill({ - status: 200, - body: JSON.stringify({ - items: [ - searchMock.token1, - searchMock.token2, - searchMock.contract1, - searchMock.address2, - searchMock.label1, - ], - }), - })); - await page.route(searchMock.token1.icon_url as string, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); - - const component = await mount( - - - , - { hooksConfig }, - ); + const data = { + items: [ + searchMock.token1, + searchMock.token2, + searchMock.contract1, + searchMock.address2, + searchMock.label1, + ], + next_page_params: null, + }; + await mockEnvs([ + [ 'NEXT_PUBLIC_MARKETPLACE_ENABLED', 'false' ], + ]); + await mockApiResponse('search', data, { queryParams: { q: 'o' } }); + await mockAssetResponse(searchMock.token1.icon_url as string, './playwright/mocks/image_s.jpg'); + const component = await render(, { hooksConfig }); await expect(component.locator('main')).toHaveScreenshot(); }); }); -test('search by address hash +@mobile', async({ mount, page }) => { +test('search by address hash +@mobile', async({ render, mockApiResponse }) => { const hooksConfig = { router: { query: { q: searchMock.address1.address }, }, }; - await page.route(buildApiUrl('search') + `?q=${ searchMock.address1.address }`, (route) => route.fulfill({ - status: 200, - body: JSON.stringify({ - items: [ - searchMock.address1, - ], - }), - })); + const data = { + items: [ searchMock.address1 ], + next_page_params: null, + }; + await mockApiResponse('search', data, { queryParams: { q: searchMock.address1.address } }); - const component = await mount( - - - , - { hooksConfig }, - ); + const component = await render(, { hooksConfig }); await expect(component.locator('main')).toHaveScreenshot(); }); -test('search by block number +@mobile', async({ mount, page }) => { +test('search by block number +@mobile', async({ render, mockApiResponse }) => { const hooksConfig = { router: { query: { q: String(searchMock.block1.block_number) }, }, }; - await page.route(buildApiUrl('search') + `?q=${ searchMock.block1.block_number }`, (route) => route.fulfill({ - status: 200, - body: JSON.stringify({ - items: [ - searchMock.block1, - searchMock.block2, - searchMock.block3, - ], - }), - })); - - const component = await mount( - - - , - { hooksConfig }, - ); + const data = { + items: [ searchMock.block1, searchMock.block2, searchMock.block3 ], + next_page_params: null, + }; + await mockApiResponse('search', data, { queryParams: { q: searchMock.block1.block_number } }); + const component = await render(, { hooksConfig }); await expect(component.locator('main')).toHaveScreenshot(); }); -test('search by block hash +@mobile', async({ mount, page }) => { +test('search by block hash +@mobile', async({ render, mockApiResponse }) => { const hooksConfig = { router: { query: { q: searchMock.block1.block_hash }, }, }; - await page.route(buildApiUrl('search') + `?q=${ searchMock.block1.block_hash }`, (route) => route.fulfill({ - status: 200, - body: JSON.stringify({ - items: [ - searchMock.block1, - ], - }), - })); - - const component = await mount( - - - , - { hooksConfig }, - ); + const data = { + items: [ searchMock.block1 ], + next_page_params: null, + }; + await mockApiResponse('search', data, { queryParams: { q: searchMock.block1.block_hash } }); + const component = await render(, { hooksConfig }); await expect(component.locator('main')).toHaveScreenshot(); }); -test('search by tx hash +@mobile', async({ mount, page }) => { +test('search by tx hash +@mobile', async({ render, mockApiResponse }) => { const hooksConfig = { router: { query: { q: searchMock.tx1.tx_hash }, }, }; - await page.route(buildApiUrl('search') + `?q=${ searchMock.tx1.tx_hash }`, (route) => route.fulfill({ - status: 200, - body: JSON.stringify({ - items: [ - searchMock.tx1, - ], - }), - })); + const data = { + items: [ searchMock.tx1 ], + next_page_params: null, + }; + await mockApiResponse('search', data, { queryParams: { q: searchMock.tx1.tx_hash } }); + const component = await render(, { hooksConfig }); - const component = await mount( - - - , - { hooksConfig }, - ); + await expect(component.locator('main')).toHaveScreenshot(); +}); + +test('search by blob hash +@mobile', async({ render, mockApiResponse }) => { + const hooksConfig = { + router: { + query: { q: searchMock.blob1.blob_hash }, + }, + }; + const data = { + items: [ searchMock.blob1 ], + next_page_params: null, + }; + await mockApiResponse('search', data, { queryParams: { q: searchMock.blob1.blob_hash } }); + const component = await render(, { hooksConfig }); await expect(component.locator('main')).toHaveScreenshot(); }); -const testWithUserOps = test.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.featureEnvs.userOps) as any, +test('search by domain name +@mobile', async({ render, mockApiResponse }) => { + const hooksConfig = { + router: { + query: { q: searchMock.domain1.ens_info.name }, + }, + }; + const data = { + items: [ searchMock.domain1 ], + next_page_params: null, + }; + await mockApiResponse('search', data, { queryParams: { q: searchMock.domain1.ens_info.name } }); + const component = await render(, { hooksConfig }); + await expect(component.locator('main')).toHaveScreenshot(); }); -testWithUserOps('search by user op hash +@mobile', async({ mount, page }) => { +test('search by user op hash +@mobile', async({ render, mockApiResponse, mockEnvs }) => { const hooksConfig = { router: { query: { q: searchMock.userOp1.user_operation_hash }, }, }; - await page.route(buildApiUrl('search') + `?q=${ searchMock.userOp1.user_operation_hash }`, (route) => route.fulfill({ - status: 200, - body: JSON.stringify({ - items: [ - searchMock.userOp1, - ], - }), - })); - - const component = await mount( - - - , - { hooksConfig }, - ); + const data = { + items: [ searchMock.userOp1 ], + next_page_params: null, + }; + await mockEnvs(ENVS_MAP.userOps); + await mockApiResponse('search', data, { queryParams: { q: searchMock.userOp1.user_operation_hash } }); + const component = await render(, { hooksConfig }); await expect(component.locator('main')).toHaveScreenshot(); }); test.describe('with apps', () => { - const MARKETPLACE_CONFIG_URL = app.url + buildExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', 'https://marketplace-config.json') || ''; - const extendedTest = test.extend({ - context: contextWithEnvs([ - { name: 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', value: MARKETPLACE_CONFIG_URL }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ]) as any, - }); - - extendedTest('default view +@mobile', async({ mount, page }) => { + test('default view +@mobile', async({ render, mockApiResponse, mockConfigResponse, mockAssetResponse, mockEnvs }) => { + const MARKETPLACE_CONFIG_URL = 'https://marketplace-config.json'; const hooksConfig = { router: { query: { q: 'o' }, }, }; - const API_URL = buildApiUrl('search') + '?q=o'; - await page.route(API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify({ - items: [ - searchMock.token1, - ], - next_page_params: { foo: 'bar' }, - }), - })); - - await page.route(MARKETPLACE_CONFIG_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(appsMock), - })); - - await page.route(appsMock[0].logo, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); - await page.route(appsMock[1].logo as string, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); - - const component = await mount( - - - , - { hooksConfig }, - ); + const data = { + items: [ searchMock.token1 ], + next_page_params: { + address_hash: null, + block_hash: null, + holder_count: null, + inserted_at: null, + item_type: 'token' as const, + items_count: 1, + name: 'foo', + q: 'o', + tx_hash: null, + }, + }; + await mockEnvs([ + [ 'NEXT_PUBLIC_MARKETPLACE_ENABLED', 'true' ], + [ 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL ], + ]); + await mockApiResponse('search', data, { queryParams: { q: 'o' } }); + await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL, JSON.stringify(appsMock)); + await mockAssetResponse(appsMock[0].logo, './playwright/mocks/image_s.jpg'); + await mockAssetResponse(appsMock[1].logo, './playwright/mocks/image_s.jpg'); + const component = await render(, { hooksConfig }); await expect(component.locator('main')).toHaveScreenshot(); }); diff --git a/ui/pages/SearchResults.tsx b/ui/pages/SearchResults.tsx index 872d20f607..608956a9df 100644 --- a/ui/pages/SearchResults.tsx +++ b/ui/pages/SearchResults.tsx @@ -58,6 +58,11 @@ const SearchResultsPageContent = () => { router.replace({ pathname: '/op/[hash]', query: { hash: redirectCheckQuery.data.parameter } }); return; } + break; + } + case 'blob': { + router.replace({ pathname: '/blobs/[hash]', query: { hash: redirectCheckQuery.data.parameter } }); + return; } } } diff --git a/ui/pages/ShibariumDeposits.pw.tsx b/ui/pages/ShibariumDeposits.pw.tsx index 75fa82ba5e..a52dfb557e 100644 --- a/ui/pages/ShibariumDeposits.pw.tsx +++ b/ui/pages/ShibariumDeposits.pw.tsx @@ -1,43 +1,18 @@ -import { test as base, expect } from '@playwright/experimental-ct-react'; import React from 'react'; import { data as depositsData } from 'mocks/shibarium/deposits'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; -import buildApiUrl from 'playwright/utils/buildApiUrl'; -import * as configs from 'playwright/utils/configs'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import { test, expect } from 'playwright/lib'; import ShibariumDeposits from './ShibariumDeposits'; -const DEPOSITS_API_URL = buildApiUrl('shibarium_deposits'); -const DEPOSITS_COUNT_API_URL = buildApiUrl('shibarium_deposits_count'); +test('base view +@mobile', async({ render, mockApiResponse, mockEnvs, mockTextAd }) => { + await mockTextAd(); + await mockEnvs(ENVS_MAP.shibariumRollup); + await mockApiResponse('shibarium_deposits', depositsData); + await mockApiResponse('shibarium_deposits_count', 3971111); -const test = base.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.featureEnvs.shibariumRollup) as any, -}); - -test('base view +@mobile', async({ mount, page }) => { - await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ - status: 200, - body: '', - })); - - await page.route(DEPOSITS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(depositsData), - })); - - await page.route(DEPOSITS_COUNT_API_URL, (route) => route.fulfill({ - status: 200, - body: '3971111', - })); - - const component = await mount( - - - , - ); + const component = await render(); await expect(component).toHaveScreenshot(); }); diff --git a/ui/pages/ShibariumDeposits.tsx b/ui/pages/ShibariumDeposits.tsx index 4e9160f6b3..d7d981a6e0 100644 --- a/ui/pages/ShibariumDeposits.tsx +++ b/ui/pages/ShibariumDeposits.tsx @@ -75,7 +75,7 @@ const L2Deposits = () => { diff --git a/ui/pages/ShibariumWithdrawals.pw.tsx b/ui/pages/ShibariumWithdrawals.pw.tsx index 98d3ff3836..9310892bd8 100644 --- a/ui/pages/ShibariumWithdrawals.pw.tsx +++ b/ui/pages/ShibariumWithdrawals.pw.tsx @@ -1,47 +1,23 @@ -import { test as base, expect } from '@playwright/experimental-ct-react'; import React from 'react'; import { data as withdrawalsData } from 'mocks/shibarium/withdrawals'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; -import buildApiUrl from 'playwright/utils/buildApiUrl'; -import * as configs from 'playwright/utils/configs'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import { test, expect } from 'playwright/lib'; -import ShibariuWithdrawals from './ShibariumWithdrawals'; +import ShibariumWithdrawals from './ShibariumWithdrawals'; -const test = base.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.featureEnvs.shibariumRollup) as any, -}); - -const WITHDRAWALS_API_URL = buildApiUrl('shibarium_withdrawals'); -const WITHDRAWALS_COUNT_API_URL = buildApiUrl('shibarium_withdrawals_count'); - -test('base view +@mobile', async({ mount, page }) => { +test('base view +@mobile', async({ render, mockApiResponse, mockEnvs, mockTextAd }) => { // test on mobile is flaky // my assumption is there is not enough time to calculate hashes truncation so component is unstable // so I raised the test timeout to check if it helps test.slow(); - await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ - status: 200, - body: '', - })); - - await page.route(WITHDRAWALS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(withdrawalsData), - })); - await page.route(WITHDRAWALS_COUNT_API_URL, (route) => route.fulfill({ - status: 200, - body: '397', - })); + await mockTextAd(); + await mockEnvs(ENVS_MAP.shibariumRollup); + await mockApiResponse('shibarium_withdrawals', withdrawalsData); + await mockApiResponse('shibarium_withdrawals_count', 397); - const component = await mount( - - - , - ); + const component = await render(); await expect(component).toHaveScreenshot({ timeout: 10_000 }); }); diff --git a/ui/pages/Stats.tsx b/ui/pages/Stats.tsx index 228226df27..d419ee3c52 100644 --- a/ui/pages/Stats.tsx +++ b/ui/pages/Stats.tsx @@ -21,6 +21,7 @@ const Stats = () => { handleFilterChange, displayedCharts, filterQuery, + initialFilterQuery, } = useStats(); return ( @@ -33,6 +34,8 @@ const Stats = () => { { ({ - createSocket: socketServer.createSocket, -}); - // FIXME // test cases which use socket cannot run in parallel since the socket server always run on the same port test.describe.configure({ mode: 'serial' }); -test.beforeEach(async({ page }) => { - await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ - status: 200, - body: '', - })); - - await page.route(TOKEN_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(tokenInfo), - })); - await page.route(ADDRESS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(contract), - })); - await page.route(TOKEN_COUNTERS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(tokenCounters), - })); - await page.route(TOKEN_TRANSFERS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify({}), - })); +test.beforeEach(async({ mockApiResponse, mockTextAd }) => { + await mockApiResponse('token', tokenInfo, { pathParams: { hash } }); + await mockApiResponse('address', contract, { pathParams: { hash } }); + await mockApiResponse('token_counters', tokenCounters, { pathParams: { hash } }); + await mockApiResponse('token_transfers', { items: [], next_page_params: null }, { pathParams: { hash } }); + await mockTextAd(); }); -test('base view', async({ mount, page, createSocket }) => { - const component = await mount( - - - , - { hooksConfig }, - ); +test('base view', async({ render, page, createSocket }) => { + const component = await render(, { hooksConfig }, { withSocket: true }); const socket = await createSocket(); - const channel = await socketServer.joinChannel(socket, 'tokens:1'); + const channel = await socketServer.joinChannel(socket, `tokens:${ hash }`); socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 }); await expect(component).toHaveScreenshot({ @@ -73,27 +46,14 @@ test('base view', async({ mount, page, createSocket }) => { }); }); -test('with verified info', async({ mount, page, createSocket }) => { - const VERIFIED_INFO_URL = buildApiUrl('token_verified_info', { chainId: '1', hash: '1' }); - await page.route(VERIFIED_INFO_URL, (route) => route.fulfill({ - body: JSON.stringify(verifiedAddressesMocks.TOKEN_INFO_APPLICATION.APPROVED), - })); - await page.route(tokenInfo.icon_url as string, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); +test('with verified info', async({ render, page, createSocket, mockApiResponse, mockAssetResponse }) => { + await mockApiResponse('token_verified_info', verifiedAddressesMocks.TOKEN_INFO_APPLICATION.APPROVED, { pathParams: { chainId, hash } }); + await mockAssetResponse(tokenInfo.icon_url as string, './playwright/mocks/image_s.jpg'); - const component = await mount( - - - , - { hooksConfig }, - ); + const component = await render(, { hooksConfig }, { withSocket: true }); const socket = await createSocket(); - const channel = await socketServer.joinChannel(socket, 'tokens:1'); + const channel = await socketServer.joinChannel(socket, `tokens:${ hash }`); socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 }); await page.getByRole('button', { name: /project info/i }).click(); @@ -104,57 +64,25 @@ test('with verified info', async({ mount, page, createSocket }) => { }); }); -const bridgedTokenTest = base.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.featureEnvs.bridgedTokens) as any, - createSocket: socketServer.createSocket, -}); - -bridgedTokenTest('bridged token', async({ mount, page, createSocket }) => { - - const VERIFIED_INFO_URL = buildApiUrl('token_verified_info', { chainId: '1', hash: '1' }); - - await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ - status: 200, - body: '', - })); - - await page.route(TOKEN_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(bridgedTokenA), - })); - await page.route(ADDRESS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(contract), - })); - await page.route(TOKEN_COUNTERS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(tokenCounters), - })); - await page.route(TOKEN_TRANSFERS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify({}), - })); - await page.route(VERIFIED_INFO_URL, (route) => route.fulfill({ - body: JSON.stringify(verifiedAddressesMocks.TOKEN_INFO_APPLICATION.APPROVED), - })); - - await page.route(tokenInfo.icon_url as string, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); - - const component = await mount( - - - , - { hooksConfig }, - ); - +test('bridged token', async({ render, page, createSocket, mockApiResponse, mockAssetResponse, mockEnvs }) => { + const hash = bridgedTokenA.address; + const hooksConfig = { + router: { + query: { hash, tab: 'token_transfers' }, + }, + }; + + await mockEnvs(ENVS_MAP.bridgedTokens); + await mockApiResponse('token', bridgedTokenA, { pathParams: { hash } }); + await mockApiResponse('address', contract, { pathParams: { hash } }); + await mockApiResponse('token_counters', tokenCounters, { pathParams: { hash } }); + await mockApiResponse('token_transfers', { items: [], next_page_params: null }, { pathParams: { hash } }); + await mockApiResponse('token_verified_info', verifiedAddressesMocks.TOKEN_INFO_APPLICATION.APPROVED, { pathParams: { chainId, hash } }); + await mockAssetResponse(tokenInfo.icon_url as string, './playwright/mocks/image_s.jpg'); + + const component = await render(, { hooksConfig }, { withSocket: true }); const socket = await createSocket(); - const channel = await socketServer.joinChannel(socket, 'tokens:1'); + const channel = await socketServer.joinChannel(socket, `tokens:${ hash.toLowerCase() }`); socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 }); await expect(component).toHaveScreenshot({ @@ -165,16 +93,11 @@ bridgedTokenTest('bridged token', async({ mount, page, createSocket }) => { test.describe('mobile', () => { test.use({ viewport: devices['iPhone 13 Pro'].viewport }); - test('base view', async({ mount, page, createSocket }) => { - const component = await mount( - - - , - { hooksConfig }, - ); + test('base view', async({ render, page, createSocket }) => { + const component = await render(, { hooksConfig }, { withSocket: true }); const socket = await createSocket(); - const channel = await socketServer.joinChannel(socket, 'tokens:1'); + const channel = await socketServer.joinChannel(socket, `tokens:${ hash }`); socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 }); await expect(component).toHaveScreenshot({ @@ -183,27 +106,13 @@ test.describe('mobile', () => { }); }); - test('with verified info', async({ mount, page, createSocket }) => { - const VERIFIED_INFO_URL = buildApiUrl('token_verified_info', { chainId: '1', hash: '1' }); - await page.route(VERIFIED_INFO_URL, (route) => route.fulfill({ - body: JSON.stringify(verifiedAddressesMocks.TOKEN_INFO_APPLICATION.APPROVED), - })); - await page.route(tokenInfo.icon_url as string, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); - - const component = await mount( - - - , - { hooksConfig }, - ); + test('with verified info', async({ render, page, createSocket, mockApiResponse, mockAssetResponse }) => { + await mockApiResponse('token_verified_info', verifiedAddressesMocks.TOKEN_INFO_APPLICATION.APPROVED, { pathParams: { chainId, hash } }); + await mockAssetResponse(tokenInfo.icon_url as string, './playwright/mocks/image_s.jpg'); + const component = await render(, { hooksConfig }, { withSocket: true }); const socket = await createSocket(); - const channel = await socketServer.joinChannel(socket, 'tokens:1'); + const channel = await socketServer.joinChannel(socket, `tokens:${ hash }`); socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 }); await expect(component).toHaveScreenshot({ diff --git a/ui/pages/Token.tsx b/ui/pages/Token.tsx index 62d2ef89d0..0ca0d57dde 100644 --- a/ui/pages/Token.tsx +++ b/ui/pages/Token.tsx @@ -1,4 +1,4 @@ -import { Box, Flex, Tooltip } from '@chakra-ui/react'; +import { Box } from '@chakra-ui/react'; import { useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'next/router'; import React, { useEffect } from 'react'; @@ -10,47 +10,44 @@ import type { RoutedTab } from 'ui/shared/Tabs/types'; import config from 'configs/app'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; -import { useAppContext } from 'lib/contexts/app'; import useContractTabs from 'lib/hooks/useContractTabs'; import useIsMobile from 'lib/hooks/useIsMobile'; import * as metadata from 'lib/metadata'; import getQueryParamString from 'lib/router/getQueryParamString'; import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketMessage from 'lib/socket/useSocketMessage'; +import { NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes'; import * as addressStubs from 'stubs/address'; import * as tokenStubs from 'stubs/token'; +import { getTokenHoldersStub } from 'stubs/token'; import { generateListStub } from 'stubs/utils'; import AddressContract from 'ui/address/AddressContract'; -import AddressQrCode from 'ui/address/details/AddressQrCode'; -import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu'; -//import TextAd from 'ui/shared/ad/TextAd'; -import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; -import * as TokenEntity from 'ui/shared/entities/token/TokenEntity'; -import EntityTags from 'ui/shared/EntityTags'; +import AddressCsvExportLink from 'ui/address/AddressCsvExportLink'; import IconSvg from 'ui/shared/IconSvg'; -import NetworkExplorers from 'ui/shared/NetworkExplorers'; -import PageTitle from 'ui/shared/Page/PageTitle'; import Pagination from 'ui/shared/pagination/Pagination'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; -import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton'; import TokenDetails from 'ui/token/TokenDetails'; import TokenHolders from 'ui/token/TokenHolders/TokenHolders'; import TokenInventory from 'ui/token/TokenInventory'; +import TokenPageTitle from 'ui/token/TokenPageTitle'; import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer'; -import TokenVerifiedInfo from 'ui/token/TokenVerifiedInfo'; +import useTokenQuery from 'ui/token/useTokenQuery'; export type TokenTabs = 'token_transfers' | 'holders' | 'inventory'; +const TABS_RIGHT_SLOT_PROPS = { + display: 'flex', + alignItems: 'center', + columnGap: 4, +}; + const TokenPageContent = () => { const [ isQueryEnabled, setIsQueryEnabled ] = React.useState(false); const [ totalSupplySocket, setTotalSupplySocket ] = React.useState(); const router = useRouter(); const isMobile = useIsMobile(); - const appProps = useAppContext(); - const scrollRef = React.useRef(null); const hashString = getQueryParamString(router.query.hash); @@ -59,15 +56,9 @@ const TokenPageContent = () => { const queryClient = useQueryClient(); - const tokenQuery = useApiQuery('token', { - pathParams: { hash: hashString }, - queryOptions: { - enabled: Boolean(router.query.hash), - placeholderData: tokenStubs.TOKEN_INFO_ERC_20, - }, - }); + const tokenQuery = useTokenQuery(hashString); - const contractQuery = useApiQuery('address', { + const addressQuery = useApiQuery('address', { pathParams: { hash: hashString }, queryOptions: { enabled: isQueryEnabled && Boolean(router.query.hash), @@ -112,13 +103,13 @@ const TokenPageContent = () => { }); useEffect(() => { - if (tokenQuery.data && !tokenQuery.isPlaceholderData) { - metadata.update({ pathname: '/token/[hash]', query: { hash: tokenQuery.data.address } }, { symbol: tokenQuery.data.symbol ?? '' }); + if (tokenQuery.data && !tokenQuery.isPlaceholderData && !config.meta.seo.enhancedDataEnabled) { + metadata.update({ pathname: '/token/[hash]', query: { hash: tokenQuery.data.address } }, tokenQuery.data); } }, [ tokenQuery.data, tokenQuery.isPlaceholderData ]); - const hasData = (tokenQuery.data && !tokenQuery.isPlaceholderData) && (contractQuery.data && !contractQuery.isPlaceholderData); - const hasInventoryTab = tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721'; + const hasData = (tokenQuery.data && !tokenQuery.isPlaceholderData) && (addressQuery.data && !addressQuery.isPlaceholderData); + const hasInventoryTab = tokenQuery.data?.type && NFT_TOKEN_TYPE_IDS.includes(tokenQuery.data.type); const transfersQuery = useQueryWithPages({ resourceName: 'token_transfers', @@ -161,30 +152,33 @@ const TokenPageContent = () => { scrollRef, options: { enabled: Boolean(hashString && tab === 'holders' && hasData), - placeholderData: generateListStub<'token_holders'>( - tokenQuery.data?.type === 'ERC-1155' ? tokenStubs.TOKEN_HOLDER_ERC_1155 : tokenStubs.TOKEN_HOLDER_ERC_20, 50, { next_page_params: null }), + placeholderData: getTokenHoldersStub(tokenQuery.data?.type, null), }, }); - const verifiedInfoQuery = useApiQuery('token_verified_info', { - pathParams: { hash: hashString, chainId: config.chain.id }, - queryOptions: { enabled: Boolean(tokenQuery.data) && config.features.verifiedTokens.isEnabled }, - }); - - const contractTabs = useContractTabs(contractQuery.data); + const isLoading = tokenQuery.isPlaceholderData || addressQuery.isPlaceholderData; + const contractTabs = useContractTabs(addressQuery.data, addressQuery.isPlaceholderData); const tabs: Array = [ - (tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721') ? { + hasInventoryTab ? { id: 'inventory', title: 'Inventory', - component: , + component: , } : undefined, - { id: 'token_transfers', title: 'Token transfers', component: }, - { id: 'holders', title: 'Holders', component: }, - contractQuery.data?.is_contract ? { + { + id: 'token_transfers', + title: 'Token transfers', + component: , + }, + { + id: 'holders', + title: 'Holders', + component: , + }, + addressQuery.data?.is_contract ? { id: 'contract', title: () => { - if (contractQuery.data?.is_verified) { + if (addressQuery.data?.is_verified) { return ( <> Contract @@ -195,8 +189,8 @@ const TokenPageContent = () => { return 'Contract'; }, - component: , - subTabs: contractTabs.map(tab => tab.id), + component: , + subTabs: contractTabs.tabs.map(tab => tab.id), } : undefined, ].filter(Boolean); @@ -212,12 +206,10 @@ const TokenPageContent = () => { } // default tab for nfts is token inventory - if (((tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721') && !tab) || tab === 'inventory') { + if ((hasInventoryTab && !tab) || tab === 'inventory') { pagination = inventoryQuery.pagination; } - const tokenSymbolText = tokenQuery.data?.symbol ? ` (${ tokenQuery.data.symbol })` : ''; - const tabListProps = React.useCallback(({ isSticky, activeTabIndex }: { isSticky: boolean; activeTabIndex: number }) => { if (isMobile) { return { mt: 8 }; @@ -231,100 +223,44 @@ const TokenPageContent = () => { }; }, [ isMobile ]); - const backLink = React.useMemo(() => { - const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/tokens'); - - if (!hasGoBackLink) { - return; + const tabsRightSlot = React.useMemo(() => { + if (isMobile) { + return null; } - return { - label: 'Back to tokens list', - url: appProps.referrer, - }; - }, [ appProps.referrer ]); - - const titleContentAfter = ( - <> - { verifiedInfoQuery.data?.tokenAddress && ( - - - - - - ) } - - - ); - - const isLoading = tokenQuery.isPlaceholderData || contractQuery.isPlaceholderData; - - const titleSecondRow = ( - - - { !isLoading && tokenQuery.data && } - - - - - - - - ); + return ( + <> + { tab === 'holders' && ( + + ) } + { pagination?.isVisible && } + + ); + }, [ hashString, isMobile, pagination, tab ]); return ( <> - - ) : null } - contentAfter={ titleContentAfter } - secondRow={ titleSecondRow } - /> + + + { /* should stay before tabs to scroll up with pagination */ } - { isLoading ? - : - ( - : null } - stickyEnabled={ !isMobile } - /> - ) } + ); }; diff --git a/ui/pages/TokenInstance.tsx b/ui/pages/TokenInstance.tsx index 94f4a7624c..7529ccefb3 100644 --- a/ui/pages/TokenInstance.tsx +++ b/ui/pages/TokenInstance.tsx @@ -11,9 +11,12 @@ import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; import useIsMobile from 'lib/hooks/useIsMobile'; import * as metadata from 'lib/metadata'; import * as regexp from 'lib/regexp'; -import { TOKEN_INSTANCE, TOKEN_INFO_ERC_1155 } from 'stubs/token'; -import * as tokenStubs from 'stubs/token'; -import { generateListStub } from 'stubs/utils'; +import { + TOKEN_INSTANCE, + TOKEN_INFO_ERC_1155, + getTokenInstanceTransfersStub, + getTokenInstanceHoldersStub, +} from 'stubs/token'; import AddressQrCode from 'ui/address/details/AddressQrCode'; import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu'; //import TextAd from 'ui/shared/ad/TextAd'; @@ -66,11 +69,7 @@ const TokenInstanceContent = () => { scrollRef, options: { enabled: Boolean(hash && id && (!tab || tab === 'token_transfers') && !tokenInstanceQuery.isPlaceholderData && tokenInstanceQuery.data), - placeholderData: generateListStub<'token_instance_transfers'>( - tokenQuery.data?.type === 'ERC-1155' ? tokenStubs.TOKEN_TRANSFER_ERC_1155 : tokenStubs.TOKEN_TRANSFER_ERC_721, - 10, - { next_page_params: null }, - ), + placeholderData: getTokenInstanceTransfersStub(tokenQuery.data?.type, null), }, }); @@ -86,8 +85,7 @@ const TokenInstanceContent = () => { scrollRef, options: { enabled: Boolean(hash && tab === 'holders' && shouldFetchHolders), - placeholderData: generateListStub<'token_instance_holders'>( - tokenQuery.data?.type === 'ERC-1155' ? tokenStubs.TOKEN_HOLDER_ERC_1155 : tokenStubs.TOKEN_HOLDER_ERC_20, 10, { next_page_params: null }), + placeholderData: getTokenInstanceHoldersStub(tokenQuery.data?.type, null), }, }); @@ -176,20 +174,34 @@ const TokenInstanceContent = () => { pagination = holdersQuery.pagination; } + const title = (() => { + if (typeof tokenInstanceQuery.data?.metadata?.name === 'string') { + return tokenInstanceQuery.data.metadata.name; + } + + if (tokenQuery.data?.symbol) { + return (tokenQuery.data.name || tokenQuery.data.symbol) + ' #' + tokenInstanceQuery.data?.id; + } + + return `ID ${ tokenInstanceQuery.data?.id }`; + })(); + const titleSecondRow = ( - + { tokenQuery.data && ( + + ) } { !isLoading && tokenInstanceQuery.data && } @@ -200,7 +212,7 @@ const TokenInstanceContent = () => { return ( <> { - await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ - status: 200, - body: JSON.stringify(textAdMock.duck), - })); - await page.route(textAdMock.duck.ad.thumbnail, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); +test.beforeEach(async({ mockTextAd }) => { + await mockTextAd(); }); -base('base view +@mobile +@dark-mode', async({ mount, page }) => { +test('base view +@mobile +@dark-mode', async({ render, mockApiResponse }) => { const allTokens = { items: [ tokens.tokenInfoERC20a, tokens.tokenInfoERC20b, tokens.tokenInfoERC20c, tokens.tokenInfoERC20d, @@ -35,6 +22,7 @@ base('base view +@mobile +@dark-mode', async({ mount, page }) => { holder_count: 1, items_count: 1, name: 'a', + market_cap: '0', }, }; const filteredTokens = { @@ -44,40 +32,25 @@ base('base view +@mobile +@dark-mode', async({ mount, page }) => { next_page_params: null, }; - const ALL_TOKENS_API_URL = buildApiUrl('tokens'); - const FILTERED_TOKENS_API_URL = buildApiUrl('tokens') + '?q=foo'; - - await page.route(ALL_TOKENS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(allTokens), - })); + await mockApiResponse('tokens', allTokens); + await mockApiResponse('tokens', filteredTokens, { queryParams: { q: 'foo' } }); - await page.route(FILTERED_TOKENS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(filteredTokens), - })); - - const component = await mount( - + const component = await render( +
- , +
, ); await expect(component).toHaveScreenshot(); await component.getByRole('textbox', { name: 'Token name or symbol' }).focus(); - await component.getByRole('textbox', { name: 'Token name or symbol' }).type('foo'); + await component.getByRole('textbox', { name: 'Token name or symbol' }).fill('foo'); await expect(component).toHaveScreenshot(); }); -base.describe('bridged tokens', async() => { - const test = base.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.featureEnvs.bridgedTokens) as any, - }); - +test.describe('bridged tokens', async() => { const bridgedTokens = { items: [ tokens.bridgedTokenA, @@ -88,6 +61,7 @@ base.describe('bridged tokens', async() => { holder_count: 1, items_count: 1, name: 'a', + market_cap: null, }, }; const bridgedFilteredTokens = { @@ -101,26 +75,17 @@ base.describe('bridged tokens', async() => { query: { tab: 'bridged' }, }, }; - const BRIDGED_TOKENS_API_URL = buildApiUrl('tokens_bridged'); - - test.beforeEach(async({ page }) => { - await page.route(BRIDGED_TOKENS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(bridgedTokens), - })); - }); - test('base view', async({ mount, page }) => { - await page.route(BRIDGED_TOKENS_API_URL + '?chain_ids=99', (route) => route.fulfill({ - status: 200, - body: JSON.stringify(bridgedFilteredTokens), - })); + test('base view', async({ render, page, mockApiResponse, mockEnvs }) => { + await mockEnvs(ENVS_MAP.bridgedTokens); + await mockApiResponse('tokens_bridged', bridgedTokens); + await mockApiResponse('tokens_bridged', bridgedFilteredTokens, { queryParams: { chain_ids: '99' } }); - const component = await mount( - + const component = await render( +
- , +
, { hooksConfig }, ); diff --git a/ui/pages/Transaction.tsx b/ui/pages/Transaction.tsx index 5c70276a5a..65dea57d3f 100644 --- a/ui/pages/Transaction.tsx +++ b/ui/pages/Transaction.tsx @@ -8,12 +8,14 @@ import { useAppContext } from 'lib/contexts/app'; import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; import getQueryParamString from 'lib/router/getQueryParamString'; import { publicClient } from 'lib/web3/client'; -//import TextAd from 'ui/shared/ad/TextAd'; -import EntityTags from 'ui/shared/EntityTags'; +import isCustomAppError from 'ui/shared/AppError/isCustomAppError'; +import EntityTags from 'ui/shared/EntityTags/EntityTags'; import PageTitle from 'ui/shared/Page/PageTitle'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton'; import useTabIndexFromQuery from 'ui/shared/Tabs/useTabIndexFromQuery'; +import TxAssetFlows from 'ui/tx/TxAssetFlows'; +import TxBlobs from 'ui/tx/TxBlobs'; import TxDetails from 'ui/tx/TxDetails'; import TxDetailsDegraded from 'ui/tx/TxDetailsDegraded'; import TxDetailsWrapped from 'ui/tx/TxDetailsWrapped'; @@ -26,6 +28,8 @@ import TxTokenTransfer from 'ui/tx/TxTokenTransfer'; import TxUserOps from 'ui/tx/TxUserOps'; import useTxQuery from 'ui/tx/useTxQuery'; +const txInterpretation = config.features.txInterpretation; + const TransactionPageContent = () => { const router = useRouter(); const appProps = useAppContext(); @@ -34,7 +38,7 @@ const TransactionPageContent = () => { const txQuery = useTxQuery(); const { data, isPlaceholderData, isError, error, errorUpdateCount } = txQuery; - const showDegradedView = publicClient && (isError || isPlaceholderData) && errorUpdateCount > 0; + const showDegradedView = publicClient && ((isError && error.status !== 422) || isPlaceholderData) && errorUpdateCount > 0; const tabs: Array = (() => { const detailsComponent = showDegradedView ? @@ -47,6 +51,9 @@ const TransactionPageContent = () => { title: config.features.suave.isEnabled && data?.wrapped ? 'Confidential compute tx details' : 'Details', component: detailsComponent, }, + txInterpretation.isEnabled && txInterpretation.provider === 'noves' ? + { id: 'asset_flows', title: 'Asset Flows', component: } : + undefined, config.features.suave.isEnabled && data?.wrapped ? { id: 'wrapped', title: 'Regular tx details', component: } : undefined, @@ -55,6 +62,9 @@ const TransactionPageContent = () => { { id: 'user_ops', title: 'User operations', component: } : undefined, { id: 'internal', title: 'Internal txns', component: }, + config.features.dataAvailability.isEnabled && txQuery.data?.blob_versioned_hashes?.length ? + { id: 'blobs', title: 'Blobs', component: } : + undefined, { id: 'logs', title: 'Logs', component: }, { id: 'state', title: 'State', component: }, { id: 'raw_trace', title: 'Raw trace', component: }, @@ -66,7 +76,7 @@ const TransactionPageContent = () => { const tags = ( ); @@ -99,7 +109,7 @@ const TransactionPageContent = () => { })(); if (isError && !showDegradedView) { - if (error?.status === 422 || error?.status === 404) { + if (isCustomAppError(error)) { throwOnResourceLoadError({ resource: 'tx', error, isError: true }); } } diff --git a/ui/pages/Transactions.tsx b/ui/pages/Transactions.tsx index 2d7027aa47..23a3566c5d 100644 --- a/ui/pages/Transactions.tsx +++ b/ui/pages/Transactions.tsx @@ -7,12 +7,14 @@ import config from 'configs/app'; import useHasAccount from 'lib/hooks/useHasAccount'; import useIsMobile from 'lib/hooks/useIsMobile'; import useNewTxsSocket from 'lib/hooks/useNewTxsSocket'; +import getQueryParamString from 'lib/router/getQueryParamString'; import { TX } from 'stubs/tx'; import { generateListStub } from 'stubs/utils'; import PageTitle from 'ui/shared/Page/PageTitle'; import Pagination from 'ui/shared/pagination/Pagination'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; +import TxsStats from 'ui/txs/TxsStats'; import TxsWatchlist from 'ui/txs/TxsWatchlist'; import TxsWithFrontendSorting from 'ui/txs/TxsWithFrontendSorting'; @@ -26,11 +28,13 @@ const Transactions = () => { const verifiedTitle = config.chain.verificationType === 'validation' ? 'Validated' : 'Mined'; const router = useRouter(); const isMobile = useIsMobile(); - const txsQuery = useQueryWithPages({ - resourceName: router.query.tab === 'pending' ? 'txs_pending' : 'txs_validated', - filters: { filter: router.query.tab === 'pending' ? 'pending' : 'validated' }, + const tab = getQueryParamString(router.query.tab); + + const txsValidatedQuery = useQueryWithPages({ + resourceName: 'txs_validated', + filters: { filter: 'validated' }, options: { - enabled: !router.query.tab || router.query.tab === 'validated' || router.query.tab === 'pending', + enabled: !tab || tab === 'validated', placeholderData: generateListStub<'txs_validated'>(TX, 50, { next_page_params: { block_number: 9005713, index: 5, @@ -40,10 +44,36 @@ const Transactions = () => { }, }); + const txsPendingQuery = useQueryWithPages({ + resourceName: 'txs_pending', + filters: { filter: 'pending' }, + options: { + enabled: tab === 'pending', + placeholderData: generateListStub<'txs_pending'>(TX, 50, { next_page_params: { + inserted_at: '2024-02-05T07:04:47.749818Z', + hash: '0x00', + filter: 'pending', + } }), + }, + }); + + const txsWithBlobsQuery = useQueryWithPages({ + resourceName: 'txs_with_blobs', + filters: { type: 'blob_transaction' }, + options: { + enabled: config.features.dataAvailability.isEnabled && tab === 'blob_txs', + placeholderData: generateListStub<'txs_with_blobs'>(TX, 50, { next_page_params: { + block_number: 10602877, + index: 8, + items_count: 50, + } }), + }, + }); + const txsWatchlistQuery = useQueryWithPages({ resourceName: 'txs_watchlist', options: { - enabled: router.query.tab === 'watchlist', + enabled: tab === 'watchlist', placeholderData: generateListStub<'txs_watchlist'>(TX, 50, { next_page_params: { block_number: 9005713, index: 5, @@ -61,15 +91,32 @@ const Transactions = () => { id: 'validated', title: verifiedTitle, component: - }, + }, { id: 'pending', title: 'Pending', component: ( + ), + }, + config.features.dataAvailability.isEnabled && { + id: 'blob_txs', + title: 'Blob txns', + component: ( + @@ -82,11 +129,19 @@ const Transactions = () => { } : undefined, ].filter(Boolean); - const pagination = router.query.tab === 'watchlist' ? txsWatchlistQuery.pagination : txsQuery.pagination; + const pagination = (() => { + switch (tab) { + case 'pending': return txsPendingQuery.pagination; + case 'watchlist': return txsWatchlistQuery.pagination; + case 'blob_txs': return txsWithBlobsQuery.pagination; + default: return txsValidatedQuery.pagination; + } + })(); return ( <> + { - await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ - status: 200, - body: '', - })); - - await page.route(USER_OP_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(userOpData), - })); +const hooksConfig = { + router: { + query: { hash: userOpData.hash }, + isReady: true, + }, +}; - const component = await mount( - - - , - { hooksConfig: { - router: { - query: { hash: userOpData.hash }, - isReady: true, - }, - } }, - ); +test.beforeEach(async({ mockEnvs }) => { + await mockEnvs(ENVS_MAP.userOps); +}); +test('base view', async({ render, mockTextAd, mockApiResponse }) => { + await mockTextAd(); + await mockApiResponse('user_op', userOpData, { pathParams: { hash: userOpData.hash } }); + const component = await render(, { hooksConfig }); await expect(component).toHaveScreenshot(); }); test.describe('mobile', () => { test.use({ viewport: devices['iPhone 13 Pro'].viewport }); - test('base view', async({ mount, page }) => { - await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ - status: 200, - body: '', - })); - - await page.route(USER_OP_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(userOpData), - })); - - const component = await mount( - - - , - { hooksConfig: { - router: { - query: { hash: userOpData.hash }, - isReady: true, - }, - } }, - ); - + test('base view', async({ render, mockTextAd, mockApiResponse }) => { + await mockTextAd(); + await mockApiResponse('user_op', userOpData, { pathParams: { hash: userOpData.hash } }); + const component = await render(, { hooksConfig }); await expect(component).toHaveScreenshot(); }); }); diff --git a/ui/pages/UserOp.tsx b/ui/pages/UserOp.tsx index 0134a2e27e..eb35fb5dfa 100644 --- a/ui/pages/UserOp.tsx +++ b/ui/pages/UserOp.tsx @@ -12,7 +12,6 @@ import throwOnAbsentParamError from 'lib/errors/throwOnAbsentParamError'; import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; import getQueryParamString from 'lib/router/getQueryParamString'; import { USER_OP } from 'stubs/userOps'; -//import TextAd from 'ui/shared/ad/TextAd'; import UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity'; import PageTitle from 'ui/shared/Page/PageTitle'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; @@ -23,6 +22,7 @@ import TxTokenTransfer from 'ui/tx/TxTokenTransfer'; import useTxQuery from 'ui/tx/useTxQuery'; import UserOpDetails from 'ui/userOp/UserOpDetails'; import UserOpRaw from 'ui/userOp/UserOpRaw'; +import UserOpSubHeading from 'ui/userOp/UserOpSubHeading'; const UserOp = () => { const router = useRouter(); @@ -90,7 +90,7 @@ const UserOp = () => { throwOnAbsentParamError(hash); throwOnResourceLoadError(userOpQuery); - const titleSecondRow = ; + const titleSecondRow = ; return ( <> diff --git a/ui/pages/UserOps.pw.tsx b/ui/pages/UserOps.pw.tsx index 4d0cb8fe1d..2a241f1d52 100644 --- a/ui/pages/UserOps.pw.tsx +++ b/ui/pages/UserOps.pw.tsx @@ -1,40 +1,16 @@ import { Box } from '@chakra-ui/react'; -import { test as base, expect } from '@playwright/experimental-ct-react'; import React from 'react'; import { userOpsData } from 'mocks/userOps/userOps'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; -import buildApiUrl from 'playwright/utils/buildApiUrl'; -import * as configs from 'playwright/utils/configs'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import { test, expect } from 'playwright/lib'; import UserOps from './UserOps'; -const test = base.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.featureEnvs.userOps) as any, -}); - -const USER_OPS_API_URL = buildApiUrl('user_ops'); - -test('base view +@mobile', async({ mount, page }) => { - await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ - status: 200, - body: '', - })); - - await page.route(USER_OPS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(userOpsData), - })); - - const component = await mount( - - - - - , - ); - +test('base view +@mobile', async({ render, mockEnvs, mockTextAd, mockApiResponse }) => { + await mockEnvs(ENVS_MAP.userOps); + await mockTextAd(); + await mockApiResponse('user_ops', userOpsData); + const component = await render( ); await expect(component).toHaveScreenshot(); }); diff --git a/ui/pages/Validators.pw.tsx b/ui/pages/Validators.pw.tsx index 3167d0bd7b..605328b313 100644 --- a/ui/pages/Validators.pw.tsx +++ b/ui/pages/Validators.pw.tsx @@ -1,51 +1,21 @@ -import { test as base, expect } from '@playwright/experimental-ct-react'; import React from 'react'; -import * as textAdMock from 'mocks/ad/textAd'; import * as validatorsMock from 'mocks/validators/index'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; -import buildApiUrl from 'playwright/utils/buildApiUrl'; -import * as configs from 'playwright/utils/configs'; +import { test, expect } from 'playwright/lib'; import Validators from './Validators'; -const VALIDATORS_API_URL = buildApiUrl('validators', { chainType: 'stability' }); -const VALIDATORS_COUNTERS_API_URL = buildApiUrl('validators_counters', { chainType: 'stability' }); +const chainType = 'stability'; -const test = base.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.featureEnvs.validators) as any, -}); - -test.beforeEach(async({ page }) => { - await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ - status: 200, - body: JSON.stringify(textAdMock.duck), - })); - await page.route(textAdMock.duck.ad.thumbnail, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); -}); - -test('base view +@mobile', async({ mount, page }) => { - await page.route(VALIDATORS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(validatorsMock.validatorsResponse), - })); - await page.route(VALIDATORS_COUNTERS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(validatorsMock.validatorsCountersResponse), - })); +test('base view +@mobile', async({ render, mockApiResponse, mockEnvs, mockTextAd }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE', chainType ], + ]); + await mockApiResponse('validators', validatorsMock.validatorsResponse, { pathParams: { chainType } }); + await mockApiResponse('validators_counters', validatorsMock.validatorsCountersResponse, { pathParams: { chainType } }); + await mockTextAd(); - const component = await mount( - - - , - ); + const component = await render(); await expect(component).toHaveScreenshot(); }); diff --git a/ui/pages/VerifiedAddresses.pw.tsx b/ui/pages/VerifiedAddresses.pw.tsx index 3c881b21d6..6a147c6cc0 100644 --- a/ui/pages/VerifiedAddresses.pw.tsx +++ b/ui/pages/VerifiedAddresses.pw.tsx @@ -1,69 +1,48 @@ -import { test, expect } from '@playwright/experimental-ct-react'; +import type { BrowserContext } from '@playwright/test'; import React from 'react'; import * as mocks from 'mocks/account/verifiedAddresses'; -import TestApp from 'playwright/TestApp'; -import buildApiUrl from 'playwright/utils/buildApiUrl'; +import * as profileMock from 'mocks/user/profile'; +import { contextWithAuth } from 'playwright/fixtures/auth'; +import { test as base, expect } from 'playwright/lib'; import VerifiedAddresses from './VerifiedAddresses'; -const VERIFIED_ADDRESS_URL = buildApiUrl('verified_addresses', { chainId: '1' }); -const TOKEN_INFO_APPLICATIONS_URL = buildApiUrl('token_info_applications', { chainId: '1', id: undefined }); - -test.beforeEach(async({ context }) => { - await context.route(mocks.TOKEN_INFO_APPLICATION_BASE.iconUrl, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); +const test = base.extend<{ context: BrowserContext }>({ + context: contextWithAuth, }); -test('base view +@mobile', async({ mount, page }) => { - await page.route(VERIFIED_ADDRESS_URL, (route) => route.fulfill({ - body: JSON.stringify(mocks.VERIFIED_ADDRESS_RESPONSE.DEFAULT), - })); - - await page.route(TOKEN_INFO_APPLICATIONS_URL, (route) => route.fulfill({ - body: JSON.stringify(mocks.TOKEN_INFO_APPLICATIONS_RESPONSE.DEFAULT), - })); +test.beforeEach(async({ mockAssetResponse }) => { + await mockAssetResponse(mocks.TOKEN_INFO_APPLICATION_BASE.iconUrl, './playwright/mocks/image_s.jpg'); +}); - const component = await mount( - - - , - ); +test('base view +@mobile', async({ render, mockApiResponse }) => { + await mockApiResponse('verified_addresses', mocks.VERIFIED_ADDRESS_RESPONSE.DEFAULT, { pathParams: { chainId: '1' } }); + await mockApiResponse('token_info_applications', mocks.TOKEN_INFO_APPLICATIONS_RESPONSE.DEFAULT, { pathParams: { chainId: '1', id: undefined } }); + await mockApiResponse('user_info', profileMock.base); + const component = await render(); await expect(component).toHaveScreenshot(); }); -test('address verification flow', async({ mount, page }) => { - const CHECK_ADDRESS_URL = buildApiUrl('address_verification', { chainId: '1', type: ':prepare' }); - const VERIFY_ADDRESS_URL = buildApiUrl('address_verification', { chainId: '1', type: ':verify' }); +test('user without email', async({ render, mockApiResponse }) => { + await mockApiResponse('verified_addresses', mocks.VERIFIED_ADDRESS_RESPONSE.DEFAULT, { pathParams: { chainId: '1' } }); + await mockApiResponse('token_info_applications', mocks.TOKEN_INFO_APPLICATIONS_RESPONSE.DEFAULT, { pathParams: { chainId: '1', id: undefined } }); + await mockApiResponse('user_info', profileMock.withoutEmail); - await page.route(VERIFIED_ADDRESS_URL, (route) => route.fulfill({ - body: JSON.stringify(mocks.VERIFIED_ADDRESS_RESPONSE.DEFAULT), - })); + const component = await render(); - await page.route(TOKEN_INFO_APPLICATIONS_URL, (route) => route.fulfill({ - body: JSON.stringify(mocks.TOKEN_INFO_APPLICATIONS_RESPONSE.DEFAULT), - })); + await expect(component).toHaveScreenshot(); +}); - await page.route(CHECK_ADDRESS_URL, (route) => route.fulfill({ - body: JSON.stringify(mocks.ADDRESS_CHECK_RESPONSE.SUCCESS), - })); +test('address verification flow', async({ render, mockApiResponse, page }) => { + await mockApiResponse('verified_addresses', mocks.VERIFIED_ADDRESS_RESPONSE.DEFAULT, { pathParams: { chainId: '1' } }); + await mockApiResponse('token_info_applications', mocks.TOKEN_INFO_APPLICATIONS_RESPONSE.DEFAULT, { pathParams: { chainId: '1', id: undefined } }); + await mockApiResponse('address_verification', mocks.ADDRESS_CHECK_RESPONSE.SUCCESS as never, { pathParams: { chainId: '1', type: ':prepare' } }); + await mockApiResponse('address_verification', mocks.ADDRESS_VERIFY_RESPONSE.SUCCESS as never, { pathParams: { chainId: '1', type: ':verify' } }); + await mockApiResponse('user_info', profileMock.base); - await page.route(VERIFY_ADDRESS_URL, (route) => { - return route.fulfill({ - body: JSON.stringify(mocks.ADDRESS_VERIFY_RESPONSE.SUCCESS), - }); - }); - - await mount( - - - , - ); + await render(); // open modal await page.getByRole('button', { name: /add address/i }).click(); @@ -74,6 +53,7 @@ test('address verification flow', async({ mount, page }) => { await page.getByRole('button', { name: /continue/i }).click(); // fill second step + await page.getByText('Sign manually').click(); const signatureInput = page.getByLabel(/signature hash/i); await signatureInput.fill(mocks.SIGNATURE); await page.getByRole('button', { name: /verify/i }).click(); @@ -84,33 +64,20 @@ test('address verification flow', async({ mount, page }) => { await expect(page).toHaveScreenshot(); }); -test('application update flow', async({ mount, page }) => { - const TOKEN_INFO_APPLICATION_URL = buildApiUrl('token_info_applications', { chainId: '1', id: mocks.TOKEN_INFO_APPLICATION.UPDATED_ITEM.id }); - const FORM_CONFIG_URL = buildApiUrl('token_info_applications_config', { chainId: '1' }); - - await page.route(VERIFIED_ADDRESS_URL, (route) => route.fulfill({ - body: JSON.stringify(mocks.VERIFIED_ADDRESS_RESPONSE.DEFAULT), - })); - - await page.route(TOKEN_INFO_APPLICATIONS_URL, (route) => route.fulfill({ - body: JSON.stringify(mocks.TOKEN_INFO_APPLICATIONS_RESPONSE.FOR_UPDATE), - })); +test('application update flow', async({ render, mockApiResponse, page }) => { + await mockApiResponse('verified_addresses', mocks.VERIFIED_ADDRESS_RESPONSE.DEFAULT, { pathParams: { chainId: '1' } }); + await mockApiResponse('token_info_applications', mocks.TOKEN_INFO_APPLICATIONS_RESPONSE.FOR_UPDATE, { pathParams: { chainId: '1', id: undefined } }); + await mockApiResponse('user_info', profileMock.base); + await mockApiResponse('token_info_applications_config', mocks.TOKEN_INFO_FORM_CONFIG, { pathParams: { chainId: '1' } }); - await page.route(FORM_CONFIG_URL, (route) => route.fulfill({ - body: JSON.stringify(mocks.TOKEN_INFO_FORM_CONFIG), - })); - - // PUT request - await page.route(TOKEN_INFO_APPLICATION_URL, (route) => route.fulfill({ - body: JSON.stringify(mocks.TOKEN_INFO_APPLICATION.UPDATED_ITEM), - })); - - await mount( - - - , + await mockApiResponse( + 'token_info_applications', + mocks.TOKEN_INFO_APPLICATION.UPDATED_ITEM as never, // this mock is for PUT request + { pathParams: { chainId: '1', id: mocks.TOKEN_INFO_APPLICATION.UPDATED_ITEM.id } }, ); + await render(); + // open form await page.locator('tr').filter({ hasText: 'waiting for update' }).locator('button[aria-label="edit"]').click(); diff --git a/ui/pages/VerifiedAddresses.tsx b/ui/pages/VerifiedAddresses.tsx index 76a2134829..417ad31710 100644 --- a/ui/pages/VerifiedAddresses.tsx +++ b/ui/pages/VerifiedAddresses.tsx @@ -1,4 +1,4 @@ -import { OrderedList, ListItem, chakra, Button, useDisclosure, Show, Hide, Skeleton, Link } from '@chakra-ui/react'; +import { OrderedList, ListItem, chakra, Button, useDisclosure, Show, Hide, Skeleton, Link, Alert } from '@chakra-ui/react'; import { useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'next/router'; import React from 'react'; @@ -7,6 +7,7 @@ import type { VerifiedAddress, TokenInfoApplication, TokenInfoApplications, Veri import config from 'configs/app'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; +import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo'; import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken'; import { PAGE_TYPE_DICT } from 'lib/mixpanel/getPageType'; import getQueryParamString from 'lib/router/getQueryParamString'; @@ -37,16 +38,20 @@ const VerifiedAddresses = () => { const modalProps = useDisclosure(); const queryClient = useQueryClient(); + const userInfoQuery = useFetchProfileInfo(); + const addressesQuery = useApiQuery('verified_addresses', { pathParams: { chainId: config.chain.id }, queryOptions: { placeholderData: { verifiedAddresses: Array(3).fill(VERIFIED_ADDRESS) }, + enabled: Boolean(userInfoQuery.data?.email), }, }); const applicationsQuery = useApiQuery('token_info_applications', { pathParams: { chainId: config.chain.id, id: undefined }, queryOptions: { placeholderData: { submissions: Array(3).fill(TOKEN_INFO_APPLICATION) }, + enabled: Boolean(userInfoQuery.data?.email), select: (data) => { return { ...data, @@ -57,6 +62,7 @@ const VerifiedAddresses = () => { }); const isLoading = addressesQuery.isPlaceholderData || applicationsQuery.isPlaceholderData; + const userWithoutEmail = userInfoQuery.data && !userInfoQuery.data.email; const handleGoBack = React.useCallback(() => { setSelectedAddress(undefined); @@ -100,13 +106,23 @@ const VerifiedAddresses = () => { }); }, [ queryClient ]); - const addButton = ( - - - - ); + const addButton = (() => { + if (userWithoutEmail) { + return ( + + ); + } + + return ( + + + + ); + })(); const backLink = React.useMemo(() => { if (!selectedAddress) { @@ -135,35 +151,53 @@ const VerifiedAddresses = () => { ); } - const content = addressesQuery.data?.verifiedAddresses ? ( - <> - - { addressesQuery.data.verifiedAddresses.map((item, index) => ( - tokenAddress.toLowerCase() === item.contractAddress.toLowerCase()) } - onAdd={ handleItemAdd } - onEdit={ handleItemEdit } - isLoading={ isLoading } - /> - )) } - - - - - - ) : null; + const content = (() => { + if (userWithoutEmail) { + return null; + } + + if (addressesQuery.data?.verifiedAddresses) { + return ( + <> + + { addressesQuery.data.verifiedAddresses.map((item, index) => ( + tokenAddress.toLowerCase() === item.contractAddress.toLowerCase()) + } + onAdd={ handleItemAdd } + onEdit={ handleItemEdit } + isLoading={ isLoading } + /> + )) } + + + + + + ); + } + + return null; + })(); return ( <> + { userWithoutEmail && ( + + You need a valid email address to verify addresses. Please logout of MyAccount then login using your email to proceed. + + ) } Verify ownership of a smart contract address to easily update information in Blockscout. @@ -188,7 +222,7 @@ const VerifiedAddresses = () => { { + await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ + status: 200, + body: '', + })); + + await page.route(WITHDRAWALS_API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(withdrawalsData), + })); + + await page.route(WITHDRAWALS_COUNTERS_API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify({ withdrawal_count: '111111', withdrawal_sum: '1010101010110101001101010' }), + })); + + const component = await mount( + + + , + ); + + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/pages/Withdrawals.tsx b/ui/pages/Withdrawals.tsx new file mode 100644 index 0000000000..648efe7980 --- /dev/null +++ b/ui/pages/Withdrawals.tsx @@ -0,0 +1,107 @@ +import { Flex, Hide, Show, Skeleton, Text } from '@chakra-ui/react'; +import BigNumber from 'bignumber.js'; +import React from 'react'; + +import config from 'configs/app'; +import useApiQuery from 'lib/api/useApiQuery'; +import getCurrencyValue from 'lib/getCurrencyValue'; +import useIsMobile from 'lib/hooks/useIsMobile'; +import { generateListStub } from 'stubs/utils'; +import { WITHDRAWAL } from 'stubs/withdrawals'; +import ActionBar from 'ui/shared/ActionBar'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import PageTitle from 'ui/shared/Page/PageTitle'; +import Pagination from 'ui/shared/pagination/Pagination'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; +import WithdrawalsListItem from 'ui/withdrawals/WithdrawalsListItem'; +import WithdrawalsTable from 'ui/withdrawals/WithdrawalsTable'; + +const feature = config.features.beaconChain; + +const Withdrawals = () => { + const isMobile = useIsMobile(); + + const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({ + resourceName: 'withdrawals', + options: { + placeholderData: generateListStub<'withdrawals'>(WITHDRAWAL, 50, { next_page_params: { + index: 5, + items_count: 50, + } }), + }, + }); + + const countersQuery = useApiQuery('withdrawals_counters'); + + const content = data?.items ? ( + <> + + { data.items.map(((item, index) => ( + + ))) } + + + + + + ) : null; + + const text = (() => { + if (countersQuery.isLoading) { + return ( + + ); + } + + if (countersQuery.isError || !feature.isEnabled) { + return null; + } + + const { valueStr } = getCurrencyValue({ value: countersQuery.data.withdrawal_sum }); + return ( + + { BigNumber(countersQuery.data.withdrawal_count).toFormat() } withdrawals processed + and { valueStr } { feature.currency.symbol } withdrawn + + ); + })(); + + const actionBar = ( + <> + { (isMobile || !pagination.isVisible) && text } + { pagination.isVisible && ( + + + { !isMobile && text } + + + + ) } + + ); + + return ( + <> + + + + ); +}; + +export default Withdrawals; diff --git a/ui/pages/ZkEvmL2Deposits.pw.tsx b/ui/pages/ZkEvmL2Deposits.pw.tsx new file mode 100644 index 0000000000..b1ae7c8f93 --- /dev/null +++ b/ui/pages/ZkEvmL2Deposits.pw.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import * as depositsMock from 'mocks/zkEvm/deposits'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import { test, expect } from 'playwright/lib'; + +import ZkEvmL2Deposits from './ZkEvmL2Deposits'; + +test('base view +@mobile', async({ render, mockApiResponse, mockEnvs, mockTextAd }) => { + await mockTextAd(); + await mockEnvs(ENVS_MAP.zkEvmRollup); + await mockApiResponse('zkevm_l2_deposits', depositsMock.baseResponse); + await mockApiResponse('zkevm_l2_deposits_count', 3971111); + + const component = await render(); + + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/pages/ZkEvmL2Deposits.tsx b/ui/pages/ZkEvmL2Deposits.tsx new file mode 100644 index 0000000000..3cfdac9b0c --- /dev/null +++ b/ui/pages/ZkEvmL2Deposits.tsx @@ -0,0 +1,81 @@ +import { Hide, Show, Skeleton } from '@chakra-ui/react'; +import React from 'react'; + +import useApiQuery from 'lib/api/useApiQuery'; +import { rightLineArrow, nbsp } from 'lib/html-entities'; +import { generateListStub } from 'stubs/utils'; +import { ZKEVM_DEPOSITS_ITEM } from 'stubs/zkEvmL2'; +import ZkEvmL2DepositsListItem from 'ui/deposits/zkEvmL2/ZkEvmL2DepositsListItem'; +import ZkEvmL2DepositsTable from 'ui/deposits/zkEvmL2/ZkEvmL2DepositsTable'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import PageTitle from 'ui/shared/Page/PageTitle'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; +import StickyPaginationWithText from 'ui/shared/StickyPaginationWithText'; + +const ZkEvmL2Deposits = () => { + const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({ + resourceName: 'zkevm_l2_deposits', + options: { + placeholderData: generateListStub<'zkevm_l2_deposits'>( + ZKEVM_DEPOSITS_ITEM, + 50, + { next_page_params: { items_count: 50, index: 1 } }, + ), + }, + }); + + const countersQuery = useApiQuery('zkevm_l2_deposits_count', { + queryOptions: { + placeholderData: 1927029, + }, + }); + + const content = data?.items ? ( + <> + + { data.items.map(((item, index) => ( + + ))) } + + + + + + ) : null; + + const text = (() => { + if (countersQuery.isError) { + return null; + } + + return ( + + A total of { countersQuery.data?.toLocaleString() } deposits found + + ); + })(); + + const actionBar = ; + + return ( + <> + + + + ); +}; + +export default ZkEvmL2Deposits; diff --git a/ui/pages/ZkEvmL2TxnBatch.pw.tsx b/ui/pages/ZkEvmL2TxnBatch.pw.tsx index b2d2e49f51..9c717759b5 100644 --- a/ui/pages/ZkEvmL2TxnBatch.pw.tsx +++ b/ui/pages/ZkEvmL2TxnBatch.pw.tsx @@ -1,70 +1,35 @@ -import { test as base, expect, devices } from '@playwright/experimental-ct-react'; import React from 'react'; -import { txnBatchData } from 'mocks/zkevmL2txnBatches/zkevmL2txnBatch'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; -import buildApiUrl from 'playwright/utils/buildApiUrl'; -import * as configs from 'playwright/utils/configs'; +import { txnBatchData } from 'mocks/zkEvm/txnBatches'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import { test, expect, devices } from 'playwright/lib'; import ZkEvmL2TxnBatch from './ZkEvmL2TxnBatch'; -const test = base.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.featureEnvs.zkEvmRollup) as any, -}); - +const batchNumber = '5'; const hooksConfig = { router: { - query: { number: '5' }, + query: { number: batchNumber }, }, }; -const BATCH_API_URL = buildApiUrl('zkevm_l2_txn_batch', { number: '5' }); +test.beforeEach(async({ mockTextAd, mockApiResponse, mockEnvs }) => { + await mockEnvs(ENVS_MAP.zkEvmRollup); + await mockTextAd(); + await mockApiResponse('zkevm_l2_txn_batch', txnBatchData, { pathParams: { number: batchNumber } }); +}); -test('base view', async({ mount, page }) => { +test('base view', async({ render }) => { test.slow(); - await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ - status: 200, - body: '', - })); - - await page.route(BATCH_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(txnBatchData), - })); - - const component = await mount( - - - , - { hooksConfig }, - ); - + const component = await render(, { hooksConfig }); await expect(component).toHaveScreenshot(); }); test.describe('mobile', () => { test.use({ viewport: devices['iPhone 13 Pro'].viewport }); - test('base view', async({ mount, page }) => { + test('base view', async({ render }) => { test.slow(); - await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ - status: 200, - body: '', - })); - - await page.route(BATCH_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(txnBatchData), - })); - - const component = await mount( - - - , - { hooksConfig }, - ); - + const component = await render(, { hooksConfig }); await expect(component).toHaveScreenshot(); }); }); diff --git a/ui/pages/ZkEvmL2TxnBatches.pw.tsx b/ui/pages/ZkEvmL2TxnBatches.pw.tsx index a94f883bda..61a3af145d 100644 --- a/ui/pages/ZkEvmL2TxnBatches.pw.tsx +++ b/ui/pages/ZkEvmL2TxnBatches.pw.tsx @@ -1,43 +1,16 @@ -import { test as base, expect } from '@playwright/experimental-ct-react'; import React from 'react'; -import { txnBatchesData } from 'mocks/zkevmL2txnBatches/zkevmL2txnBatches'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; -import buildApiUrl from 'playwright/utils/buildApiUrl'; -import * as configs from 'playwright/utils/configs'; +import { txnBatchesData } from 'mocks/zkEvm/txnBatches'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import { test, expect } from 'playwright/lib'; import ZkEvmL2TxnBatches from './ZkEvmL2TxnBatches'; -const test = base.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.featureEnvs.zkEvmRollup) as any, -}); - -const BATCHES_API_URL = buildApiUrl('zkevm_l2_txn_batches'); -const BATCHES_COUNTERS_API_URL = buildApiUrl('zkevm_l2_txn_batches_count'); - -test('base view +@mobile', async({ mount, page }) => { - await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ - status: 200, - body: '', - })); - - await page.route(BATCHES_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(txnBatchesData), - })); - - await page.route(BATCHES_COUNTERS_API_URL, (route) => route.fulfill({ - status: 200, - body: '9927', - })); - - const component = await mount( - - - , - ); - +test('base view +@mobile', async({ render, mockTextAd, mockEnvs, mockApiResponse }) => { + await mockEnvs(ENVS_MAP.zkEvmRollup); + await mockTextAd(); + await mockApiResponse('zkevm_l2_txn_batches', txnBatchesData); + await mockApiResponse('zkevm_l2_txn_batches_count', 9927); + const component = await render(); await expect(component).toHaveScreenshot(); }); diff --git a/ui/pages/ZkEvmL2Withdrawals.pw.tsx b/ui/pages/ZkEvmL2Withdrawals.pw.tsx new file mode 100644 index 0000000000..92a9fbf88a --- /dev/null +++ b/ui/pages/ZkEvmL2Withdrawals.pw.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import * as withdrawalsMock from 'mocks/zkEvm/withdrawals'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import { test, expect } from 'playwright/lib'; + +import ZkEvmL2Withdrawals from './ZkEvmL2Withdrawals'; + +test('base view +@mobile', async({ render, mockApiResponse, mockEnvs, mockTextAd }) => { + await mockTextAd(); + await mockEnvs(ENVS_MAP.zkEvmRollup); + await mockApiResponse('zkevm_l2_withdrawals', withdrawalsMock.baseResponse); + await mockApiResponse('zkevm_l2_withdrawals_count', 3971111); + + const component = await render(); + + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/pages/ZkEvmL2Withdrawals.tsx b/ui/pages/ZkEvmL2Withdrawals.tsx new file mode 100644 index 0000000000..d5671c738b --- /dev/null +++ b/ui/pages/ZkEvmL2Withdrawals.tsx @@ -0,0 +1,81 @@ +import { Hide, Show, Skeleton } from '@chakra-ui/react'; +import React from 'react'; + +import useApiQuery from 'lib/api/useApiQuery'; +import { rightLineArrow, nbsp } from 'lib/html-entities'; +import { generateListStub } from 'stubs/utils'; +import { ZKEVM_WITHDRAWALS_ITEM } from 'stubs/zkEvmL2'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import PageTitle from 'ui/shared/Page/PageTitle'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; +import StickyPaginationWithText from 'ui/shared/StickyPaginationWithText'; +import ZkEvmL2WithdrawalsListItem from 'ui/withdrawals/zkEvmL2/ZkEvmL2WithdrawalsListItem'; +import ZkEvmL2WithdrawalsTable from 'ui/withdrawals/zkEvmL2/ZkEvmL2WithdrawalsTable'; + +const ZkEvmL2Withdrawals = () => { + const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({ + resourceName: 'zkevm_l2_withdrawals', + options: { + placeholderData: generateListStub<'zkevm_l2_withdrawals'>( + ZKEVM_WITHDRAWALS_ITEM, + 50, + { next_page_params: { items_count: 50, index: 1 } }, + ), + }, + }); + + const countersQuery = useApiQuery('zkevm_l2_withdrawals_count', { + queryOptions: { + placeholderData: 1927029, + }, + }); + + const content = data?.items ? ( + <> + + { data.items.map(((item, index) => ( + + ))) } + + + + + + ) : null; + + const text = (() => { + if (countersQuery.isError) { + return null; + } + + return ( + + A total of { countersQuery.data?.toLocaleString() } withdrawals found + + ); + })(); + + const actionBar = ; + + return ( + <> + + + + ); +}; + +export default ZkEvmL2Withdrawals; diff --git a/ui/pages/ZkSyncL2TxnBatch.pw.tsx b/ui/pages/ZkSyncL2TxnBatch.pw.tsx new file mode 100644 index 0000000000..b7bc910f19 --- /dev/null +++ b/ui/pages/ZkSyncL2TxnBatch.pw.tsx @@ -0,0 +1,33 @@ +import React from 'react'; + +import * as zkSyncTxnBatchMock from 'mocks/zkSync/zkSyncTxnBatch'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import { test, expect, devices } from 'playwright/lib'; + +import ZkSyncL2TxnBatch from './ZkSyncL2TxnBatch'; + +const batchNumber = String(zkSyncTxnBatchMock.base.number); +const hooksConfig = { + router: { + query: { number: batchNumber }, + }, +}; + +test.beforeEach(async({ mockTextAd, mockApiResponse, mockEnvs }) => { + await mockEnvs(ENVS_MAP.zkSyncRollup); + await mockTextAd(); + await mockApiResponse('zksync_l2_txn_batch', zkSyncTxnBatchMock.base, { pathParams: { number: batchNumber } }); +}); + +test('base view', async({ render }) => { + const component = await render(, { hooksConfig }); + await expect(component).toHaveScreenshot(); +}); + +test.describe('mobile', () => { + test.use({ viewport: devices['iPhone 13 Pro'].viewport }); + test('base view', async({ render }) => { + const component = await render(, { hooksConfig }); + await expect(component).toHaveScreenshot(); + }); +}); diff --git a/ui/pages/ZkSyncL2TxnBatch.tsx b/ui/pages/ZkSyncL2TxnBatch.tsx new file mode 100644 index 0000000000..14608aa143 --- /dev/null +++ b/ui/pages/ZkSyncL2TxnBatch.tsx @@ -0,0 +1,102 @@ +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { RoutedTab } from 'ui/shared/Tabs/types'; + +import useApiQuery from 'lib/api/useApiQuery'; +import { useAppContext } from 'lib/contexts/app'; +import throwOnAbsentParamError from 'lib/errors/throwOnAbsentParamError'; +import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; +import useIsMobile from 'lib/hooks/useIsMobile'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import { TX } from 'stubs/tx'; +import { generateListStub } from 'stubs/utils'; +import { ZKSYNC_L2_TXN_BATCH } from 'stubs/zkSyncL2'; +import TextAd from 'ui/shared/ad/TextAd'; +import PageTitle from 'ui/shared/Page/PageTitle'; +import Pagination from 'ui/shared/pagination/Pagination'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; +import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; +import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton'; +import ZkSyncL2TxnBatchDetails from 'ui/txnBatches/zkSyncL2/ZkSyncL2TxnBatchDetails'; +import TxsWithFrontendSorting from 'ui/txs/TxsWithFrontendSorting'; + +const TAB_LIST_PROPS = { + marginBottom: 0, + py: 5, + marginTop: -5, +}; + +const ZkSyncL2TxnBatch = () => { + const router = useRouter(); + const appProps = useAppContext(); + const number = getQueryParamString(router.query.number); + const tab = getQueryParamString(router.query.tab); + const isMobile = useIsMobile(); + + const batchQuery = useApiQuery('zksync_l2_txn_batch', { + pathParams: { number }, + queryOptions: { + enabled: Boolean(number), + placeholderData: ZKSYNC_L2_TXN_BATCH, + }, + }); + + const batchTxsQuery = useQueryWithPages({ + resourceName: 'zksync_l2_txn_batch_txs', + pathParams: { number }, + options: { + enabled: Boolean(!batchQuery.isPlaceholderData && batchQuery.data?.number && tab === 'txs'), + placeholderData: generateListStub<'zksync_l2_txn_batch_txs'>(TX, 50, { next_page_params: { + batch_number: '8122', + block_number: 1338932, + index: 0, + items_count: 50, + } }), + }, + }); + + throwOnAbsentParamError(number); + throwOnResourceLoadError(batchQuery); + + const tabs: Array = React.useMemo(() => ([ + { id: 'index', title: 'Details', component: }, + { id: 'txs', title: 'Transactions', component: }, + ].filter(Boolean)), [ batchQuery, batchTxsQuery ]); + + const backLink = React.useMemo(() => { + const hasGoBackLink = appProps.referrer && appProps.referrer.endsWith('/batches'); + + if (!hasGoBackLink) { + return; + } + + return { + label: 'Back to tx batches list', + url: appProps.referrer, + }; + }, [ appProps.referrer ]); + + const hasPagination = !isMobile && batchTxsQuery.pagination.isVisible && tab === 'txs'; + + return ( + <> + + + { batchQuery.isPlaceholderData ? + : ( + : null } + stickyEnabled={ hasPagination } + /> + ) } + + ); +}; + +export default ZkSyncL2TxnBatch; diff --git a/ui/pages/ZkSyncL2TxnBatches.pw.tsx b/ui/pages/ZkSyncL2TxnBatches.pw.tsx new file mode 100644 index 0000000000..9530e0a569 --- /dev/null +++ b/ui/pages/ZkSyncL2TxnBatches.pw.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import * as zkSyncTxnBatchesMock from 'mocks/zkSync/zkSyncTxnBatches'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import { test, expect } from 'playwright/lib'; + +import ZkSyncL2TxnBatches from './ZkSyncL2TxnBatches'; + +test('base view +@mobile', async({ render, mockEnvs, mockTextAd, mockApiResponse }) => { + test.slow(); + await mockEnvs(ENVS_MAP.zkSyncRollup); + await mockTextAd(); + await mockApiResponse('zksync_l2_txn_batches', zkSyncTxnBatchesMock.baseResponse); + await mockApiResponse('zksync_l2_txn_batches_count', 9927); + + const component = await render(); + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/pages/ZkSyncL2TxnBatches.tsx b/ui/pages/ZkSyncL2TxnBatches.tsx new file mode 100644 index 0000000000..d01bbef7de --- /dev/null +++ b/ui/pages/ZkSyncL2TxnBatches.tsx @@ -0,0 +1,83 @@ +import { Hide, Show, Skeleton, Text } from '@chakra-ui/react'; +import React from 'react'; + +import useApiQuery from 'lib/api/useApiQuery'; +import { generateListStub } from 'stubs/utils'; +import { ZKSYNC_L2_TXN_BATCHES_ITEM } from 'stubs/zkSyncL2'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import PageTitle from 'ui/shared/Page/PageTitle'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; +import StickyPaginationWithText from 'ui/shared/StickyPaginationWithText'; +import ZkSyncTxnBatchesListItem from 'ui/txnBatches/zkSyncL2/ZkSyncTxnBatchesListItem'; +import ZkSyncTxnBatchesTable from 'ui/txnBatches/zkSyncL2/ZkSyncTxnBatchesTable'; + +const ZkSyncL2TxnBatches = () => { + const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({ + resourceName: 'zksync_l2_txn_batches', + options: { + placeholderData: generateListStub<'zksync_l2_txn_batches'>( + ZKSYNC_L2_TXN_BATCHES_ITEM, + 50, + { + next_page_params: { + items_count: 50, + number: 9045200, + }, + }, + ), + }, + }); + + const countersQuery = useApiQuery('zksync_l2_txn_batches_count', { + queryOptions: { + placeholderData: 5231746, + }, + }); + + const content = data?.items ? ( + <> + + { data.items.map(((item, index) => ( + + ))) } + + + + ) : null; + + const text = (() => { + if (countersQuery.isError || isError || !data?.items.length) { + return null; + } + + return ( + + Tx batch + #{ data.items[0].number } to + #{ data.items[data.items.length - 1].number } + (total of { countersQuery.data?.toLocaleString() } batches) + + ); + })(); + + const actionBar = ; + + return ( + <> + + + + ); +}; + +export default ZkSyncL2TxnBatches; diff --git a/ui/pages/__screenshots__/BeaconChainWithdrawals.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/BeaconChainWithdrawals.pw.tsx_default_base-view-mobile-1.png index 601ac6bbb7..f3eb1c52ce 100644 Binary files a/ui/pages/__screenshots__/BeaconChainWithdrawals.pw.tsx_default_base-view-mobile-1.png and b/ui/pages/__screenshots__/BeaconChainWithdrawals.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/BeaconChainWithdrawals.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/BeaconChainWithdrawals.pw.tsx_mobile_base-view-mobile-1.png index add6a71f0d..f8afc166ef 100644 Binary files a/ui/pages/__screenshots__/BeaconChainWithdrawals.pw.tsx_mobile_base-view-mobile-1.png and b/ui/pages/__screenshots__/BeaconChainWithdrawals.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/Blob.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png b/ui/pages/__screenshots__/Blob.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..41128e8e02 Binary files /dev/null and b/ui/pages/__screenshots__/Blob.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Blob.pw.tsx_default_base-view-mobile-dark-mode-1.png b/ui/pages/__screenshots__/Blob.pw.tsx_default_base-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..53cf7028de Binary files /dev/null and b/ui/pages/__screenshots__/Blob.pw.tsx_default_base-view-mobile-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Blob.pw.tsx_default_without-data-1.png b/ui/pages/__screenshots__/Blob.pw.tsx_default_without-data-1.png new file mode 100644 index 0000000000..f8a97c6db3 Binary files /dev/null and b/ui/pages/__screenshots__/Blob.pw.tsx_default_without-data-1.png differ diff --git a/ui/pages/__screenshots__/Blob.pw.tsx_mobile_base-view-mobile-dark-mode-1.png b/ui/pages/__screenshots__/Blob.pw.tsx_mobile_base-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..7db4844088 Binary files /dev/null and b/ui/pages/__screenshots__/Blob.pw.tsx_mobile_base-view-mobile-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/CsvExport.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png b/ui/pages/__screenshots__/CsvExport.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png index a7e7280953..e5daa6120d 100644 Binary files a/ui/pages/__screenshots__/CsvExport.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png and b/ui/pages/__screenshots__/CsvExport.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/CsvExport.pw.tsx_default_base-view-mobile-dark-mode-1.png b/ui/pages/__screenshots__/CsvExport.pw.tsx_default_base-view-mobile-dark-mode-1.png index 2003834634..3fbbd9f208 100644 Binary files a/ui/pages/__screenshots__/CsvExport.pw.tsx_default_base-view-mobile-dark-mode-1.png and b/ui/pages/__screenshots__/CsvExport.pw.tsx_default_base-view-mobile-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/CsvExport.pw.tsx_default_token-holders-1.png b/ui/pages/__screenshots__/CsvExport.pw.tsx_default_token-holders-1.png new file mode 100644 index 0000000000..d5174a646f Binary files /dev/null and b/ui/pages/__screenshots__/CsvExport.pw.tsx_default_token-holders-1.png differ diff --git a/ui/pages/__screenshots__/CsvExport.pw.tsx_mobile_base-view-mobile-dark-mode-1.png b/ui/pages/__screenshots__/CsvExport.pw.tsx_mobile_base-view-mobile-dark-mode-1.png index d5640e74c9..6a089cd481 100644 Binary files a/ui/pages/__screenshots__/CsvExport.pw.tsx_mobile_base-view-mobile-dark-mode-1.png and b/ui/pages/__screenshots__/CsvExport.pw.tsx_mobile_base-view-mobile-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/GasTracker.pw.tsx_dark-color-mode_base-view-dark-mode-mobile-1.png b/ui/pages/__screenshots__/GasTracker.pw.tsx_dark-color-mode_base-view-dark-mode-mobile-1.png index daae678871..f773687f4e 100644 Binary files a/ui/pages/__screenshots__/GasTracker.pw.tsx_dark-color-mode_base-view-dark-mode-mobile-1.png and b/ui/pages/__screenshots__/GasTracker.pw.tsx_dark-color-mode_base-view-dark-mode-mobile-1.png differ diff --git a/ui/pages/__screenshots__/GasTracker.pw.tsx_default_base-view-dark-mode-mobile-1.png b/ui/pages/__screenshots__/GasTracker.pw.tsx_default_base-view-dark-mode-mobile-1.png index 7e95806cb4..f8330a6402 100644 Binary files a/ui/pages/__screenshots__/GasTracker.pw.tsx_default_base-view-dark-mode-mobile-1.png and b/ui/pages/__screenshots__/GasTracker.pw.tsx_default_base-view-dark-mode-mobile-1.png differ diff --git a/ui/pages/__screenshots__/GasTracker.pw.tsx_mobile_base-view-dark-mode-mobile-1.png b/ui/pages/__screenshots__/GasTracker.pw.tsx_mobile_base-view-dark-mode-mobile-1.png index 59f2896793..3d0aba111a 100644 Binary files a/ui/pages/__screenshots__/GasTracker.pw.tsx_mobile_base-view-dark-mode-mobile-1.png and b/ui/pages/__screenshots__/GasTracker.pw.tsx_mobile_base-view-dark-mode-mobile-1.png differ diff --git a/ui/pages/__screenshots__/Home.pw.tsx_dark-color-mode_default-view---default-dark-mode-1.png b/ui/pages/__screenshots__/Home.pw.tsx_dark-color-mode_default-view---default-dark-mode-1.png index 074f02295c..894038a81c 100644 Binary files a/ui/pages/__screenshots__/Home.pw.tsx_dark-color-mode_default-view---default-dark-mode-1.png and b/ui/pages/__screenshots__/Home.pw.tsx_dark-color-mode_default-view---default-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Home.pw.tsx_default_custom-hero-plate-background-default-view-1.png b/ui/pages/__screenshots__/Home.pw.tsx_default_custom-hero-plate-background-default-view-1.png index 1851dc3744..d600afbd5f 100644 Binary files a/ui/pages/__screenshots__/Home.pw.tsx_default_custom-hero-plate-background-default-view-1.png and b/ui/pages/__screenshots__/Home.pw.tsx_default_custom-hero-plate-background-default-view-1.png differ diff --git a/ui/pages/__screenshots__/Home.pw.tsx_default_default-view-screen-xl-1.png b/ui/pages/__screenshots__/Home.pw.tsx_default_default-view-screen-xl-1.png index 18b42c45ba..8356835e2e 100644 Binary files a/ui/pages/__screenshots__/Home.pw.tsx_default_default-view-screen-xl-1.png and b/ui/pages/__screenshots__/Home.pw.tsx_default_default-view-screen-xl-1.png differ diff --git a/ui/pages/__screenshots__/Home.pw.tsx_default_mobile-base-view-1.png b/ui/pages/__screenshots__/Home.pw.tsx_default_mobile-base-view-1.png index d5c4e1f321..655a4ad4a5 100644 Binary files a/ui/pages/__screenshots__/Home.pw.tsx_default_mobile-base-view-1.png and b/ui/pages/__screenshots__/Home.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/pages/__screenshots__/L2Deposits.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/L2Deposits.pw.tsx_default_base-view-mobile-1.png new file mode 100644 index 0000000000..af883e0365 Binary files /dev/null and b/ui/pages/__screenshots__/L2Deposits.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/L2Deposits.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/L2Deposits.pw.tsx_mobile_base-view-mobile-1.png new file mode 100644 index 0000000000..57f48bfcc4 Binary files /dev/null and b/ui/pages/__screenshots__/L2Deposits.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/L2OutputRoots.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/L2OutputRoots.pw.tsx_default_base-view-mobile-1.png new file mode 100644 index 0000000000..25c3210032 Binary files /dev/null and b/ui/pages/__screenshots__/L2OutputRoots.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/L2OutputRoots.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/L2OutputRoots.pw.tsx_mobile_base-view-mobile-1.png new file mode 100644 index 0000000000..477bcaaca4 Binary files /dev/null and b/ui/pages/__screenshots__/L2OutputRoots.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/L2TxnBatches.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/L2TxnBatches.pw.tsx_default_base-view-mobile-1.png new file mode 100644 index 0000000000..8409b37609 Binary files /dev/null and b/ui/pages/__screenshots__/L2TxnBatches.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/L2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/L2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png new file mode 100644 index 0000000000..e9b20c4699 Binary files /dev/null and b/ui/pages/__screenshots__/L2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/L2Withdrawals.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/L2Withdrawals.pw.tsx_default_base-view-mobile-1.png new file mode 100644 index 0000000000..4a0e9c3394 Binary files /dev/null and b/ui/pages/__screenshots__/L2Withdrawals.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/L2Withdrawals.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/L2Withdrawals.pw.tsx_mobile_base-view-mobile-1.png new file mode 100644 index 0000000000..6e12fd8a6e Binary files /dev/null and b/ui/pages/__screenshots__/L2Withdrawals.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_base-view-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_base-view-dark-mode-1.png new file mode 100644 index 0000000000..e25ba73da9 Binary files /dev/null and b/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_base-view-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-banner-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-banner-dark-mode-1.png new file mode 100644 index 0000000000..fceedc3185 Binary files /dev/null and b/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-banner-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-featured-app-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-featured-app-dark-mode-1.png new file mode 100644 index 0000000000..5ae62491ab Binary files /dev/null and b/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-featured-app-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-scores-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-scores-dark-mode-1.png new file mode 100644 index 0000000000..cda2818ef2 Binary files /dev/null and b/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-scores-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_base-view-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_base-view-dark-mode-1.png new file mode 100644 index 0000000000..17c54d8959 Binary files /dev/null and b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_base-view-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-base-view-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-base-view-1.png new file mode 100644 index 0000000000..fd0ab1cb5f Binary files /dev/null and b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-banner-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-banner-1.png new file mode 100644 index 0000000000..fbb38d03af Binary files /dev/null and b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-banner-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-featured-app-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-featured-app-1.png new file mode 100644 index 0000000000..35245e7d9f Binary files /dev/null and b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-featured-app-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-scores-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-scores-1.png new file mode 100644 index 0000000000..6aa6e1999e Binary files /dev/null and b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-scores-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-banner-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-banner-dark-mode-1.png new file mode 100644 index 0000000000..d1b2888b75 Binary files /dev/null and b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-banner-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-featured-app-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-featured-app-dark-mode-1.png new file mode 100644 index 0000000000..c6f93f138e Binary files /dev/null and b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-featured-app-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-scores-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-scores-dark-mode-1.png new file mode 100644 index 0000000000..aedf4aa291 Binary files /dev/null and b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-scores-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_dark-color-mode_base-view-dark-mode-1.png b/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_dark-color-mode_base-view-dark-mode-1.png new file mode 100644 index 0000000000..3ebdbba8ef Binary files /dev/null and b/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_dark-color-mode_base-view-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_default_base-view-dark-mode-1.png b/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_default_base-view-dark-mode-1.png new file mode 100644 index 0000000000..4bb6f375b8 Binary files /dev/null and b/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_default_base-view-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_default_mobile-base-view-1.png b/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_default_mobile-base-view-1.png new file mode 100644 index 0000000000..de5e5f6786 Binary files /dev/null and b/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/pages/__screenshots__/NameDomain.pw.tsx_default_history-tab-mobile-1.png b/ui/pages/__screenshots__/NameDomain.pw.tsx_default_history-tab-mobile-1.png index 216d8daf6b..d135f542ad 100644 Binary files a/ui/pages/__screenshots__/NameDomain.pw.tsx_default_history-tab-mobile-1.png and b/ui/pages/__screenshots__/NameDomain.pw.tsx_default_history-tab-mobile-1.png differ diff --git a/ui/pages/__screenshots__/NameDomain.pw.tsx_mobile_history-tab-mobile-1.png b/ui/pages/__screenshots__/NameDomain.pw.tsx_mobile_history-tab-mobile-1.png index bfeb78ed07..d37225d13f 100644 Binary files a/ui/pages/__screenshots__/NameDomain.pw.tsx_mobile_history-tab-mobile-1.png and b/ui/pages/__screenshots__/NameDomain.pw.tsx_mobile_history-tab-mobile-1.png differ diff --git a/ui/pages/__screenshots__/OptimisticL2Deposits.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/OptimisticL2Deposits.pw.tsx_default_base-view-mobile-1.png index cc84b3b632..91200b6ada 100644 Binary files a/ui/pages/__screenshots__/OptimisticL2Deposits.pw.tsx_default_base-view-mobile-1.png and b/ui/pages/__screenshots__/OptimisticL2Deposits.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/OptimisticL2Deposits.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/OptimisticL2Deposits.pw.tsx_mobile_base-view-mobile-1.png index 14787cec19..20be8d7d75 100644 Binary files a/ui/pages/__screenshots__/OptimisticL2Deposits.pw.tsx_mobile_base-view-mobile-1.png and b/ui/pages/__screenshots__/OptimisticL2Deposits.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/OptimisticL2OutputRoots.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/OptimisticL2OutputRoots.pw.tsx_default_base-view-mobile-1.png index 3c0ddb505c..004a03d2aa 100644 Binary files a/ui/pages/__screenshots__/OptimisticL2OutputRoots.pw.tsx_default_base-view-mobile-1.png and b/ui/pages/__screenshots__/OptimisticL2OutputRoots.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/OptimisticL2OutputRoots.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/OptimisticL2OutputRoots.pw.tsx_mobile_base-view-mobile-1.png index 8724dfff72..b04c166403 100644 Binary files a/ui/pages/__screenshots__/OptimisticL2OutputRoots.pw.tsx_mobile_base-view-mobile-1.png and b/ui/pages/__screenshots__/OptimisticL2OutputRoots.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/OptimisticL2TxnBatches.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/OptimisticL2TxnBatches.pw.tsx_default_base-view-mobile-1.png index 10bed40ba0..318dd12bfc 100644 Binary files a/ui/pages/__screenshots__/OptimisticL2TxnBatches.pw.tsx_default_base-view-mobile-1.png and b/ui/pages/__screenshots__/OptimisticL2TxnBatches.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/OptimisticL2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/OptimisticL2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png index 9175326615..16909272ec 100644 Binary files a/ui/pages/__screenshots__/OptimisticL2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png and b/ui/pages/__screenshots__/OptimisticL2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/OptimisticL2Withdrawals.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/OptimisticL2Withdrawals.pw.tsx_default_base-view-mobile-1.png index a9b858056c..2f959502d4 100644 Binary files a/ui/pages/__screenshots__/OptimisticL2Withdrawals.pw.tsx_default_base-view-mobile-1.png and b/ui/pages/__screenshots__/OptimisticL2Withdrawals.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/OptimisticL2Withdrawals.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/OptimisticL2Withdrawals.pw.tsx_mobile_base-view-mobile-1.png index f29201642a..2744ff898f 100644 Binary files a/ui/pages/__screenshots__/OptimisticL2Withdrawals.pw.tsx_mobile_base-view-mobile-1.png and b/ui/pages/__screenshots__/OptimisticL2Withdrawals.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_dark-color-mode_search-by-name-mobile-dark-mode-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_dark-color-mode_search-by-name-mobile-dark-mode-1.png index febacb309f..5a03d585ed 100644 Binary files a/ui/pages/__screenshots__/SearchResults.pw.tsx_dark-color-mode_search-by-name-mobile-dark-mode-1.png and b/ui/pages/__screenshots__/SearchResults.pw.tsx_dark-color-mode_search-by-name-mobile-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-address-hash-mobile-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-address-hash-mobile-1.png index d88025d48d..e9a1435305 100644 Binary files a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-address-hash-mobile-1.png and b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-address-hash-mobile-1.png differ diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-blob-hash-mobile-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-blob-hash-mobile-1.png new file mode 100644 index 0000000000..f8f4960916 Binary files /dev/null and b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-blob-hash-mobile-1.png differ diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-block-hash-mobile-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-block-hash-mobile-1.png index 650e6f77af..e706aff342 100644 Binary files a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-block-hash-mobile-1.png and b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-block-hash-mobile-1.png differ diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-block-number-mobile-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-block-number-mobile-1.png index aa030d6a62..2c4926aec8 100644 Binary files a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-block-number-mobile-1.png and b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-block-number-mobile-1.png differ diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-domain-name-mobile-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-domain-name-mobile-1.png new file mode 100644 index 0000000000..4a83be499a Binary files /dev/null and b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-domain-name-mobile-1.png differ diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-name-mobile-dark-mode-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-name-mobile-dark-mode-1.png index 22a47915c5..4997dd1819 100644 Binary files a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-name-mobile-dark-mode-1.png and b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-name-mobile-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-tx-hash-mobile-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-tx-hash-mobile-1.png index 77929a19f8..de1314a9cb 100644 Binary files a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-tx-hash-mobile-1.png and b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-tx-hash-mobile-1.png differ diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-user-op-hash-mobile-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-user-op-hash-mobile-1.png index a23bc41866..9fea2b448f 100644 Binary files a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-user-op-hash-mobile-1.png and b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-user-op-hash-mobile-1.png differ diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_with-apps-default-view-mobile-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_with-apps-default-view-mobile-1.png index f6f6b4c8b3..60bf4b9a6b 100644 Binary files a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_with-apps-default-view-mobile-1.png and b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_with-apps-default-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-blob-hash-mobile-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-blob-hash-mobile-1.png new file mode 100644 index 0000000000..4c31703d79 Binary files /dev/null and b/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-blob-hash-mobile-1.png differ diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-block-number-mobile-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-block-number-mobile-1.png index 95c8ef3f0a..ab3db505a4 100644 Binary files a/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-block-number-mobile-1.png and b/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-block-number-mobile-1.png differ diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-domain-name-mobile-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-domain-name-mobile-1.png new file mode 100644 index 0000000000..044408cac3 Binary files /dev/null and b/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-domain-name-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ShibariumDeposits.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/ShibariumDeposits.pw.tsx_default_base-view-mobile-1.png index 7cab3d682e..a7b79f5741 100644 Binary files a/ui/pages/__screenshots__/ShibariumDeposits.pw.tsx_default_base-view-mobile-1.png and b/ui/pages/__screenshots__/ShibariumDeposits.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ShibariumDeposits.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/ShibariumDeposits.pw.tsx_mobile_base-view-mobile-1.png index 0335fb3110..7ac99bfd32 100644 Binary files a/ui/pages/__screenshots__/ShibariumDeposits.pw.tsx_mobile_base-view-mobile-1.png and b/ui/pages/__screenshots__/ShibariumDeposits.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ShibariumWithdrawals.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/ShibariumWithdrawals.pw.tsx_default_base-view-mobile-1.png index 47e093f593..457bdbb859 100644 Binary files a/ui/pages/__screenshots__/ShibariumWithdrawals.pw.tsx_default_base-view-mobile-1.png and b/ui/pages/__screenshots__/ShibariumWithdrawals.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ShibariumWithdrawals.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/ShibariumWithdrawals.pw.tsx_mobile_base-view-mobile-1.png index 9f9063b226..a56071f28e 100644 Binary files a/ui/pages/__screenshots__/ShibariumWithdrawals.pw.tsx_mobile_base-view-mobile-1.png and b/ui/pages/__screenshots__/ShibariumWithdrawals.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/Token.pw.tsx_default_base-view-1.png b/ui/pages/__screenshots__/Token.pw.tsx_default_base-view-1.png index e8e194d881..3275b0e094 100644 Binary files a/ui/pages/__screenshots__/Token.pw.tsx_default_base-view-1.png and b/ui/pages/__screenshots__/Token.pw.tsx_default_base-view-1.png differ diff --git a/ui/pages/__screenshots__/Token.pw.tsx_default_bridged-token-1.png b/ui/pages/__screenshots__/Token.pw.tsx_default_bridged-token-1.png index 6288560904..6a3328be1c 100644 Binary files a/ui/pages/__screenshots__/Token.pw.tsx_default_bridged-token-1.png and b/ui/pages/__screenshots__/Token.pw.tsx_default_bridged-token-1.png differ diff --git a/ui/pages/__screenshots__/Token.pw.tsx_default_mobile-base-view-1.png b/ui/pages/__screenshots__/Token.pw.tsx_default_mobile-base-view-1.png index bf6ba7e403..4c799af178 100644 Binary files a/ui/pages/__screenshots__/Token.pw.tsx_default_mobile-base-view-1.png and b/ui/pages/__screenshots__/Token.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/pages/__screenshots__/Token.pw.tsx_default_mobile-with-verified-info-1.png b/ui/pages/__screenshots__/Token.pw.tsx_default_mobile-with-verified-info-1.png index 39df838aae..0ca82751f1 100644 Binary files a/ui/pages/__screenshots__/Token.pw.tsx_default_mobile-with-verified-info-1.png and b/ui/pages/__screenshots__/Token.pw.tsx_default_mobile-with-verified-info-1.png differ diff --git a/ui/pages/__screenshots__/Token.pw.tsx_default_with-verified-info-1.png b/ui/pages/__screenshots__/Token.pw.tsx_default_with-verified-info-1.png index 191f0b5a25..83e9b9e25c 100644 Binary files a/ui/pages/__screenshots__/Token.pw.tsx_default_with-verified-info-1.png and b/ui/pages/__screenshots__/Token.pw.tsx_default_with-verified-info-1.png differ diff --git a/ui/pages/__screenshots__/Tokens.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png b/ui/pages/__screenshots__/Tokens.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png index 9ca4d23064..1e3f0939a6 100644 Binary files a/ui/pages/__screenshots__/Tokens.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png and b/ui/pages/__screenshots__/Tokens.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Tokens.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-2.png b/ui/pages/__screenshots__/Tokens.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-2.png index 06a1553f0d..d2915eb093 100644 Binary files a/ui/pages/__screenshots__/Tokens.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-2.png and b/ui/pages/__screenshots__/Tokens.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-2.png differ diff --git a/ui/pages/__screenshots__/Tokens.pw.tsx_default_base-view-mobile-dark-mode-1.png b/ui/pages/__screenshots__/Tokens.pw.tsx_default_base-view-mobile-dark-mode-1.png index c7e5cb7e25..330bd78e34 100644 Binary files a/ui/pages/__screenshots__/Tokens.pw.tsx_default_base-view-mobile-dark-mode-1.png and b/ui/pages/__screenshots__/Tokens.pw.tsx_default_base-view-mobile-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Tokens.pw.tsx_default_base-view-mobile-dark-mode-2.png b/ui/pages/__screenshots__/Tokens.pw.tsx_default_base-view-mobile-dark-mode-2.png index 2624b74d24..1f1755e8d7 100644 Binary files a/ui/pages/__screenshots__/Tokens.pw.tsx_default_base-view-mobile-dark-mode-2.png and b/ui/pages/__screenshots__/Tokens.pw.tsx_default_base-view-mobile-dark-mode-2.png differ diff --git a/ui/pages/__screenshots__/Tokens.pw.tsx_default_bridged-tokens-base-view-1.png b/ui/pages/__screenshots__/Tokens.pw.tsx_default_bridged-tokens-base-view-1.png index 7f6f5f467c..0727fd449e 100644 Binary files a/ui/pages/__screenshots__/Tokens.pw.tsx_default_bridged-tokens-base-view-1.png and b/ui/pages/__screenshots__/Tokens.pw.tsx_default_bridged-tokens-base-view-1.png differ diff --git a/ui/pages/__screenshots__/Tokens.pw.tsx_default_bridged-tokens-base-view-2.png b/ui/pages/__screenshots__/Tokens.pw.tsx_default_bridged-tokens-base-view-2.png index f2f79d6bcb..3b9f4ed162 100644 Binary files a/ui/pages/__screenshots__/Tokens.pw.tsx_default_bridged-tokens-base-view-2.png and b/ui/pages/__screenshots__/Tokens.pw.tsx_default_bridged-tokens-base-view-2.png differ diff --git a/ui/pages/__screenshots__/Tokens.pw.tsx_mobile_base-view-mobile-dark-mode-1.png b/ui/pages/__screenshots__/Tokens.pw.tsx_mobile_base-view-mobile-dark-mode-1.png index a5d0f10c5b..8d91310520 100644 Binary files a/ui/pages/__screenshots__/Tokens.pw.tsx_mobile_base-view-mobile-dark-mode-1.png and b/ui/pages/__screenshots__/Tokens.pw.tsx_mobile_base-view-mobile-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Tokens.pw.tsx_mobile_base-view-mobile-dark-mode-2.png b/ui/pages/__screenshots__/Tokens.pw.tsx_mobile_base-view-mobile-dark-mode-2.png index 5dee58eb04..9c2ea5a88a 100644 Binary files a/ui/pages/__screenshots__/Tokens.pw.tsx_mobile_base-view-mobile-dark-mode-2.png and b/ui/pages/__screenshots__/Tokens.pw.tsx_mobile_base-view-mobile-dark-mode-2.png differ diff --git a/ui/pages/__screenshots__/UserOp.pw.tsx_default_base-view-1.png b/ui/pages/__screenshots__/UserOp.pw.tsx_default_base-view-1.png index eb4f533595..25ac4e7eb3 100644 Binary files a/ui/pages/__screenshots__/UserOp.pw.tsx_default_base-view-1.png and b/ui/pages/__screenshots__/UserOp.pw.tsx_default_base-view-1.png differ diff --git a/ui/pages/__screenshots__/UserOp.pw.tsx_default_mobile-base-view-1.png b/ui/pages/__screenshots__/UserOp.pw.tsx_default_mobile-base-view-1.png index fbe5973dc2..3be7bed259 100644 Binary files a/ui/pages/__screenshots__/UserOp.pw.tsx_default_mobile-base-view-1.png and b/ui/pages/__screenshots__/UserOp.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/pages/__screenshots__/UserOps.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/UserOps.pw.tsx_default_base-view-mobile-1.png index b17c113b1b..97b8d3e2aa 100644 Binary files a/ui/pages/__screenshots__/UserOps.pw.tsx_default_base-view-mobile-1.png and b/ui/pages/__screenshots__/UserOps.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/UserOps.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/UserOps.pw.tsx_mobile_base-view-mobile-1.png index 3f7ecc7900..757be59890 100644 Binary files a/ui/pages/__screenshots__/UserOps.pw.tsx_mobile_base-view-mobile-1.png and b/ui/pages/__screenshots__/UserOps.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/Validators.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/Validators.pw.tsx_default_base-view-mobile-1.png index dbd1ff0f1d..7e19851028 100644 Binary files a/ui/pages/__screenshots__/Validators.pw.tsx_default_base-view-mobile-1.png and b/ui/pages/__screenshots__/Validators.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/Validators.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/Validators.pw.tsx_mobile_base-view-mobile-1.png index ccbbc644c3..04ced8f304 100644 Binary files a/ui/pages/__screenshots__/Validators.pw.tsx_mobile_base-view-mobile-1.png and b/ui/pages/__screenshots__/Validators.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/VerifiedAddresses.pw.tsx_default_user-without-email-1.png b/ui/pages/__screenshots__/VerifiedAddresses.pw.tsx_default_user-without-email-1.png new file mode 100644 index 0000000000..216748289b Binary files /dev/null and b/ui/pages/__screenshots__/VerifiedAddresses.pw.tsx_default_user-without-email-1.png differ diff --git a/ui/pages/__screenshots__/VerifiedContracts.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/VerifiedContracts.pw.tsx_default_base-view-mobile-1.png index f5019a9a34..3a5eb8c821 100644 Binary files a/ui/pages/__screenshots__/VerifiedContracts.pw.tsx_default_base-view-mobile-1.png and b/ui/pages/__screenshots__/VerifiedContracts.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/VerifiedContracts.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/VerifiedContracts.pw.tsx_mobile_base-view-mobile-1.png index ecd79eb7e6..1d34aeadae 100644 Binary files a/ui/pages/__screenshots__/VerifiedContracts.pw.tsx_mobile_base-view-mobile-1.png and b/ui/pages/__screenshots__/VerifiedContracts.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/Withdrawals.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/Withdrawals.pw.tsx_default_base-view-mobile-1.png new file mode 100644 index 0000000000..6d40ccf1aa Binary files /dev/null and b/ui/pages/__screenshots__/Withdrawals.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/Withdrawals.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/Withdrawals.pw.tsx_mobile_base-view-mobile-1.png new file mode 100644 index 0000000000..a74f498008 Binary files /dev/null and b/ui/pages/__screenshots__/Withdrawals.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ZkEvmL2Deposits.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/ZkEvmL2Deposits.pw.tsx_default_base-view-mobile-1.png new file mode 100644 index 0000000000..1d18b041db Binary files /dev/null and b/ui/pages/__screenshots__/ZkEvmL2Deposits.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ZkEvmL2Deposits.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/ZkEvmL2Deposits.pw.tsx_mobile_base-view-mobile-1.png new file mode 100644 index 0000000000..203839311c Binary files /dev/null and b/ui/pages/__screenshots__/ZkEvmL2Deposits.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ZkEvmL2TxnBatch.pw.tsx_default_base-view-1.png b/ui/pages/__screenshots__/ZkEvmL2TxnBatch.pw.tsx_default_base-view-1.png index 342942ece0..c816635460 100644 Binary files a/ui/pages/__screenshots__/ZkEvmL2TxnBatch.pw.tsx_default_base-view-1.png and b/ui/pages/__screenshots__/ZkEvmL2TxnBatch.pw.tsx_default_base-view-1.png differ diff --git a/ui/pages/__screenshots__/ZkEvmL2TxnBatch.pw.tsx_default_mobile-base-view-1.png b/ui/pages/__screenshots__/ZkEvmL2TxnBatch.pw.tsx_default_mobile-base-view-1.png index d978d60f7a..f61caf8af7 100644 Binary files a/ui/pages/__screenshots__/ZkEvmL2TxnBatch.pw.tsx_default_mobile-base-view-1.png and b/ui/pages/__screenshots__/ZkEvmL2TxnBatch.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/pages/__screenshots__/ZkEvmL2TxnBatches.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/ZkEvmL2TxnBatches.pw.tsx_default_base-view-mobile-1.png index 028be0242b..65204d5399 100644 Binary files a/ui/pages/__screenshots__/ZkEvmL2TxnBatches.pw.tsx_default_base-view-mobile-1.png and b/ui/pages/__screenshots__/ZkEvmL2TxnBatches.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ZkEvmL2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/ZkEvmL2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png index 581620d596..43d3d24f71 100644 Binary files a/ui/pages/__screenshots__/ZkEvmL2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png and b/ui/pages/__screenshots__/ZkEvmL2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ZkEvmL2Withdrawals.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/ZkEvmL2Withdrawals.pw.tsx_default_base-view-mobile-1.png new file mode 100644 index 0000000000..b099a63a5e Binary files /dev/null and b/ui/pages/__screenshots__/ZkEvmL2Withdrawals.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ZkEvmL2Withdrawals.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/ZkEvmL2Withdrawals.pw.tsx_mobile_base-view-mobile-1.png new file mode 100644 index 0000000000..97906ab6e0 Binary files /dev/null and b/ui/pages/__screenshots__/ZkEvmL2Withdrawals.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ZkSyncL2TxnBatch.pw.tsx_default_base-view-1.png b/ui/pages/__screenshots__/ZkSyncL2TxnBatch.pw.tsx_default_base-view-1.png new file mode 100644 index 0000000000..7c1cd303a7 Binary files /dev/null and b/ui/pages/__screenshots__/ZkSyncL2TxnBatch.pw.tsx_default_base-view-1.png differ diff --git a/ui/pages/__screenshots__/ZkSyncL2TxnBatch.pw.tsx_default_mobile-base-view-1.png b/ui/pages/__screenshots__/ZkSyncL2TxnBatch.pw.tsx_default_mobile-base-view-1.png new file mode 100644 index 0000000000..3fdca929ba Binary files /dev/null and b/ui/pages/__screenshots__/ZkSyncL2TxnBatch.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/pages/__screenshots__/ZkSyncL2TxnBatches.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/ZkSyncL2TxnBatches.pw.tsx_default_base-view-mobile-1.png new file mode 100644 index 0000000000..ca59213667 Binary files /dev/null and b/ui/pages/__screenshots__/ZkSyncL2TxnBatches.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ZkSyncL2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/ZkSyncL2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png new file mode 100644 index 0000000000..486be5b14d Binary files /dev/null and b/ui/pages/__screenshots__/ZkSyncL2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/searchResults/SearchResultListItem.tsx b/ui/searchResults/SearchResultListItem.tsx index 743d378853..d09cb77983 100644 --- a/ui/searchResults/SearchResultListItem.tsx +++ b/ui/searchResults/SearchResultListItem.tsx @@ -12,7 +12,9 @@ import * as mixpanel from 'lib/mixpanel/index'; import { saveToRecentKeywords } from 'lib/recentSearchKeywords'; import { ADDRESS_REGEXP } from 'lib/validations/address'; import * as AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import * as BlobEntity from 'ui/shared/entities/blob/BlobEntity'; import * as BlockEntity from 'ui/shared/entities/block/BlockEntity'; +import * as EnsEntity from 'ui/shared/entities/ens/EnsEntity'; import * as TokenEntity from 'ui/shared/entities/token/TokenEntity'; import * as TxEntity from 'ui/shared/entities/tx/TxEntity'; import * as UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity'; @@ -200,6 +202,28 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => { ); } + + case 'blob': { + return ( + + + + + + + ); + } + case 'user_operation': { return ( @@ -220,6 +244,30 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => { ); } + + case 'ens_domain': { + return ( + + + + + + + ); + } } })(); @@ -311,6 +359,21 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => { ) : null; } + case 'ens_domain': { + const expiresText = data.ens_info?.expiry_date ? ` expires ${ dayjs(data.ens_info.expiry_date).fromNow() }` : ''; + return ( + + + + + { + data.ens_info.names_count > 1 ? + ({ data.ens_info.names_count > 39 ? '40+' : `+${ data.ens_info.names_count - 1 }` }) : + { expiresText } + } + + ); + } default: return null; diff --git a/ui/searchResults/SearchResultTableItem.tsx b/ui/searchResults/SearchResultTableItem.tsx index 6e3ea5140c..d9a1cf6d74 100644 --- a/ui/searchResults/SearchResultTableItem.tsx +++ b/ui/searchResults/SearchResultTableItem.tsx @@ -12,7 +12,9 @@ import * as mixpanel from 'lib/mixpanel/index'; import { saveToRecentKeywords } from 'lib/recentSearchKeywords'; import { ADDRESS_REGEXP } from 'lib/validations/address'; import * as AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import * as BlobEntity from 'ui/shared/entities/blob/BlobEntity'; import * as BlockEntity from 'ui/shared/entities/block/BlockEntity'; +import * as EnsEntity from 'ui/shared/entities/ens/EnsEntity'; import * as TokenEntity from 'ui/shared/entities/token/TokenEntity'; import * as TxEntity from 'ui/shared/entities/tx/TxEntity'; import * as UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity'; @@ -285,6 +287,30 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => { ); } + + case 'blob': { + return ( +
+ ); + } + case 'user_operation': { return ( <> @@ -312,6 +338,48 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => { ); } + + case 'ens_domain': { + const expiresText = data.ens_info?.expiry_date ? ` expires ${ dayjs(data.ens_info.expiry_date).fromNow() }` : ''; + return ( + <> + + + + + ); + } } })(); diff --git a/ui/searchResults/SearchResultsInput.tsx b/ui/searchResults/SearchResultsInput.tsx index a50b82b4e4..3559b1978d 100644 --- a/ui/searchResults/SearchResultsInput.tsx +++ b/ui/searchResults/SearchResultsInput.tsx @@ -5,6 +5,7 @@ import React from 'react'; import useIsMobile from 'lib/hooks/useIsMobile'; import { getRecentSearchKeywords } from 'lib/recentSearchKeywords'; +import SearchBarBackdrop from 'ui/snippets/searchBar/SearchBarBackdrop'; import SearchBarInput from 'ui/snippets/searchBar/SearchBarInput'; import SearchBarRecentKeywords from 'ui/snippets/searchBar/SearchBarRecentKeywords'; @@ -66,33 +67,39 @@ const SearchResultsInput = ({ searchTerm, handleSubmit, handleSearchTermChange } }; }, [ calculateMenuWidth ]); + const isSuggestOpen = isOpen && recentSearchKeywords.length > 0 && searchTerm.trim().length === 0; + return ( - 0 && searchTerm.trim().length === 0 } - autoFocus={ false } - onClose={ onClose } - placement="bottom-start" - offset={ isMobile ? [ 16, -12 ] : undefined } - isLazy - > - - - - - - - - - + <> + + + + + + + + + + + + ); }; diff --git a/ui/shared/AccountActionsMenu/AccountActionsMenu.pw.tsx b/ui/shared/AccountActionsMenu/AccountActionsMenu.pw.tsx index 891fb4fb04..814b380a81 100644 --- a/ui/shared/AccountActionsMenu/AccountActionsMenu.pw.tsx +++ b/ui/shared/AccountActionsMenu/AccountActionsMenu.pw.tsx @@ -1,10 +1,16 @@ -import { test, expect } from '@playwright/experimental-ct-react'; +import type { BrowserContext } from '@playwright/test'; import React from 'react'; -import TestApp from 'playwright/TestApp'; +import * as profileMock from 'mocks/user/profile'; +import { contextWithAuth } from 'playwright/fixtures/auth'; +import { test as base, expect } from 'playwright/lib'; import AccountActionsMenu from './AccountActionsMenu'; +const test = base.extend<{ context: BrowserContext }>({ + context: contextWithAuth, +}); + test.use({ viewport: { width: 200, height: 200 } }); test.describe('with multiple items', async() => { @@ -16,50 +22,32 @@ test.describe('with multiple items', async() => { }, }; - test('base view', async({ mount, page }) => { - const component = await mount( - - - , - { hooksConfig }, - ); + test.beforeEach(async({ mockApiResponse }) => { + mockApiResponse('user_info', profileMock.base); + }); + test('base view', async({ render, page }) => { + const component = await render(, { hooksConfig }); await component.getByRole('button').click(); await expect(page).toHaveScreenshot(); }); - test('base view with styles', async({ mount, page }) => { - const component = await mount( - - - , - { hooksConfig }, - ); - + test('base view with styles', async({ render, page }) => { + const component = await render(, { hooksConfig }); await component.getByRole('button').click(); await expect(page).toHaveScreenshot(); }); - test('loading', async({ mount }) => { - const component = await mount( - - - , - { hooksConfig }, - ); + test('loading', async({ render }) => { + const component = await render(, { hooksConfig }); await expect(component).toHaveScreenshot(); }); - test('loading with styles', async({ mount }) => { - const component = await mount( - - - , - { hooksConfig }, - ); + test('loading with styles', async({ render }) => { + const component = await render(, { hooksConfig }); await expect(component).toHaveScreenshot(); }); @@ -74,39 +62,22 @@ test.describe('with one item', async() => { }, }; - test('base view', async({ mount, page }) => { - const component = await mount( - - - , - { hooksConfig }, - ); - + test('base view', async({ render, page }) => { + const component = await render(, { hooksConfig }); await component.getByRole('button').hover(); await expect(page).toHaveScreenshot(); }); - test('base view with styles', async({ mount, page }) => { - const component = await mount( - - - , - { hooksConfig }, - ); - + test('base view with styles', async({ render, page }) => { + const component = await render(, { hooksConfig }); await component.getByRole('button').hover(); await expect(page).toHaveScreenshot(); }); - test('loading', async({ mount }) => { - const component = await mount( - - - , - { hooksConfig }, - ); + test('loading', async({ render }) => { + const component = await render(, { hooksConfig }); await expect(component).toHaveScreenshot(); }); diff --git a/ui/shared/AccountActionsMenu/AccountActionsMenu.tsx b/ui/shared/AccountActionsMenu/AccountActionsMenu.tsx index 1818c399a8..d443dc2691 100644 --- a/ui/shared/AccountActionsMenu/AccountActionsMenu.tsx +++ b/ui/shared/AccountActionsMenu/AccountActionsMenu.tsx @@ -5,6 +5,7 @@ import React from 'react'; import type { ItemProps } from './types'; import config from 'configs/app'; +import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo'; import useIsAccountActionAllowed from 'lib/hooks/useIsAccountActionAllowed'; import * as mixpanel from 'lib/mixpanel/index'; import getQueryParamString from 'lib/router/getQueryParamString'; @@ -27,6 +28,8 @@ const AccountActionsMenu = ({ isLoading, className }: Props) => { const isTxPage = router.pathname === '/tx/[hash]'; const isAccountActionAllowed = useIsAccountActionAllowed(); + const userInfoQuery = useFetchProfileInfo(); + const handleButtonClick = React.useCallback(() => { mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Address actions (more button)' }); }, []); @@ -35,10 +38,12 @@ const AccountActionsMenu = ({ isLoading, className }: Props) => { return null; } + const userWithoutEmail = userInfoQuery.data && !userInfoQuery.data.email; + const items = [ { render: (props: ItemProps) => , - enabled: isTokenPage && config.features.addressVerification.isEnabled, + enabled: isTokenPage && config.features.addressVerification.isEnabled && !userWithoutEmail, }, { render: (props: ItemProps) => , diff --git a/ui/shared/AdditionalInfoButton.tsx b/ui/shared/AdditionalInfoButton.tsx index 5b1002bfeb..de15304086 100644 --- a/ui/shared/AdditionalInfoButton.tsx +++ b/ui/shared/AdditionalInfoButton.tsx @@ -37,6 +37,7 @@ const AdditionalInfoButton = ({ isOpen, onClick, className, isLoading }: Props, onClick={ onClick } cursor="pointer" flexShrink={ 0 } + aria-label="Transaction info" > { + const router = useRouter(); + + const hash = getQueryParamString(router.query.hash); + const isTokenPage = router.pathname === '/token/[hash]'; + const isAccountActionAllowed = useIsAccountActionAllowed(); + + return ( + + + + + More + + + + + + { isTokenPage && config.features.addressVerification.isEnabled && + } + + + + + ); +}; + +export default React.memo(AddressActions); diff --git a/ui/shared/AddressActions/PrivateTagMenuItem.tsx b/ui/shared/AddressActions/PrivateTagMenuItem.tsx new file mode 100644 index 0000000000..452a2d2599 --- /dev/null +++ b/ui/shared/AddressActions/PrivateTagMenuItem.tsx @@ -0,0 +1,58 @@ +import { MenuItem, Icon, chakra, useDisclosure } from '@chakra-ui/react'; +import { useQueryClient } from '@tanstack/react-query'; +import React from 'react'; + +import type { Address } from 'types/api/address'; + +import iconPrivateTags from 'icons/privattags.svg'; +import { getResourceKey } from 'lib/api/useApiQuery'; +import PrivateTagModal from 'ui/privateTags/AddressModal/AddressModal'; + +interface Props { + className?: string; + hash: string; + onBeforeClick: () => boolean; +} + +const PrivateTagMenuItem = ({ className, hash, onBeforeClick }: Props) => { + const modal = useDisclosure(); + const queryClient = useQueryClient(); + + const queryKey = getResourceKey('address', { pathParams: { hash } }); + const addressData = queryClient.getQueryData
(queryKey); + + const handleClick = React.useCallback(() => { + if (!onBeforeClick()) { + return; + } + + modal.onOpen(); + }, [ modal, onBeforeClick ]); + + const handleAddPrivateTag = React.useCallback(async() => { + await queryClient.refetchQueries({ queryKey }); + modal.onClose(); + }, [ queryClient, queryKey, modal ]); + + const formData = React.useMemo(() => { + return { + address_hash: hash, + }; + }, [ hash ]); + + if (addressData?.private_tags?.length) { + return null; + } + + return ( + <> + + + Add private tag + + + + ); +}; + +export default React.memo(chakra(PrivateTagMenuItem)); diff --git a/ui/shared/AddressActions/PublicTagMenuItem.tsx b/ui/shared/AddressActions/PublicTagMenuItem.tsx new file mode 100644 index 0000000000..2f3c0c0e32 --- /dev/null +++ b/ui/shared/AddressActions/PublicTagMenuItem.tsx @@ -0,0 +1,32 @@ +import { MenuItem, Icon, chakra } from '@chakra-ui/react'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import iconPublicTags from 'icons/publictags.svg'; + +interface Props { + className?: string; + hash: string; + onBeforeClick: () => boolean; +} + +const PublicTagMenuItem = ({ className, hash, onBeforeClick }: Props) => { + const router = useRouter(); + + const handleClick = React.useCallback(() => { + if (!onBeforeClick()) { + return; + } + + router.push({ pathname: '/account/public-tags-request', query: { address: hash } }); + }, [ hash, onBeforeClick, router ]); + + return ( + + + Add public tag + + ); +}; + +export default React.memo(chakra(PublicTagMenuItem)); diff --git a/ui/shared/AddressActions/TokenInfoMenuItem.tsx b/ui/shared/AddressActions/TokenInfoMenuItem.tsx new file mode 100644 index 0000000000..ad01af5243 --- /dev/null +++ b/ui/shared/AddressActions/TokenInfoMenuItem.tsx @@ -0,0 +1,106 @@ +import { MenuItem, Icon, chakra, useDisclosure } from '@chakra-ui/react'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { Route } from 'nextjs-routes'; + +import config from 'configs/app'; +import iconEdit from 'icons/edit.svg'; +import useApiQuery from 'lib/api/useApiQuery'; +import useHasAccount from 'lib/hooks/useHasAccount'; +import AddressVerificationModal from 'ui/addressVerification/AddressVerificationModal'; + +interface Props { + className?: string; + hash: string; + onBeforeClick: (route: Route) => boolean; +} + +const TokenInfoMenuItem = ({ className, hash, onBeforeClick }: Props) => { + const router = useRouter(); + const modal = useDisclosure(); + const isAuth = useHasAccount(); + + const verifiedAddressesQuery = useApiQuery('verified_addresses', { + pathParams: { chainId: config.chain.id }, + queryOptions: { + enabled: isAuth, + }, + }); + const applicationsQuery = useApiQuery('token_info_applications', { + pathParams: { chainId: config.chain.id, id: undefined }, + queryOptions: { + enabled: isAuth, + }, + }); + const tokenInfoQuery = useApiQuery('token_verified_info', { + pathParams: { hash, chainId: config.chain.id }, + queryOptions: { + refetchOnMount: false, + }, + }); + + const handleAddAddressClick = React.useCallback(() => { + if (!onBeforeClick({ pathname: '/account/verified-addresses' })) { + return; + } + + modal.onOpen(); + }, [ modal, onBeforeClick ]); + + const handleAddApplicationClick = React.useCallback(async() => { + router.push({ pathname: '/account/verified-addresses', query: { address: hash } }); + }, [ hash, router ]); + + const handleVerifiedAddressSubmit = React.useCallback(async() => { + await verifiedAddressesQuery.refetch(); + }, [ verifiedAddressesQuery ]); + + const handleShowMyAddressesClick = React.useCallback(async() => { + router.push({ pathname: '/account/verified-addresses' }); + }, [ router ]); + + const icon = ; + + const content = (() => { + if (!verifiedAddressesQuery.data?.verifiedAddresses.find(({ contractAddress }) => contractAddress.toLowerCase() === hash.toLowerCase())) { + return ( + + { icon } + { tokenInfoQuery.data?.tokenAddress ? 'Update token info' : 'Add token info' } + + ); + } + + const hasApplication = applicationsQuery.data?.submissions.some(({ tokenAddress }) => tokenAddress.toLowerCase() === hash.toLowerCase()); + + return ( + + { icon } + + { + hasApplication || tokenInfoQuery.data?.tokenAddress ? + 'Update token info' : + 'Add token info' + } + + + ); + })(); + + return ( + <> + { content } + + + ); +}; + +export default React.memo(chakra(TokenInfoMenuItem)); diff --git a/ui/shared/AddressHeadingInfo.tsx b/ui/shared/AddressHeadingInfo.tsx new file mode 100644 index 0000000000..31a000f5f7 --- /dev/null +++ b/ui/shared/AddressHeadingInfo.tsx @@ -0,0 +1,42 @@ +import { Flex } from '@chakra-ui/react'; +import React from 'react'; + +import type { Address } from 'types/api/address'; +import type { TokenInfo } from 'types/api/token'; + +import config from 'configs/app'; +import AddressFavoriteButton from 'ui/address/details/AddressFavoriteButton'; +import AddressQrCode from 'ui/address/details/AddressQrCode'; +import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet'; +import AddressActionsMenu from 'ui/shared/AddressActions/Menu'; +import AddressEntity from 'ui/shared/entities/address/AddressEntity'; + +interface Props { + address: Pick; + token?: TokenInfo | null; + isLinkDisabled?: boolean; + isLoading?: boolean; +} + +const AddressHeadingInfo = ({ address, token, isLinkDisabled, isLoading }: Props) => { + return ( + + + { !isLoading && address.is_contract && token && } + { !isLoading && !address.is_contract && config.features.account.isEnabled && ( + + ) } + + { config.features.account.isEnabled && } + + ); +}; + +export default AddressHeadingInfo; diff --git a/ui/shared/AppError/AppError.pw.tsx b/ui/shared/AppError/AppError.pw.tsx index a3405e2b99..36f23b03f3 100644 --- a/ui/shared/AppError/AppError.pw.tsx +++ b/ui/shared/AppError/AppError.pw.tsx @@ -36,8 +36,8 @@ test('status code 500', async({ mount }) => { await expect(component).toHaveScreenshot(); }); -test('invalid tx hash', async({ mount }) => { - const error = { message: 'Invalid tx hash', cause: { status: 422, resource: 'tx' } } as Error; +test('tx not found', async({ mount }) => { + const error = { message: 'Not found', cause: { status: 404, resource: 'tx' } } as Error; const component = await mount( diff --git a/ui/shared/AppError/AppError.tsx b/ui/shared/AppError/AppError.tsx index b5199d5e5b..e3a0a16aff 100644 --- a/ui/shared/AppError/AppError.tsx +++ b/ui/shared/AppError/AppError.tsx @@ -11,8 +11,8 @@ import getResourceErrorPayload from 'lib/errors/getResourceErrorPayload'; import AppErrorIcon from './AppErrorIcon'; import AppErrorTitle from './AppErrorTitle'; import AppErrorBlockConsensus from './custom/AppErrorBlockConsensus'; -import AppErrorInvalidTxHash from './custom/AppErrorInvalidTxHash'; import AppErrorTooManyRequests from './custom/AppErrorTooManyRequests'; +import AppErrorTxNotFound from './custom/AppErrorTxNotFound'; interface Props { className?: string; @@ -47,11 +47,11 @@ const AppError = ({ error, className }: Props) => { undefined; const statusCode = getErrorCauseStatusCode(error) || getErrorObjStatusCode(error); - const isInvalidTxHash = cause && 'resource' in cause && cause.resource === 'tx' && statusCode === 422; + const isInvalidTxHash = cause && 'resource' in cause && cause.resource === 'tx' && statusCode === 404; const isBlockConsensus = messageInPayload?.includes('Block lost consensus'); if (isInvalidTxHash) { - return ; + return ; } if (isBlockConsensus) { diff --git a/ui/shared/AppError/__screenshots__/AppError.pw.tsx_default_invalid-tx-hash-1.png b/ui/shared/AppError/__screenshots__/AppError.pw.tsx_default_invalid-tx-hash-1.png deleted file mode 100644 index b06fb92cb9..0000000000 Binary files a/ui/shared/AppError/__screenshots__/AppError.pw.tsx_default_invalid-tx-hash-1.png and /dev/null differ diff --git a/ui/shared/AppError/__screenshots__/AppError.pw.tsx_default_tx-not-found-1.png b/ui/shared/AppError/__screenshots__/AppError.pw.tsx_default_tx-not-found-1.png new file mode 100644 index 0000000000..bcb5755e61 Binary files /dev/null and b/ui/shared/AppError/__screenshots__/AppError.pw.tsx_default_tx-not-found-1.png differ diff --git a/ui/shared/AppError/custom/AppErrorInvalidTxHash.tsx b/ui/shared/AppError/custom/AppErrorTxNotFound.tsx similarity index 76% rename from ui/shared/AppError/custom/AppErrorInvalidTxHash.tsx rename to ui/shared/AppError/custom/AppErrorTxNotFound.tsx index a6da98596f..ee8ee0b00f 100644 --- a/ui/shared/AppError/custom/AppErrorInvalidTxHash.tsx +++ b/ui/shared/AppError/custom/AppErrorTxNotFound.tsx @@ -1,13 +1,14 @@ /* eslint-disable max-len */ -import { Box, OrderedList, ListItem, useColorModeValue, Flex } from '@chakra-ui/react'; +import { Box, OrderedList, ListItem, useColorModeValue, Flex, chakra, Button } from '@chakra-ui/react'; import React from 'react'; +import { route } from 'nextjs-routes'; + import IconSvg from 'ui/shared/IconSvg'; import AppErrorTitle from '../AppErrorTitle'; -const AppErrorInvalidTxHash = () => { - const textColor = useColorModeValue('gray.500', 'gray.400'); +const AppErrorTxNotFound = () => { const snippet = { borderColor: useColorModeValue('blackAlpha.300', 'whiteAlpha.300'), iconBg: useColorModeValue('blackAlpha.800', 'whiteAlpha.800'), @@ -36,7 +37,7 @@ const AppErrorInvalidTxHash = () => { - + If you have just submitted this transaction please wait for at least 30 seconds before refreshing this page. @@ -47,11 +48,22 @@ const AppErrorInvalidTxHash = () => { During times when the network is busy (i.e during ICOs) it can take a while for your transaction to propagate through the network and for us to index it. - If it still does not show up after 1 hour, please check with your sender/exchange/wallet/transaction provider for additional information. + If it still does not show up after 1 hour, please check with your + sender/exchange/wallet/transaction provider + for additional information. + ); }; -export default AppErrorInvalidTxHash; +export default AppErrorTxNotFound; diff --git a/ui/shared/AppError/isCustomAppError.ts b/ui/shared/AppError/isCustomAppError.ts new file mode 100644 index 0000000000..328368e2ae --- /dev/null +++ b/ui/shared/AppError/isCustomAppError.ts @@ -0,0 +1,8 @@ +import type { ResourceError } from 'lib/api/resources'; + +// status codes when custom error screen should be shown +const CUSTOM_STATUS_CODES = [ 404, 422, 429 ]; + +export default function isCustomAppError(error: ResourceError) { + return CUSTOM_STATUS_CODES.includes(error.status); +} diff --git a/ui/shared/CopyToClipboard.tsx b/ui/shared/CopyToClipboard.tsx index 4941909638..c3b67545de 100644 --- a/ui/shared/CopyToClipboard.tsx +++ b/ui/shared/CopyToClipboard.tsx @@ -7,9 +7,10 @@ export interface Props { text: string; className?: string; isLoading?: boolean; + onClick?: (event: React.MouseEvent) => void; } -const CopyToClipboard = ({ text, className, isLoading }: Props) => { +const CopyToClipboard = ({ text, className, isLoading, onClick }: Props) => { const { hasCopied, onCopy } = useClipboard(text, 1000); const [ copied, setCopied ] = useState(false); // have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107 @@ -24,8 +25,13 @@ const CopyToClipboard = ({ text, className, isLoading }: Props) => { } }, [ hasCopied ]); + const handleClick = React.useCallback((event: React.MouseEvent) => { + onCopy(); + onClick?.(event); + }, [ onClick, onCopy ]); + if (isLoading) { - return ; + return ; } return ( @@ -39,7 +45,7 @@ const CopyToClipboard = ({ text, className, isLoading }: Props) => { variant="simple" display="inline-block" flexShrink={ 0 } - onClick={ onCopy } + onClick={ handleClick } className={ className } onMouseEnter={ onOpen } onMouseLeave={ onClose } diff --git a/ui/tx/details/txDetailsActions/TxDetailsActionsWrapper.tsx b/ui/shared/DetailsActionsWrapper.tsx similarity index 77% rename from ui/tx/details/txDetailsActions/TxDetailsActionsWrapper.tsx rename to ui/shared/DetailsActionsWrapper.tsx index 5f8de8364e..a65932c4fa 100644 --- a/ui/tx/details/txDetailsActions/TxDetailsActionsWrapper.tsx +++ b/ui/shared/DetailsActionsWrapper.tsx @@ -9,9 +9,10 @@ const SCROLL_GRADIENT_HEIGHT = 48; type Props = { children: React.ReactNode; isLoading?: boolean; + type: 'tx' | 'user_op'; } -const TxDetailsActions = ({ children, isLoading }: Props) => { +const DetailsActionsWrapper = ({ children, isLoading, type }: Props) => { const containerRef = React.useRef(null); const [ hasScroll, setHasScroll ] = React.useState(false); @@ -25,8 +26,8 @@ const TxDetailsActions = ({ children, isLoading }: Props) => { return ( { ); }; -export default React.memo(TxDetailsActions); +export default React.memo(DetailsActionsWrapper); diff --git a/ui/shared/DetailsInfoItemDivider.tsx b/ui/shared/DetailsInfoItemDivider.tsx index 22c5b456e0..227837fa74 100644 --- a/ui/shared/DetailsInfoItemDivider.tsx +++ b/ui/shared/DetailsInfoItemDivider.tsx @@ -1,9 +1,16 @@ -import { GridItem } from '@chakra-ui/react'; +import { GridItem, chakra } from '@chakra-ui/react'; import React from 'react'; -const DetailsInfoItemDivider = () => { +interface Props { + className?: string; + id?: string; +} + +const DetailsInfoItemDivider = ({ className, id }: Props) => { return ( { ); }; -export default DetailsInfoItemDivider; +export default chakra(DetailsInfoItemDivider); diff --git a/ui/shared/EmptySearchResult.tsx b/ui/shared/EmptySearchResult.tsx index 92c27fbecc..93433c17c7 100644 --- a/ui/shared/EmptySearchResult.tsx +++ b/ui/shared/EmptySearchResult.tsx @@ -16,23 +16,21 @@ const EmptySearchResult = ({ text }: Props) => { display="flex" flexDirection="column" alignItems="center" + justifyContent="center" + mt="50px" > - + - + No results - + { text } diff --git a/ui/shared/EntityTags.tsx b/ui/shared/EntityTags.tsx deleted file mode 100644 index 7e0a62d94c..0000000000 --- a/ui/shared/EntityTags.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import type { ThemingProps } from '@chakra-ui/react'; -import { Flex, chakra, useDisclosure, Popover, PopoverTrigger, PopoverContent, PopoverBody } from '@chakra-ui/react'; -import React from 'react'; - -import type { UserTags } from 'types/api/addressParams'; - -import useIsMobile from 'lib/hooks/useIsMobile'; -import Tag from 'ui/shared/chakra/Tag'; - -interface TagData { - label: string; - display_name: string; - colorScheme?: ThemingProps<'Tag'>['colorScheme']; - variant?: ThemingProps<'Tag'>['variant']; -} - -interface Props { - className?: string; - data?: UserTags; - isLoading?: boolean; - tagsBefore?: Array; - tagsAfter?: Array; - contentAfter?: React.ReactNode; -} - -const EntityTags = ({ className, data, tagsBefore = [], tagsAfter = [], isLoading, contentAfter }: Props) => { - const isMobile = useIsMobile(); - const { isOpen, onToggle, onClose } = useDisclosure(); - - const tags: Array = [ - ...tagsBefore, - ...(data?.private_tags || []), - ...(data?.public_tags || []), - ...(data?.watchlist_names || []), - ...tagsAfter, - ] - .filter(Boolean); - - if (tags.length === 0 && !contentAfter) { - return null; - } - - const content = (() => { - if (isMobile && tags.length > 2) { - return ( - <> - { - tags - .slice(0, 2) - .map((tag) => ( - - { tag.display_name } - - )) - } - - - +{ tags.length - 1 } - - - - - { - tags - .slice(2) - .map((tag) => ( - - { tag.display_name } - - )) - } - - - - - - ); - } - - return tags.map((tag) => ( - - { tag.display_name } - - )); - })(); - - return ( - - { content } - { contentAfter } - - ); -}; - -export default React.memo(chakra(EntityTags)); diff --git a/ui/shared/EntityTags/EntityTag.pw.tsx b/ui/shared/EntityTags/EntityTag.pw.tsx new file mode 100644 index 0000000000..299c48cf31 --- /dev/null +++ b/ui/shared/EntityTags/EntityTag.pw.tsx @@ -0,0 +1,37 @@ +import { Box } from '@chakra-ui/react'; +import React from 'react'; + +import * as addressMetadataMock from 'mocks/metadata/address'; +import { test, expect } from 'playwright/lib'; + +import EntityTag from './EntityTag'; + +test.use({ viewport: { width: 400, height: 300 } }); + +test('custom name tag +@dark-mode', async({ render }) => { + const component = await render(); + await expect(component).toHaveScreenshot(); +}); + +test('generic tag +@dark-mode', async({ render }) => { + const component = await render(); + await expect(component).toHaveScreenshot(); +}); + +test('protocol tag +@dark-mode', async({ render }) => { + const component = await render(); + await expect(component).toHaveScreenshot(); +}); + +test('tag with link and long name +@dark-mode', async({ render }) => { + const component = await render(); + await expect(component).toHaveScreenshot(); +}); + +test('tag with tooltip +@dark-mode', async({ render, page, mockAssetResponse }) => { + await mockAssetResponse(addressMetadataMock.tagWithTooltip.meta?.tooltipIcon as string, './playwright/mocks/image_s.jpg'); + const component = await render(); + await component.getByText('BlockscoutHeroes').hover(); + await page.getByText('Blockscout team member').waitFor({ state: 'visible' }); + await expect(page).toHaveScreenshot(); +}); diff --git a/ui/shared/EntityTags/EntityTag.tsx b/ui/shared/EntityTags/EntityTag.tsx new file mode 100644 index 0000000000..5a83b3d4b4 --- /dev/null +++ b/ui/shared/EntityTags/EntityTag.tsx @@ -0,0 +1,51 @@ +import { chakra, Skeleton, Tag } from '@chakra-ui/react'; +import React from 'react'; + +import type { EntityTag as TEntityTag } from './types'; + +import IconSvg from 'ui/shared/IconSvg'; +import TruncatedValue from 'ui/shared/TruncatedValue'; + +import EntityTagLink from './EntityTagLink'; +import EntityTagPopover from './EntityTagPopover'; + +interface Props { + data: TEntityTag; + isLoading?: boolean; + truncate?: boolean; +} + +const EntityTag = ({ data, isLoading, truncate }: Props) => { + + if (isLoading) { + return ; + } + + // const hasLink = Boolean(data.meta?.tagUrl || data.tagType === 'generic' || data.tagType === 'protocol'); + // Change the condition when "Tag search" page is ready - issue #1869 + const hasLink = Boolean(data.meta?.tagUrl); + const iconColor = data.meta?.textColor ?? 'gray.400'; + + return ( + + + + { data.tagType === 'name' && } + { (data.tagType === 'protocol' || data.tagType === 'generic') && # } + + + + + ); +}; + +export default React.memo(EntityTag); diff --git a/ui/shared/EntityTags/EntityTagLink.tsx b/ui/shared/EntityTags/EntityTagLink.tsx new file mode 100644 index 0000000000..00f6b23115 --- /dev/null +++ b/ui/shared/EntityTags/EntityTagLink.tsx @@ -0,0 +1,70 @@ +import type { LinkProps } from '@chakra-ui/react'; +import React from 'react'; + +import type { EntityTag } from './types'; + +import * as mixpanel from 'lib/mixpanel/index'; +import LinkExternal from 'ui/shared/LinkExternal'; + +// import { route } from 'nextjs-routes'; +// import LinkInternal from 'ui/shared/LinkInternal'; + +interface Props { + data: EntityTag; + children: React.ReactNode; +} + +const EntityTagLink = ({ data, children }: Props) => { + + const handleLinkClick = React.useCallback(() => { + if (!data.meta?.tagUrl) { + return; + } + + mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { + Type: 'Address tag', + Info: data.slug, + URL: data.meta.tagUrl, + }); + }, [ data.meta?.tagUrl, data.slug ]); + + const linkProps: LinkProps = { + color: 'inherit', + display: 'inline-flex', + overflow: 'hidden', + _hover: { textDecor: 'none', color: 'inherit' }, + onClick: handleLinkClick, + }; + + // Uncomment this block when "Tag search" page is ready - issue #1869 + // switch (data.tagType) { + // case 'generic': + // case 'protocol': { + // return ( + // + // { children } + // + // ); + // } + // } + + if (data.meta?.tagUrl) { + return ( + + { children } + + ); + } + + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{ children }; +}; + +export default React.memo(EntityTagLink); diff --git a/ui/shared/EntityTags/EntityTagPopover.tsx b/ui/shared/EntityTags/EntityTagPopover.tsx new file mode 100644 index 0000000000..1451918ce8 --- /dev/null +++ b/ui/shared/EntityTags/EntityTagPopover.tsx @@ -0,0 +1,61 @@ +import { chakra, Image, Flex, Popover, PopoverArrow, PopoverBody, PopoverContent, PopoverTrigger, useColorModeValue, DarkMode } from '@chakra-ui/react'; +import React from 'react'; + +import type { EntityTag } from './types'; + +import makePrettyLink from 'lib/makePrettyLink'; +import * as mixpanel from 'lib/mixpanel/index'; +import LinkExternal from 'ui/shared/LinkExternal'; + +interface Props { + data: EntityTag; + children: React.ReactNode; +} + +const EntityTagPopover = ({ data, children }: Props) => { + const bgColor = useColorModeValue('gray.700', 'gray.900'); + const link = makePrettyLink(data.meta?.tooltipUrl); + const hasPopover = Boolean(data.meta?.tooltipIcon || data.meta?.tooltipTitle || data.meta?.tooltipDescription || data.meta?.tooltipUrl); + + const handleLinkClick = React.useCallback(() => { + if (!data.meta?.tooltipUrl) { + return; + } + + mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { + Type: 'Address tag', + Info: data.slug, + URL: data.meta.tooltipUrl, + }); + }, [ data.meta?.tooltipUrl, data.slug ]); + + if (!hasPopover) { + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{ children }; + } + + return ( + + + { children } + + + + + + { (data.meta?.tooltipIcon || data.meta?.tooltipTitle) && ( + + { data.meta?.tooltipIcon && } + { data.meta?.tooltipTitle && { data.meta.tooltipTitle } } + + ) } + { data.meta?.tooltipDescription && { data.meta.tooltipDescription } } + { link && { link.domain } } + + + + + ); +}; + +export default React.memo(EntityTagPopover); diff --git a/ui/shared/EntityTags/EntityTags.tsx b/ui/shared/EntityTags/EntityTags.tsx new file mode 100644 index 0000000000..26698b8be2 --- /dev/null +++ b/ui/shared/EntityTags/EntityTags.tsx @@ -0,0 +1,69 @@ +import { Box, Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger, chakra } from '@chakra-ui/react'; +import React from 'react'; + +import type { EntityTag as TEntityTag } from './types'; + +import config from 'configs/app'; +import useIsMobile from 'lib/hooks/useIsMobile'; +import Tag from 'ui/shared/chakra/Tag'; + +import EntityTag from './EntityTag'; + +interface Props { + className?: string; + tags: Array; + isLoading?: boolean; +} + +const EntityTags = ({ tags, className, isLoading }: Props) => { + const isMobile = useIsMobile(); + const visibleNum = isMobile ? 2 : 3; + + const metaSuitesPlaceholder = config.features.metasuites.isEnabled ? + : + null; + + if (tags.length === 0) { + return metaSuitesPlaceholder; + } + + const content = (() => { + if (tags.length > visibleNum) { + return ( + <> + { tags.slice(0, visibleNum).map((tag) => ) } + { metaSuitesPlaceholder } + + + + +{ tags.length - visibleNum } + + + + + + { tags.slice(visibleNum).map((tag) => ) } + + + + + + ); + } + + return ( + <> + { tags.map((tag) => ) } + { metaSuitesPlaceholder } + + ); + })(); + + return ( + + { content } + + ); +}; + +export default React.memo(chakra(EntityTags)); diff --git a/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_dark-color-mode_custom-name-tag-dark-mode-1.png b/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_dark-color-mode_custom-name-tag-dark-mode-1.png new file mode 100644 index 0000000000..932254450b Binary files /dev/null and b/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_dark-color-mode_custom-name-tag-dark-mode-1.png differ diff --git a/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_dark-color-mode_generic-tag-dark-mode-1.png b/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_dark-color-mode_generic-tag-dark-mode-1.png new file mode 100644 index 0000000000..2381600421 Binary files /dev/null and b/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_dark-color-mode_generic-tag-dark-mode-1.png differ diff --git a/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_dark-color-mode_protocol-tag-dark-mode-1.png b/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_dark-color-mode_protocol-tag-dark-mode-1.png new file mode 100644 index 0000000000..41836e3717 Binary files /dev/null and b/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_dark-color-mode_protocol-tag-dark-mode-1.png differ diff --git a/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_dark-color-mode_tag-with-link-and-long-name-dark-mode-1.png b/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_dark-color-mode_tag-with-link-and-long-name-dark-mode-1.png new file mode 100644 index 0000000000..dc42e698ef Binary files /dev/null and b/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_dark-color-mode_tag-with-link-and-long-name-dark-mode-1.png differ diff --git a/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_dark-color-mode_tag-with-tooltip-dark-mode-1.png b/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_dark-color-mode_tag-with-tooltip-dark-mode-1.png new file mode 100644 index 0000000000..f86bb76d63 Binary files /dev/null and b/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_dark-color-mode_tag-with-tooltip-dark-mode-1.png differ diff --git a/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_default_custom-name-tag-dark-mode-1.png b/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_default_custom-name-tag-dark-mode-1.png new file mode 100644 index 0000000000..2fa5802c36 Binary files /dev/null and b/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_default_custom-name-tag-dark-mode-1.png differ diff --git a/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_default_generic-tag-dark-mode-1.png b/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_default_generic-tag-dark-mode-1.png new file mode 100644 index 0000000000..9e12c057a6 Binary files /dev/null and b/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_default_generic-tag-dark-mode-1.png differ diff --git a/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_default_protocol-tag-dark-mode-1.png b/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_default_protocol-tag-dark-mode-1.png new file mode 100644 index 0000000000..c5de547a2d Binary files /dev/null and b/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_default_protocol-tag-dark-mode-1.png differ diff --git a/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_default_tag-with-link-and-long-name-dark-mode-1.png b/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_default_tag-with-link-and-long-name-dark-mode-1.png new file mode 100644 index 0000000000..ec1924e091 Binary files /dev/null and b/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_default_tag-with-link-and-long-name-dark-mode-1.png differ diff --git a/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_default_tag-with-tooltip-dark-mode-1.png b/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_default_tag-with-tooltip-dark-mode-1.png new file mode 100644 index 0000000000..244342f1c3 Binary files /dev/null and b/ui/shared/EntityTags/__screenshots__/EntityTag.pw.tsx_default_tag-with-tooltip-dark-mode-1.png differ diff --git a/ui/shared/EntityTags/formatUserTags.ts b/ui/shared/EntityTags/formatUserTags.ts new file mode 100644 index 0000000000..f644647ae0 --- /dev/null +++ b/ui/shared/EntityTags/formatUserTags.ts @@ -0,0 +1,9 @@ +import type { EntityTag } from './types'; +import type { UserTags } from 'types/api/addressParams'; + +export default function formatUserTags(data: UserTags | undefined): Array { + return [ + ...(data?.private_tags || []).map((tag) => ({ slug: tag.label, name: tag.display_name, tagType: 'private_tag' as const, ordinal: 1_000 })), + ...(data?.watchlist_names || []).map((tag) => ({ slug: tag.label, name: tag.display_name, tagType: 'watchlist' as const, ordinal: 1_000 })), + ]; +} diff --git a/ui/shared/EntityTags/sortEntityTags.ts b/ui/shared/EntityTags/sortEntityTags.ts new file mode 100644 index 0000000000..8227829f5d --- /dev/null +++ b/ui/shared/EntityTags/sortEntityTags.ts @@ -0,0 +1,13 @@ +import type { EntityTag } from './types'; + +export default function sortEntityTags(tagA: EntityTag, tagB: EntityTag): number { + if (tagA.ordinal < tagB.ordinal) { + return 1; + } + + if (tagA.ordinal > tagB.ordinal) { + return -1; + } + + return 0; +} diff --git a/ui/shared/EntityTags/types.ts b/ui/shared/EntityTags/types.ts new file mode 100644 index 0000000000..47a444f0ad --- /dev/null +++ b/ui/shared/EntityTags/types.ts @@ -0,0 +1,9 @@ +import type { AddressMetadataTagType } from 'types/api/addressMetadata'; +import type { AddressMetadataTagFormatted } from 'types/client/addressMetadata'; + +export type EntityTagType = AddressMetadataTagType | 'custom' | 'watchlist' | 'private_tag'; + +export interface EntityTag extends Pick { + tagType: EntityTagType; + meta?: AddressMetadataTagFormatted['meta']; +} diff --git a/ui/shared/InOutTag.tsx b/ui/shared/InOutTag.tsx new file mode 100644 index 0000000000..bf7be50a85 --- /dev/null +++ b/ui/shared/InOutTag.tsx @@ -0,0 +1,33 @@ +import { chakra } from '@chakra-ui/react'; +import React from 'react'; + +import Tag from 'ui/shared/chakra/Tag'; + +interface Props { + isIn: boolean; + isOut: boolean; + className?: string; + isLoading?: boolean; +} + +const InOutTag = ({ isIn, isOut, className, isLoading }: Props) => { + if (!isIn && !isOut) { + return null; + } + + const colorScheme = isOut ? 'orange' : 'green'; + + return ( + + { isOut ? 'OUT' : 'IN' } + + ); +}; + +export default React.memo(chakra(InOutTag)); diff --git a/ui/shared/LinkExternal.tsx b/ui/shared/LinkExternal.tsx index cae2427170..31aaa7593d 100644 --- a/ui/shared/LinkExternal.tsx +++ b/ui/shared/LinkExternal.tsx @@ -1,4 +1,4 @@ -import type { ChakraProps } from '@chakra-ui/react'; +import type { ChakraProps, LinkProps } from '@chakra-ui/react'; import { Link, chakra, Box, Skeleton, useColorModeValue } from '@chakra-ui/react'; import React from 'react'; @@ -10,9 +10,11 @@ interface Props { children: React.ReactNode; isLoading?: boolean; variant?: 'subtle'; + iconColor?: LinkProps['color']; + onClick?: LinkProps['onClick']; } -const LinkExternal = ({ href, children, className, isLoading, variant }: Props) => { +const LinkExternal = ({ href, children, className, isLoading, variant, iconColor, onClick }: Props) => { const subtleLinkBg = useColorModeValue('gray.100', 'gray.700'); const styleProps: ChakraProps = (() => { @@ -57,9 +59,9 @@ const LinkExternal = ({ href, children, className, isLoading, variant }: Props) } return ( - + { children } - + ); }; diff --git a/ui/shared/NetworkExplorers.tsx b/ui/shared/NetworkExplorers.tsx index 6671a07274..200341f87e 100644 --- a/ui/shared/NetworkExplorers.tsx +++ b/ui/shared/NetworkExplorers.tsx @@ -20,6 +20,7 @@ import config from 'configs/app'; import stripTrailingSlash from 'lib/stripTrailingSlash'; import IconSvg from 'ui/shared/IconSvg'; import LinkExternal from 'ui/shared/LinkExternal'; +import PopoverTriggerTooltip from 'ui/shared/PopoverTriggerTooltip'; interface Props { className?: string; @@ -55,26 +56,27 @@ const NetworkExplorers = ({ className, type, pathParam }: Props) => { return ( - + + + diff --git a/ui/shared/Noves/NovesFromTo.tsx b/ui/shared/Noves/NovesFromTo.tsx new file mode 100644 index 0000000000..db1ea47205 --- /dev/null +++ b/ui/shared/Noves/NovesFromTo.tsx @@ -0,0 +1,62 @@ +import { Box, Skeleton } from '@chakra-ui/react'; +import type { FC } from 'react'; +import React from 'react'; + +import type { NovesResponseData } from 'types/api/noves'; + +import type { NovesFlowViewItem } from 'ui/tx/assetFlows/utils/generateFlowViewData'; + +import Tag from '../chakra/Tag'; +import AddressEntity from '../entities/address/AddressEntity'; +import { getActionFromTo, getFromTo } from './utils'; + +interface Props { + isLoaded: boolean; + txData?: NovesResponseData; + currentAddress?: string; + item?: NovesFlowViewItem; +} + +const NovesFromTo: FC = ({ isLoaded, txData, currentAddress = '', item }) => { + const data = React.useMemo(() => { + if (txData) { + return getFromTo(txData, currentAddress); + } + if (item) { + return getActionFromTo(item); + } + + return { text: 'Sent to', address: '' }; + }, [ currentAddress, item, txData ]); + + const isSent = data.text.startsWith('Sent'); + + const address = { hash: data.address || '', name: data.name || '' }; + + return ( + + + + { data.text } + + + + + + ); +}; + +export default NovesFromTo; diff --git a/ui/shared/Noves/utils.test.ts b/ui/shared/Noves/utils.test.ts new file mode 100644 index 0000000000..52bdc97a9c --- /dev/null +++ b/ui/shared/Noves/utils.test.ts @@ -0,0 +1,49 @@ +import * as transactionMock from 'mocks/noves/transaction'; +import type { NovesFlowViewItem } from 'ui/tx/assetFlows/utils/generateFlowViewData'; + +import { getActionFromTo, getFromTo, getFromToValue } from './utils'; + +it('get data for FromTo component from transaction', async() => { + const result = getFromTo(transactionMock.transaction, transactionMock.transaction.accountAddress); + + expect(result).toEqual({ + text: 'Sent to', + address: '0xef6595A423c99f3f2821190A4d96fcE4DcD89a80', + }); +}); + +it('get what type of FromTo component will be', async() => { + const result = getFromToValue(transactionMock.transaction, transactionMock.transaction.accountAddress); + + expect(result).toEqual('sent'); +}); + +it('get data for FromTo component from flow item', async() => { + const item: NovesFlowViewItem = { + action: { + label: 'Sent', + amount: '3000', + flowDirection: 'toRight', + nft: undefined, + token: { + address: '0x1bfe4298796198f8664b18a98640cec7c89b5baa', + decimals: 18, + name: 'PQR-Test', + symbol: 'PQR', + }, + }, + rightActor: { + address: '0xdD15D2650387Fb6FEDE27ae7392C402a393F8A37', + name: null, + }, + accountAddress: '0xef6595a423c99f3f2821190a4d96fce4dcd89a80', + }; + + const result = getActionFromTo(item); + + expect(result).toEqual({ + text: 'Sent to', + address: '0xdD15D2650387Fb6FEDE27ae7392C402a393F8A37', + name: null, + }); +}); diff --git a/ui/shared/Noves/utils.ts b/ui/shared/Noves/utils.ts new file mode 100644 index 0000000000..9e0bf88677 --- /dev/null +++ b/ui/shared/Noves/utils.ts @@ -0,0 +1,89 @@ +import type { NovesResponseData, NovesSentReceived } from 'types/api/noves'; + +import type { NovesFlowViewItem } from 'ui/tx/assetFlows/utils/generateFlowViewData'; + +export interface FromToData { + text: string; + address: string; + name?: string | null; +} + +export const getFromTo = (txData: NovesResponseData, currentAddress: string): FromToData => { + const raw = txData.rawTransactionData; + const sent = txData.classificationData.sent; + let sentFound: Array = []; + if (sent && sent[0]) { + sentFound = sent + .filter((sent) => sent.from.address.toLocaleLowerCase() === currentAddress) + .filter((sent) => sent.to.address); + } + + const received = txData.classificationData.received; + let receivedFound: Array = []; + if (received && received[0]) { + receivedFound = received + .filter((received) => received.to.address?.toLocaleLowerCase() === currentAddress) + .filter((received) => received.from.address); + } + + if (sentFound[0] && receivedFound[0]) { + if (sentFound.length === receivedFound.length) { + if (raw.toAddress.toLocaleLowerCase() === currentAddress) { + return { text: 'Received from', address: raw.fromAddress }; + } + + if (raw.fromAddress.toLocaleLowerCase() === currentAddress) { + return { text: 'Sent to', address: raw.toAddress }; + } + } + if (sentFound.length > receivedFound.length) { + // already filtered if null + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return { text: 'Sent to', address: sentFound[0].to.address! } ; + } else { + return { text: 'Received from', address: receivedFound[0].from.address } ; + } + } + + if (sent && sentFound[0]) { + // already filtered if null + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return { text: 'Sent to', address: sentFound[0].to.address! } ; + } + + if (received && receivedFound[0]) { + return { text: 'Received from', address: receivedFound[0].from.address }; + } + + if (raw.toAddress && raw.toAddress.toLocaleLowerCase() === currentAddress) { + return { text: 'Received from', address: raw.fromAddress }; + } + + if (raw.fromAddress && raw.fromAddress.toLocaleLowerCase() === currentAddress) { + return { text: 'Sent to', address: raw.toAddress }; + } + + if (!raw.toAddress && raw.fromAddress) { + return { text: 'Received from', address: raw.fromAddress }; + } + + if (!raw.fromAddress && raw.toAddress) { + return { text: 'Sent to', address: raw.toAddress }; + } + + return { text: 'Sent to', address: currentAddress }; +}; + +export const getFromToValue = (txData: NovesResponseData, currentAddress: string) => { + const fromTo = getFromTo(txData, currentAddress); + + return fromTo.text.split(' ').shift()?.toLowerCase(); +}; + +export const getActionFromTo = (item: NovesFlowViewItem): FromToData => { + return { + text: item.action.flowDirection === 'toRight' ? 'Sent to' : 'Received from', + address: item.rightActor.address, + name: item.rightActor.name, + }; +}; diff --git a/ui/shared/Page/PageTitle.tsx b/ui/shared/Page/PageTitle.tsx index 793d839bbf..7cafd18f31 100644 --- a/ui/shared/Page/PageTitle.tsx +++ b/ui/shared/Page/PageTitle.tsx @@ -143,7 +143,7 @@ const PageTitle = ({ title, contentAfter, backLink, className, isLoading, afterT { /* { withTextAd && } */ } { secondRow && ( - + { secondRow } ) } diff --git a/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_default_default-view-mobile-1.png b/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_default_default-view-mobile-1.png index d633cbb80a..479736d81a 100644 Binary files a/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_default_default-view-mobile-1.png and b/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_default_default-view-mobile-1.png differ diff --git a/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_default_with-long-name-and-many-tags-mobile-1.png b/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_default_with-long-name-and-many-tags-mobile-1.png index 8d02d7da7c..6e08e3c204 100644 Binary files a/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_default_with-long-name-and-many-tags-mobile-1.png and b/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_default_with-long-name-and-many-tags-mobile-1.png differ diff --git a/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_default_with-text-ad-mobile-1.png b/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_default_with-text-ad-mobile-1.png index 9a03708d13..d55d170ab8 100644 Binary files a/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_default_with-text-ad-mobile-1.png and b/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_default_with-text-ad-mobile-1.png differ diff --git a/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_mobile_default-view-mobile-1.png b/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_mobile_default-view-mobile-1.png index efabd99eed..39ea00f5a9 100644 Binary files a/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_mobile_default-view-mobile-1.png and b/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_mobile_default-view-mobile-1.png differ diff --git a/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_mobile_with-long-name-and-many-tags-mobile-1.png b/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_mobile_with-long-name-and-many-tags-mobile-1.png index f2b21cc103..c1904cbeca 100644 Binary files a/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_mobile_with-long-name-and-many-tags-mobile-1.png and b/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_mobile_with-long-name-and-many-tags-mobile-1.png differ diff --git a/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_mobile_with-text-ad-mobile-1.png b/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_mobile_with-text-ad-mobile-1.png index 01d7d12f52..389d966a56 100644 Binary files a/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_mobile_with-text-ad-mobile-1.png and b/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_mobile_with-text-ad-mobile-1.png differ diff --git a/ui/shared/Page/specs/DefaultView.tsx b/ui/shared/Page/specs/DefaultView.tsx index 0c924f55e0..0c117d1411 100644 --- a/ui/shared/Page/specs/DefaultView.tsx +++ b/ui/shared/Page/specs/DefaultView.tsx @@ -5,7 +5,7 @@ import type { TokenInfo } from 'types/api/token'; import * as addressMock from 'mocks/address/address'; import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import * as TokenEntity from 'ui/shared/entities/token/TokenEntity'; -import EntityTags from 'ui/shared/EntityTags'; +import EntityTags from 'ui/shared/EntityTags/EntityTags'; import IconSvg from 'ui/shared/IconSvg'; import NetworkExplorers from 'ui/shared/NetworkExplorers'; @@ -34,8 +34,8 @@ const DefaultView = () => { <> diff --git a/ui/shared/Page/specs/LongNameAndManyTags.tsx b/ui/shared/Page/specs/LongNameAndManyTags.tsx index ae8e190cda..4c690737b9 100644 --- a/ui/shared/Page/specs/LongNameAndManyTags.tsx +++ b/ui/shared/Page/specs/LongNameAndManyTags.tsx @@ -5,7 +5,8 @@ import type { TokenInfo } from 'types/api/token'; import { publicTag, privateTag, watchlistName } from 'mocks/address/tag'; import * as TokenEntity from 'ui/shared/entities/token/TokenEntity'; -import EntityTags from 'ui/shared/EntityTags'; +import EntityTags from 'ui/shared/EntityTags/EntityTags'; +import formatUserTags from 'ui/shared/EntityTags/formatUserTags'; import IconSvg from 'ui/shared/IconSvg'; import NetworkExplorers from 'ui/shared/NetworkExplorers'; @@ -29,21 +30,19 @@ const LongNameAndManyTags = () => { <> } flexGrow={ 1 } /> + ); diff --git a/ui/shared/PopoverTriggerTooltip.tsx b/ui/shared/PopoverTriggerTooltip.tsx new file mode 100644 index 0000000000..b0ed7ab91e --- /dev/null +++ b/ui/shared/PopoverTriggerTooltip.tsx @@ -0,0 +1,30 @@ +import { Skeleton, Tooltip, chakra } from '@chakra-ui/react'; +import React from 'react'; + +import useIsMobile from 'lib/hooks/useIsMobile'; + +type Props = { + label: string; + isLoading?: boolean; + className?: string; + children: React.ReactNode; +} + +const PopoverTriggerTooltip = ({ label, isLoading, className, children }: Props, ref: React.ForwardedRef) => { + const isMobile = useIsMobile(); + return ( + // tooltip need to be wrapped in div for proper popover positioning + + + { children } + + + ); +}; + +export default chakra(React.forwardRef(PopoverTriggerTooltip)); diff --git a/ui/shared/RawDataSnippet.tsx b/ui/shared/RawDataSnippet.tsx index d7a6789bda..b18a6009f8 100644 --- a/ui/shared/RawDataSnippet.tsx +++ b/ui/shared/RawDataSnippet.tsx @@ -1,3 +1,4 @@ +import type { ChakraProps } from '@chakra-ui/react'; import { Box, Flex, chakra, useColorModeValue, Skeleton } from '@chakra-ui/react'; import React from 'react'; @@ -12,9 +13,10 @@ interface Props { textareaMaxHeight?: string; showCopy?: boolean; isLoading?: boolean; + contentProps?: ChakraProps; } -const RawDataSnippet = ({ data, className, title, rightSlot, beforeSlot, textareaMaxHeight, showCopy = true, isLoading }: Props) => { +const RawDataSnippet = ({ data, className, title, rightSlot, beforeSlot, textareaMaxHeight, showCopy = true, isLoading, contentProps }: Props) => { // see issue in theme/components/Textarea.ts const bgColor = useColorModeValue('#f5f5f6', '#1a1b1b'); return ( @@ -36,8 +38,10 @@ const RawDataSnippet = ({ data, className, title, rightSlot, beforeSlot, textare borderRadius="md" wordBreak="break-all" whiteSpace="pre-wrap" + overflowX="hidden" overflowY="auto" isLoaded={ !isLoading } + { ...contentProps } > { data } diff --git a/ui/shared/Tabs/AdaptiveTabsList.tsx b/ui/shared/Tabs/AdaptiveTabsList.tsx index 5f5ff81f1f..a84babf06d 100644 --- a/ui/shared/Tabs/AdaptiveTabsList.tsx +++ b/ui/shared/Tabs/AdaptiveTabsList.tsx @@ -1,5 +1,5 @@ import type { StyleProps, ThemingProps } from '@chakra-ui/react'; -import { Box, Tab, TabList, useColorModeValue } from '@chakra-ui/react'; +import { Box, Skeleton, Tab, TabList, useColorModeValue } from '@chakra-ui/react'; import React from 'react'; import { useScrollDirection } from 'lib/contexts/scrollDirection'; @@ -24,6 +24,7 @@ interface Props extends TabsProps { activeTabIndex: number; onItemClick: (index: number) => void; themeProps: ThemingProps<'Tabs'>; + isLoading?: boolean; } const AdaptiveTabsList = (props: Props) => { @@ -79,6 +80,10 @@ const AdaptiveTabsList = (props: Props) => { > { tabsList.map((tab, index) => { if (!tab.id) { + if (props.isLoading) { + return null; + } + return ( { }, }} > - { typeof tab.title === 'function' ? tab.title() : tab.title } - + + { typeof tab.title === 'function' ? tab.title() : tab.title } + + ); }) } diff --git a/ui/shared/Tabs/RoutedTabs.tsx b/ui/shared/Tabs/RoutedTabs.tsx index a57b68f222..cfc7036bd3 100644 --- a/ui/shared/Tabs/RoutedTabs.tsx +++ b/ui/shared/Tabs/RoutedTabs.tsx @@ -17,9 +17,10 @@ interface Props extends ThemingProps<'Tabs'> { stickyEnabled?: boolean; className?: string; onTabChange?: (index: number) => void; + isLoading?: boolean; } -const RoutedTabs = ({ tabs, tabListProps, rightSlot, rightSlotProps, stickyEnabled, className, onTabChange, ...themeProps }: Props) => { +const RoutedTabs = ({ tabs, tabListProps, rightSlot, rightSlotProps, stickyEnabled, className, onTabChange, isLoading, ...themeProps }: Props) => { const router = useRouter(); const tabIndex = useTabIndexFromQuery(tabs); const tabsRef = useRef(null); @@ -63,6 +64,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, rightSlotProps, stickyEnabl stickyEnabled={ stickyEnabled } onTabChange={ handleTabChange } defaultTabIndex={ tabIndex } + isLoading={ isLoading } { ...themeProps } /> ); diff --git a/ui/shared/Tabs/TabsWithScroll.tsx b/ui/shared/Tabs/TabsWithScroll.tsx index f09644b675..efbde5723d 100644 --- a/ui/shared/Tabs/TabsWithScroll.tsx +++ b/ui/shared/Tabs/TabsWithScroll.tsx @@ -25,6 +25,7 @@ export interface Props extends ThemingProps<'Tabs'> { stickyEnabled?: boolean; onTabChange?: (index: number) => void; defaultTabIndex?: number; + isLoading?: boolean; className?: string; } @@ -37,6 +38,7 @@ const TabsWithScroll = ({ stickyEnabled, onTabChange, defaultTabIndex, + isLoading, className, ...themeProps }: Props) => { @@ -50,8 +52,11 @@ const TabsWithScroll = ({ }, [ tabs ]); const handleTabChange = React.useCallback((index: number) => { + if (isLoading) { + return; + } onTabChange ? onTabChange(index) : setActiveTabIndex(index); - }, [ onTabChange ]); + }, [ isLoading, onTabChange ]); useEffect(() => { if (defaultTabIndex !== undefined) { @@ -89,10 +94,12 @@ const TabsWithScroll = ({ lazyBehavior={ lazyBehavior } > tab.id).join(':') } tabs={ tabs } tabListProps={ tabListProps } rightSlot={ rightSlot } @@ -101,6 +108,7 @@ const TabsWithScroll = ({ activeTabIndex={ activeTabIndex } onItemClick={ handleTabChange } themeProps={ themeProps } + isLoading={ isLoading } /> { tabsList.map((tab) => { tab.component }) } diff --git a/ui/shared/TextSeparator.tsx b/ui/shared/TextSeparator.tsx index bc386c0ddb..ed929fafdb 100644 --- a/ui/shared/TextSeparator.tsx +++ b/ui/shared/TextSeparator.tsx @@ -2,8 +2,8 @@ import { chakra } from '@chakra-ui/react'; import type { StyleProps } from '@chakra-ui/styled-system'; import React from 'react'; -const TextSeparator = (props: StyleProps) => { - return |; +const TextSeparator = ({ id, ...props }: StyleProps & { id?: string }) => { + return |; }; export default React.memo(TextSeparator); diff --git a/ui/shared/TokenTransfer/TokenTransferListItem.tsx b/ui/shared/TokenTransfer/TokenTransferListItem.tsx index 4d78789860..4707f9d917 100644 --- a/ui/shared/TokenTransfer/TokenTransferListItem.tsx +++ b/ui/shared/TokenTransfer/TokenTransferListItem.tsx @@ -35,7 +35,7 @@ const TokenTransferListItem = ({ isLoading, }: Props) => { const timeAgo = useTimeAgoIncrement(timestamp, enableTimeIncrement); - const { usd, valueStr } = 'value' in total ? getCurrencyValue({ + const { usd, valueStr } = 'value' in total && total.value !== null ? getCurrencyValue({ value: total.value, exchangeRate: token.exchange_rate, accuracy: 8, diff --git a/ui/shared/TokenTransfer/TokenTransferNft.tsx b/ui/shared/TokenTransfer/TokenTransferNft.tsx new file mode 100644 index 0000000000..551820f6fc --- /dev/null +++ b/ui/shared/TokenTransfer/TokenTransferNft.tsx @@ -0,0 +1,42 @@ +import { Box, chakra, Skeleton } from '@chakra-ui/react'; +import React from 'react'; + +import { route } from 'nextjs-routes'; + +import nftPlaceholder from 'icons/nft_shield.svg'; +import Icon from 'ui/shared/chakra/Icon'; +import HashStringShorten from 'ui/shared/HashStringShorten'; +import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; +import LinkInternal from 'ui/shared/LinkInternal'; + +interface Props { + hash: string; + id: string; + className?: string; + isDisabled?: boolean; + truncation?: 'dynamic' | 'constant'; + isLoading?: boolean; +} + +const TokenTransferNft = ({ hash, id, className, isDisabled, isLoading, truncation = 'dynamic' }: Props) => { + const Component = isDisabled || isLoading ? Box : LinkInternal; + + return ( + + + + { truncation === 'constant' ? : } + + + ); +}; + +export default React.memo(chakra(TokenTransferNft)); diff --git a/ui/shared/TokenTransfer/TokenTransferTable.tsx b/ui/shared/TokenTransfer/TokenTransferTable.tsx index b707ccbdc0..bcb006faf9 100644 --- a/ui/shared/TokenTransfer/TokenTransferTable.tsx +++ b/ui/shared/TokenTransfer/TokenTransferTable.tsx @@ -38,7 +38,7 @@ const TokenTransferTable = ({
{ showTxInfo && } - + { showTxInfo && } diff --git a/ui/shared/TokenTransfer/TokenTransferTableItem.tsx b/ui/shared/TokenTransfer/TokenTransferTableItem.tsx index b93a917250..2d4ad7d1d6 100644 --- a/ui/shared/TokenTransfer/TokenTransferTableItem.tsx +++ b/ui/shared/TokenTransfer/TokenTransferTableItem.tsx @@ -34,7 +34,7 @@ const TokenTransferTableItem = ({ isLoading, }: Props) => { const timeAgo = useTimeAgoIncrement(timestamp, enableTimeIncrement); - const { usd, valueStr } = 'value' in total ? getCurrencyValue({ + const { usd, valueStr } = 'value' in total && total.value !== null ? getCurrencyValue({ value: total.value, exchangeRate: token.exchange_rate, accuracy: 8, @@ -52,14 +52,14 @@ const TokenTransferTableItem = ({ ) } - { token.type === 'ERC-1155' && } + { (token.type === 'ERC-1155' || token.type === 'ERC-404') && } - { token.total_supply && } + { token.total_supply && token.type !== 'ERC-404' && } diff --git a/ui/token/TokenHolders/TokenHoldersTableItem.tsx b/ui/token/TokenHolders/TokenHoldersTableItem.tsx index 8bfc187e0a..8bbb815548 100644 --- a/ui/token/TokenHolders/TokenHoldersTableItem.tsx +++ b/ui/token/TokenHolders/TokenHoldersTableItem.tsx @@ -26,7 +26,7 @@ const TokenTransferTableItem = ({ holder, token, isLoading }: Props) => { fontWeight="700" /> - { token.type === 'ERC-1155' && 'token_id' in holder && ( + { (token.type === 'ERC-1155' || token.type === 'ERC-404') && 'token_id' in holder && ( - { token.total_supply && ( + { token.total_supply && token.type !== 'ERC-404' && ( - - { (tokenType === 'ERC-721' || tokenType === 'ERC-1155') && - + + { (NFT_TOKEN_TYPE_IDS.includes(tokenType)) && + } - { (tokenType === 'ERC-20' || tokenType === 'ERC-1155') && ( - ) } diff --git a/ui/token/TokenTransfer/TokenTransferTableItem.tsx b/ui/token/TokenTransfer/TokenTransferTableItem.tsx index 4b782466db..63b2ee005f 100644 --- a/ui/token/TokenTransfer/TokenTransferTableItem.tsx +++ b/ui/token/TokenTransfer/TokenTransferTableItem.tsx @@ -5,6 +5,7 @@ import type { TokenTransfer } from 'types/api/tokenTransfer'; import getCurrencyValue from 'lib/getCurrencyValue'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; +import { NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes'; import AddressFromTo from 'ui/shared/address/AddressFromTo'; import Tag from 'ui/shared/chakra/Tag'; import NftEntity from 'ui/shared/entities/nft/NftEntity'; @@ -24,7 +25,7 @@ const TokenTransferTableItem = ({ isLoading, }: Props) => { const timeAgo = useTimeAgoIncrement(timestamp, true); - const { usd, valueStr } = 'value' in total ? getCurrencyValue({ + const { usd, valueStr } = 'value' in total && total.value !== null ? getCurrencyValue({ value: total.value, exchangeRate: token.exchange_rate, accuracy: 8, @@ -69,7 +70,7 @@ const TokenTransferTableItem = ({ tokenHash={ token.address } /> - { (token.type === 'ERC-721' || token.type === 'ERC-1155') && ( + { (NFT_TOKEN_TYPE_IDS.includes(token.type)) && ( + + + + ); +}; + +export default React.memo(TxAssetFlowsTableItem); diff --git a/ui/tx/assetFlows/components/NovesActionSnippet.tsx b/ui/tx/assetFlows/components/NovesActionSnippet.tsx new file mode 100644 index 0000000000..c1acc6b0db --- /dev/null +++ b/ui/tx/assetFlows/components/NovesActionSnippet.tsx @@ -0,0 +1,116 @@ +import { Box, Hide, Popover, PopoverArrow, PopoverContent, PopoverTrigger, Show, Skeleton, Text, useColorModeValue } from '@chakra-ui/react'; +import type { FC } from 'react'; +import React from 'react'; + +import { HEX_REGEXP } from 'lib/regexp'; +import TokenEntity from 'ui/shared/entities/token/TokenEntity'; +import IconSvg from 'ui/shared/IconSvg'; + +import type { NovesFlowViewItem } from '../utils/generateFlowViewData'; +import NovesTokenTooltipContent from './NovesTokenTooltipContent'; + +interface Props { + item: NovesFlowViewItem; + isLoaded: boolean; +} + +const NovesActionSnippet: FC = ({ item, isLoaded }) => { + const popoverBg = useColorModeValue('gray.700', 'gray.300'); + + const token = React.useMemo(() => { + const action = item.action; + + const name = action.nft?.name || action.token?.name; + const symbol = action.nft?.symbol || action.token?.symbol; + + const token = { + name: name, + symbol: symbol?.toLowerCase() === name?.toLowerCase() ? undefined : symbol, + address: action.nft?.address || action.token?.address, + }; + + return token; + }, [ item.action ]); + + const validTokenAddress = token.address ? HEX_REGEXP.test(token.address) : false; + + return ( + + + + + { item.action.label } + + + { item.action.amount } + + + + + + + + + + + + { item.action.label } + + + { item.action.amount } + + + + + + + + + + + + + + ); +}; + +export default React.memo(NovesActionSnippet); diff --git a/ui/tx/assetFlows/components/NovesTokenTooltipContent.tsx b/ui/tx/assetFlows/components/NovesTokenTooltipContent.tsx new file mode 100644 index 0000000000..f2bebb068c --- /dev/null +++ b/ui/tx/assetFlows/components/NovesTokenTooltipContent.tsx @@ -0,0 +1,55 @@ +import { Box, Text, useColorModeValue } from '@chakra-ui/react'; +import type { FC } from 'react'; +import React from 'react'; + +import type { NovesNft, NovesToken } from 'types/api/noves'; + +import { HEX_REGEXP } from 'lib/regexp'; +import CopyToClipboard from 'ui/shared/CopyToClipboard'; + +interface Props { + amount?: string; + token: NovesToken | NovesNft | undefined; +} + +const NovesTokenTooltipContent: FC = ({ token, amount }) => { + const textColor = useColorModeValue('white', 'blackAlpha.900'); + + if (!token) { + return null; + } + + const showTokenName = token.symbol !== token.name; + const showTokenAddress = HEX_REGEXP.test(token.address); + + return ( + + + + { amount } + + + { token.symbol } + + + + { showTokenName && ( + + { token.name } + + ) } + + { showTokenAddress && ( + + + { token.address } + + + + ) } + + + ); +}; + +export default React.memo(NovesTokenTooltipContent); diff --git a/ui/tx/assetFlows/utils/createNovesSummaryObject.test.ts b/ui/tx/assetFlows/utils/createNovesSummaryObject.test.ts new file mode 100644 index 0000000000..35275d22bc --- /dev/null +++ b/ui/tx/assetFlows/utils/createNovesSummaryObject.test.ts @@ -0,0 +1,20 @@ +import * as transactionMock from 'mocks/noves/transaction'; + +import { createNovesSummaryObject } from './createNovesSummaryObject'; + +it('creates interpretation summary object', async() => { + const result = createNovesSummaryObject(transactionMock.transaction); + + expect(result).toEqual({ + summary_template: ' Called function \'stake\' on contract{0xef326CdAdA59D3A740A76bB5f4F88Fb2}', + summary_template_variables: { + '0xef326CdAdA59D3A740A76bB5f4F88Fb2': { + type: 'address', + value: { + hash: '0xef326CdAdA59D3A740A76bB5f4F88Fb2f1076164', + is_contract: true, + }, + }, + }, + }); +}); diff --git a/ui/tx/assetFlows/utils/createNovesSummaryObject.ts b/ui/tx/assetFlows/utils/createNovesSummaryObject.ts new file mode 100644 index 0000000000..81c2e956f0 --- /dev/null +++ b/ui/tx/assetFlows/utils/createNovesSummaryObject.ts @@ -0,0 +1,139 @@ +import type { NovesResponseData } from 'types/api/noves'; +import type { TxInterpretationSummary } from 'types/api/txInterpretation'; + +import { createAddressValues } from './getAddressValues'; +import type { NovesTokenInfo, TokensData } from './getTokensData'; +import { getTokensData } from './getTokensData'; + +export interface SummaryAddress { + hash: string; + name?: string | null; + is_contract?: boolean; +} + +export interface SummaryValues { + match: string; + value: NovesTokenInfo | SummaryAddress; + type: 'token' | 'address'; +} + +interface NovesSummary { + summary_template: string; + summary_template_variables: {[x: string]: unknown}; +} + +export const createNovesSummaryObject = (translateData: NovesResponseData) => { + + // Remove final dot and add space at the start to avoid matching issues + const description = translateData.classificationData.description; + const removedFinalDot = description.endsWith('.') ? description.slice(0, description.length - 1) : description; + let parsedDescription = ' ' + removedFinalDot + ' '; + const tokenData = getTokensData(translateData); + + const idsMatched = tokenData.idList.filter(id => parsedDescription.includes(`#${ id }`)); + const tokensMatchedByName = tokenData.nameList.filter(name => parsedDescription.toUpperCase().includes(` ${ name.toUpperCase() }`)); + let tokensMatchedBySymbol = tokenData.symbolList.filter(symbol => parsedDescription.toUpperCase().includes(` ${ symbol.toUpperCase() }`)); + + // Filter symbols if they're already matched by name + tokensMatchedBySymbol = tokensMatchedBySymbol.filter(symbol => !tokensMatchedByName.includes(tokenData.bySymbol[symbol]?.name || '')); + + const summaryValues: Array = []; + + if (idsMatched.length) { + parsedDescription = removeIds(tokensMatchedByName, tokensMatchedBySymbol, idsMatched, tokenData, parsedDescription); + } + + if (tokensMatchedByName.length) { + const values = createTokensSummaryValues(tokensMatchedByName, tokenData.byName); + summaryValues.push(...values); + } + + if (tokensMatchedBySymbol.length) { + const values = createTokensSummaryValues(tokensMatchedBySymbol, tokenData.bySymbol); + summaryValues.push(...values); + } + + const addressSummaryValues = createAddressValues(translateData, parsedDescription); + if (addressSummaryValues.length) { + summaryValues.push(...addressSummaryValues); + } + + return createSummaryTemplate(summaryValues, parsedDescription) as TxInterpretationSummary; +}; + +const removeIds = ( + tokensMatchedByName: Array, + tokensMatchedBySymbol: Array, + idsMatched: Array, + tokenData: TokensData, + parsedDescription: string, +) => { + // Remove ids from the description since we already have that info in the token object + let description = parsedDescription; + + tokensMatchedByName.forEach(name => { + const hasId = idsMatched.includes(tokenData.byName[name].id || ''); + if (hasId) { + description = description.replaceAll(`#${ tokenData.byName[name].id }`, ''); + } + }); + + tokensMatchedBySymbol.forEach(name => { + const hasId = idsMatched.includes(tokenData.bySymbol[name].id || ''); + if (hasId) { + description = description.replaceAll(`#${ tokenData.bySymbol[name].id }`, ''); + } + }); + + return description; +}; + +const createTokensSummaryValues = ( + matchedStrings: Array, + tokens: { + [x: string]: NovesTokenInfo; + }, +) => { + const summaryValues: Array = matchedStrings.map(match => ({ + match, + type: 'token', + value: tokens[match], + })); + + return summaryValues; +}; + +const createSummaryTemplate = (summaryValues: Array, parsedDescription: string) => { + let newDescription = parsedDescription; + + const result: NovesSummary = { + summary_template: newDescription, + summary_template_variables: {}, + }; + + if (!summaryValues[0]) { + return result; + } + + const createTemplate = (data: SummaryValues, index = 0) => { + newDescription = newDescription.replaceAll(new RegExp(` ${ data.match } `, 'gi'), `{${ data.match }}`); + + const variable = { + type: data.type, + value: data.value, + }; + + result.summary_template_variables[data.match] = variable; + + const nextValue = summaryValues[index + 1]; + if (nextValue) { + createTemplate(nextValue, index + 1); + } + }; + + createTemplate(summaryValues[0]); + + result.summary_template = newDescription; + + return result; +}; diff --git a/ui/tx/assetFlows/utils/generateFlowViewData.test.ts b/ui/tx/assetFlows/utils/generateFlowViewData.test.ts new file mode 100644 index 0000000000..0cbb2770dd --- /dev/null +++ b/ui/tx/assetFlows/utils/generateFlowViewData.test.ts @@ -0,0 +1,48 @@ +import * as transactionMock from 'mocks/noves/transaction'; + +import { generateFlowViewData } from './generateFlowViewData'; + +it('creates asset flows items', async() => { + const result = generateFlowViewData(transactionMock.transaction); + + expect(result).toEqual( + [ + { + action: { + label: 'Sent', + amount: '3000', + flowDirection: 'toRight', + token: { + address: '0x1bfe4298796198f8664b18a98640cec7c89b5baa', + decimals: 18, + name: 'PQR-Test', + symbol: 'PQR', + }, + }, + rightActor: { + address: '0xdD15D2650387Fb6FEDE27ae7392C402a393F8A37', + name: null, + }, + accountAddress: '0xef6595a423c99f3f2821190a4d96fce4dcd89a80', + }, + { + action: { + label: 'Paid Gas', + amount: '0.000395521502109448', + flowDirection: 'toRight', + token: { + address: 'ETH', + decimals: 18, + name: 'ETH', + symbol: 'ETH', + }, + }, + rightActor: { + address: '', + name: 'Validators', + }, + accountAddress: '0xef6595a423c99f3f2821190a4d96fce4dcd89a80', + }, + ], + ); +}); diff --git a/ui/tx/assetFlows/utils/generateFlowViewData.ts b/ui/tx/assetFlows/utils/generateFlowViewData.ts new file mode 100644 index 0000000000..9564c574d8 --- /dev/null +++ b/ui/tx/assetFlows/utils/generateFlowViewData.ts @@ -0,0 +1,76 @@ +import _ from 'lodash'; + +import type { NovesNft, NovesResponseData, NovesSentReceived, NovesToken } from 'types/api/noves'; + +export interface NovesAction { + label: string; + amount: string | undefined; + flowDirection: 'toLeft' | 'toRight'; + nft: NovesNft | undefined; + token: NovesToken | undefined; +} + +export interface NovesFlowViewItem { + action: NovesAction; + rightActor: { + address: string ; + name: string | null; + }; + accountAddress: string; +} + +export function generateFlowViewData(data: NovesResponseData): Array { + const perspectiveAddress = data.accountAddress.toLowerCase(); + + const sent = data.classificationData.sent || []; + const received = data.classificationData.received || []; + + const txItems = [ ...sent, ...received ]; + + const paidGasIndex = _.findIndex(txItems, (item) => item.action === 'paidGas'); + if (paidGasIndex >= 0) { + const element = txItems.splice(paidGasIndex, 1)[0]; + element.to.name = 'Validators'; + txItems.splice(txItems.length, 0, element); + } + + const flowViewData = txItems.map((item) => { + const action = { + label: item.actionFormatted || item.action, + amount: item.amount || undefined, + flowDirection: getFlowDirection(item, perspectiveAddress), + nft: item.nft || undefined, + token: item.token || undefined, + }; + + if (item.from.name && item.from.name.includes('(this wallet)')) { + item.from.name = item.from.name.split('(this wallet)')[0]; + } + + if (item.to.name && item.to.name.includes('(this wallet)')) { + item.to.name = item.to.name.split('(this wallet)')[0]; + } + + const rightActor = getRightActor(item, perspectiveAddress); + + return { action, rightActor, accountAddress: perspectiveAddress }; + }); + + return flowViewData; +} + +function getRightActor(item: NovesSentReceived, perspectiveAddress: string) { + if (!item.to.address || item.to.address.toLowerCase() !== perspectiveAddress) { + return { address: item.to.address || '', name: item.to.name }; + } + + return { address: item.from.address, name: item.from.name }; +} + +function getFlowDirection(item: NovesSentReceived, perspectiveAddress: string): 'toLeft' | 'toRight' { + if (item.from.address && item.from.address.toLowerCase() === perspectiveAddress) { + return 'toRight'; + } + + return 'toLeft'; +} diff --git a/ui/tx/assetFlows/utils/getAddressValues.test.ts b/ui/tx/assetFlows/utils/getAddressValues.test.ts new file mode 100644 index 0000000000..f911253cce --- /dev/null +++ b/ui/tx/assetFlows/utils/getAddressValues.test.ts @@ -0,0 +1,18 @@ +import * as transactionMock from 'mocks/noves/transaction'; + +import { createAddressValues } from './getAddressValues'; + +it('creates addresses summary values', async() => { + const result = createAddressValues(transactionMock.transaction, transactionMock.transaction.classificationData.description); + + expect(result).toEqual([ + { + match: '0xef326CdAdA59D3A740A76bB5f4F88Fb2', + type: 'address', + value: { + hash: '0xef326CdAdA59D3A740A76bB5f4F88Fb2f1076164', + is_contract: true, + }, + }, + ]); +}); diff --git a/ui/tx/assetFlows/utils/getAddressValues.ts b/ui/tx/assetFlows/utils/getAddressValues.ts new file mode 100644 index 0000000000..7e20b92843 --- /dev/null +++ b/ui/tx/assetFlows/utils/getAddressValues.ts @@ -0,0 +1,80 @@ +import type { NovesResponseData } from 'types/api/noves'; + +import type { SummaryAddress, SummaryValues } from './createNovesSummaryObject'; + +const ADDRESS_REGEXP = /(0x[\da-f]+\b)/gi; +const CONTRACT_REGEXP = /(contract 0x[\da-f]+\b)/gi; + +export const createAddressValues = (translateData: NovesResponseData, description: string) => { + const addressMatches = description.match(ADDRESS_REGEXP); + const contractMatches = description.match(CONTRACT_REGEXP); + + let descriptionAddresses: Array = addressMatches ? addressMatches : []; + let contractAddresses: Array = []; + + if (contractMatches?.length) { + contractAddresses = contractMatches.map(text => text.split(ADDRESS_REGEXP)[1]); + descriptionAddresses = addressMatches?.filter(address => !contractAddresses.includes(address)) || []; + } + + const addresses = extractAddresses(translateData); + + const descriptionSummaryValues = createAddressSummaryValues(descriptionAddresses, addresses); + const contractSummaryValues = createAddressSummaryValues(contractAddresses, addresses, true); + + const summaryValues = [ ...descriptionSummaryValues, ...contractSummaryValues ]; + + return summaryValues; +}; + +const createAddressSummaryValues = (descriptionAddresses: Array, addresses: Array, isContract = false) => { + const summaryValues: Array = descriptionAddresses.map(match => { + const address = addresses.find(address => address.hash.toUpperCase().startsWith(match.toUpperCase())); + + if (!address) { + return undefined; + } + + const value: SummaryValues = { + match: match, + type: 'address', + value: isContract ? { ...address, is_contract: true } : address, + }; + + return value; + }); + + return summaryValues.filter(value => value !== undefined) as Array; +}; + +function extractAddresses(data: NovesResponseData) { + const addressesSet: Set<{ hash: string | null; name?: string | null }> = new Set(); // Use a Set to store unique addresses + + addressesSet.add({ hash: data.rawTransactionData.fromAddress }); + addressesSet.add({ hash: data.rawTransactionData.toAddress }); + + if (data.classificationData.approved) { + addressesSet.add({ hash: data.classificationData.approved.spender }); + } + + if (data.classificationData.deployedContractAddress) { + addressesSet.add({ hash: data.classificationData.deployedContractAddress }); + } + + if (data.txTypeVersion === 2) { + data.classificationData.sent.forEach((transaction) => { + addressesSet.add({ hash: transaction.from.address, name: transaction.from.name }); + addressesSet.add({ hash: transaction.to.address, name: transaction.to.name }); + }); + + data.classificationData.received.forEach((transaction) => { + addressesSet.add({ hash: transaction.from.address, name: transaction.from.name }); + addressesSet.add({ hash: transaction.to.address, name: transaction.to.name }); + }); + } + + const addresses = Array.from(addressesSet) as Array<{hash: string; name?: string}>; // Convert Set to an array + + // Remove empty and null values + return addresses.filter(address => address.hash !== null && address.hash !== '' && address.hash !== undefined); +} diff --git a/ui/tx/assetFlows/utils/getTokensData.test.ts b/ui/tx/assetFlows/utils/getTokensData.test.ts new file mode 100644 index 0000000000..37124e000c --- /dev/null +++ b/ui/tx/assetFlows/utils/getTokensData.test.ts @@ -0,0 +1,9 @@ +import * as transactionMock from 'mocks/noves/transaction'; + +import { getTokensData } from './getTokensData'; + +it('creates a tokens data object', async() => { + const result = getTokensData(transactionMock.transaction); + + expect(result).toEqual(transactionMock.tokenData); +}); diff --git a/ui/tx/assetFlows/utils/getTokensData.ts b/ui/tx/assetFlows/utils/getTokensData.ts new file mode 100644 index 0000000000..2b14c9231c --- /dev/null +++ b/ui/tx/assetFlows/utils/getTokensData.ts @@ -0,0 +1,80 @@ +import _ from 'lodash'; + +import type { NovesResponseData } from 'types/api/noves'; +import type { TokenInfo } from 'types/api/token'; + +import { HEX_REGEXP } from 'lib/regexp'; + +export interface NovesTokenInfo extends Pick { + id?: string | undefined; +} + +export interface TokensData { + nameList: Array; + symbolList: Array; + idList: Array; + byName: { + [x: string]: NovesTokenInfo; + }; + bySymbol: { + [x: string]: NovesTokenInfo; + }; +} + +export function getTokensData(data: NovesResponseData): TokensData { + const sent = data.classificationData.sent || []; + const received = data.classificationData.received || []; + const approved = data.classificationData.approved ? [ data.classificationData.approved ] : []; + + const txItems = [ ...sent, ...received, ...approved ]; + + // Extract all tokens data + const tokens = txItems.map((item) => { + const name = item.nft?.name || item.token?.name || null; + const symbol = item.nft?.symbol || item.token?.symbol || null; + const address = item.nft?.address || item.token?.address || ''; + + const validTokenAddress = address ? HEX_REGEXP.test(address) : false; + + const token = { + name: name, + symbol: symbol?.toLowerCase() === name?.toLowerCase() ? null : symbol, + address: validTokenAddress ? address : '', + id: item.nft?.id || item.token?.id, + }; + + return token; + }); + + // Group tokens by property into arrays + const tokensGroupByname = _.groupBy(tokens, 'name'); + const tokensGroupBySymbol = _.groupBy(tokens, 'symbol'); + const tokensGroupById = _.groupBy(tokens, 'id'); + + // Map properties to an object and remove duplicates + const mappedNames = _.mapValues(tokensGroupByname, (i) => { + return i[0]; + }); + + const mappedSymbols = _.mapValues(tokensGroupBySymbol, (i) => { + return i[0]; + }); + + const mappedIds = _.mapValues(tokensGroupById, (i) => { + return i[0]; + }); + + const filters = [ 'undefined', 'null' ]; + // Array of keys to match in string + const nameList = _.keysIn(mappedNames).filter(i => !filters.includes(i)); + const symbolList = _.keysIn(mappedSymbols).filter(i => !filters.includes(i)); + const idList = _.keysIn(mappedIds).filter(i => !filters.includes(i)); + + return { + nameList, + symbolList, + idList, + byName: mappedNames, + bySymbol: mappedSymbols, + }; +} diff --git a/ui/tx/blobs/TxBlobListItem.tsx b/ui/tx/blobs/TxBlobListItem.tsx new file mode 100644 index 0000000000..e246d24b1a --- /dev/null +++ b/ui/tx/blobs/TxBlobListItem.tsx @@ -0,0 +1,40 @@ +import { Skeleton } from '@chakra-ui/react'; +import React from 'react'; + +import type { TxBlob } from 'types/api/blobs'; + +import BlobDataType from 'ui/shared/blob/BlobDataType'; +import BlobEntity from 'ui/shared/entities/blob/BlobEntity'; +import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; + +interface Props { + data: TxBlob; + isLoading?: boolean; +} + +const TxBlobListItem = ({ data, isLoading }: Props) => { + const size = data.blob_data ? data.blob_data.replace('0x', '').length / 2 : '-'; + + return ( + + Blob hash + + + + + Data type + + { data.blob_data ? : '-' } + + + Size, bytes + + + { size.toLocaleString() } + + + + ); +}; + +export default React.memo(TxBlobListItem); diff --git a/ui/tx/blobs/TxBlobsList.tsx b/ui/tx/blobs/TxBlobsList.tsx new file mode 100644 index 0000000000..29018039f6 --- /dev/null +++ b/ui/tx/blobs/TxBlobsList.tsx @@ -0,0 +1,22 @@ +import { Box } from '@chakra-ui/react'; +import React from 'react'; + +import type { TxBlob } from 'types/api/blobs'; + +import TxBlobListItem from './TxBlobListItem'; + +const TxBlobList = ({ data, isLoading }: { data: Array; isLoading?: boolean }) => { + return ( + + { data.map((item, index) => ( + + )) } + + ); +}; + +export default TxBlobList; diff --git a/ui/tx/blobs/TxBlobsTable.tsx b/ui/tx/blobs/TxBlobsTable.tsx new file mode 100644 index 0000000000..5dc0cd28af --- /dev/null +++ b/ui/tx/blobs/TxBlobsTable.tsx @@ -0,0 +1,36 @@ +import { Table, Tbody, Tr, Th } from '@chakra-ui/react'; +import React from 'react'; + +import type { TxBlob } from 'types/api/blobs'; + +import { default as Thead } from 'ui/shared/TheadSticky'; + +import TxBlobsTableItem from './TxBlobsTableItem'; + +interface Props { + data: Array; + top: number; + isLoading?: boolean; +} + +const TxInternalsTable = ({ data, top, isLoading }: Props) => { + + return ( +
AppContracts scoreTotalVerified
+ + : + + } + /> + + + + + + + + { securityReport?.overallInfo.totalContractsNumber ?? 0 } + + + + { securityReport?.overallInfo.verifiedNumber ?? 0 } + + + Data will be available soon + + +
+ + + + + + + + + + + + + + + + + + + { data.is_smart_contract_verified && } + + + { data.ens_info.names_count > 1 ? + ({ data.ens_info.names_count > 39 ? '40+' : `+${ data.ens_info.names_count - 1 }` }) : + { expiresText } } +
TokenToken Token IDTxn hashFrom/To - - + + { token.type } { getTokenTransferTypeText(type) } diff --git a/ui/shared/TokenTransfer/__screenshots__/TokenTransferList.pw.tsx_default_with-tx-info-1.png b/ui/shared/TokenTransfer/__screenshots__/TokenTransferList.pw.tsx_default_with-tx-info-1.png index 523dbce1e9..c031b777c1 100644 Binary files a/ui/shared/TokenTransfer/__screenshots__/TokenTransferList.pw.tsx_default_with-tx-info-1.png and b/ui/shared/TokenTransfer/__screenshots__/TokenTransferList.pw.tsx_default_with-tx-info-1.png differ diff --git a/ui/shared/TokenTransfer/__screenshots__/TokenTransferList.pw.tsx_default_without-tx-info-1.png b/ui/shared/TokenTransfer/__screenshots__/TokenTransferList.pw.tsx_default_without-tx-info-1.png index ccd697846d..3c2a6ee5a2 100644 Binary files a/ui/shared/TokenTransfer/__screenshots__/TokenTransferList.pw.tsx_default_without-tx-info-1.png and b/ui/shared/TokenTransfer/__screenshots__/TokenTransferList.pw.tsx_default_without-tx-info-1.png differ diff --git a/ui/shared/TokenTransfer/__screenshots__/TokenTransferTable.pw.tsx_default_with-tx-info-1.png b/ui/shared/TokenTransfer/__screenshots__/TokenTransferTable.pw.tsx_default_with-tx-info-1.png index 4bbe99bcfb..725d066269 100644 Binary files a/ui/shared/TokenTransfer/__screenshots__/TokenTransferTable.pw.tsx_default_with-tx-info-1.png and b/ui/shared/TokenTransfer/__screenshots__/TokenTransferTable.pw.tsx_default_with-tx-info-1.png differ diff --git a/ui/shared/TokenTransfer/__screenshots__/TokenTransferTable.pw.tsx_default_without-tx-info-1.png b/ui/shared/TokenTransfer/__screenshots__/TokenTransferTable.pw.tsx_default_without-tx-info-1.png index 51fc3312d5..f55a41ca6d 100644 Binary files a/ui/shared/TokenTransfer/__screenshots__/TokenTransferTable.pw.tsx_default_without-tx-info-1.png and b/ui/shared/TokenTransfer/__screenshots__/TokenTransferTable.pw.tsx_default_without-tx-info-1.png differ diff --git a/ui/shared/TruncatedTextTooltip.tsx b/ui/shared/TruncatedTextTooltip.tsx index 1f24937d7d..a929f1b63a 100644 --- a/ui/shared/TruncatedTextTooltip.tsx +++ b/ui/shared/TruncatedTextTooltip.tsx @@ -1,3 +1,4 @@ +import type { PlacementWithLogical } from '@chakra-ui/react'; import { Tooltip } from '@chakra-ui/react'; import debounce from 'lodash/debounce'; import React from 'react'; @@ -8,9 +9,10 @@ import { BODY_TYPEFACE } from 'theme/foundations/typography'; interface Props { children: React.ReactNode; label: string; + placement?: PlacementWithLogical; } -const TruncatedTextTooltip = ({ children, label }: Props) => { +const TruncatedTextTooltip = ({ children, label, placement }: Props) => { const childRef = React.useRef(null); const [ isTruncated, setTruncated ] = React.useState(false); @@ -60,7 +62,7 @@ const TruncatedTextTooltip = ({ children, label }: Props) => { ); if (isTruncated) { - return { modifiedChildren }; + return { modifiedChildren }; } return modifiedChildren; diff --git a/ui/shared/TruncatedValue.tsx b/ui/shared/TruncatedValue.tsx index 626039c5c7..f953477b08 100644 --- a/ui/shared/TruncatedValue.tsx +++ b/ui/shared/TruncatedValue.tsx @@ -1,3 +1,4 @@ +import type { PlacementWithLogical } from '@chakra-ui/react'; import { Skeleton, chakra } from '@chakra-ui/react'; import React from 'react'; @@ -7,11 +8,12 @@ interface Props { className?: string; isLoading?: boolean; value: string; + tooltipPlacement?: PlacementWithLogical; } -const TruncatedValue = ({ className, isLoading, value }: Props) => { +const TruncatedValue = ({ className, isLoading, value, tooltipPlacement }: Props) => { return ( - + { + let label; + let icon; + let colorScheme; + + switch (status) { + case 'ok': + label = 'Success'; + icon = successIcon; + colorScheme = 'green'; + break; + case 'error': + label = 'Failed'; + icon = errorIcon; + colorScheme = 'red'; + break; + case null: + label = 'Pending'; + icon = pendingIcon; + // FIXME: it's not gray on mockups + // need to implement new color scheme or redefine colors here + colorScheme = 'gray'; + break; + } + + return ( + + + + { label } + + + ); +}; + +export default TxStatus; diff --git a/ui/shared/UserAvatar.tsx b/ui/shared/UserAvatar.tsx index 818eaceda7..3a75d56105 100644 --- a/ui/shared/UserAvatar.tsx +++ b/ui/shared/UserAvatar.tsx @@ -8,9 +8,10 @@ import IconSvg from 'ui/shared/IconSvg'; interface Props { size: number; + fallbackIconSize?: number; } -const UserAvatar = ({ size }: Props) => { +const UserAvatar = ({ size, fallbackIconSize = 20 }: Props) => { const appProps = useAppContext(); const hasAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN, appProps.cookies)); const [ isImageLoadError, setImageLoadError ] = React.useState(false); @@ -34,7 +35,7 @@ const UserAvatar = ({ size }: Props) => { boxSize={ `${ size }px` } borderRadius="full" overflow="hidden" - fallback={ isImageLoadError || !data?.avatar ? : undefined } + fallback={ isImageLoadError || !data?.avatar ? : undefined } onError={ handleImageLoadError } /> ); diff --git a/ui/shared/Web3ModalProvider.tsx b/ui/shared/Web3ModalProvider.tsx index fc2f8553b7..6dd8be6149 100644 --- a/ui/shared/Web3ModalProvider.tsx +++ b/ui/shared/Web3ModalProvider.tsx @@ -1,58 +1,38 @@ import { useColorMode } from '@chakra-ui/react'; -import { jsonRpcProvider } from '@wagmi/core/providers/jsonRpc'; -import { createWeb3Modal, useWeb3ModalTheme, defaultWagmiConfig } from '@web3modal/wagmi/react'; +import { createWeb3Modal, useWeb3ModalTheme } from '@web3modal/wagmi/react'; import React from 'react'; -import { configureChains, WagmiConfig } from 'wagmi'; +import { WagmiProvider } from 'wagmi'; import config from 'configs/app'; -import currentChain from 'lib/web3/currentChain'; +import wagmiConfig from 'lib/web3/wagmiConfig'; import colors from 'theme/foundations/colors'; import { BODY_TYPEFACE } from 'theme/foundations/typography'; import zIndices from 'theme/foundations/zIndices'; const feature = config.features.blockchainInteraction; -const getConfig = () => { +const init = () => { try { - if (!feature.isEnabled) { - throw new Error(); + if (!wagmiConfig || !feature.isEnabled) { + return; } - const { chains } = configureChains( - [ currentChain ], - [ - jsonRpcProvider({ - rpc: () => ({ - http: config.chain.rpcUrl || '', - }), - }), - ], - ); - - const wagmiConfig = defaultWagmiConfig({ - chains, - projectId: feature.walletConnect.projectId, - }); - createWeb3Modal({ wagmiConfig, projectId: feature.walletConnect.projectId, - chains, themeVariables: { '--w3m-font-family': `${ BODY_TYPEFACE }, sans-serif`, '--w3m-accent': colors.blue[600], '--w3m-border-radius-master': '2px', '--w3m-z-index': zIndices.modal, }, + featuredWalletIds: [], + allowUnsupportedChain: true, }); - - return { wagmiConfig }; - } catch (error) { - return { }; - } + } catch (error) {} }; -const { wagmiConfig } = getConfig(); +init(); interface Props { children: React.ReactNode; @@ -77,9 +57,9 @@ const Provider = ({ children, fallback }: Props) => { } return ( - + { children } - + ); }; diff --git a/ui/shared/ad/AdBanner.tsx b/ui/shared/ad/AdBanner.tsx index 2ad19fda35..424e9c9ae9 100644 --- a/ui/shared/ad/AdBanner.tsx +++ b/ui/shared/ad/AdBanner.tsx @@ -7,24 +7,29 @@ import * as cookies from 'lib/cookies'; import AdbutlerBanner from './AdbutlerBanner'; import CoinzillaBanner from './CoinzillaBanner'; +import GetitBanner from './GetitBanner'; import HypeBanner from './HypeBanner'; //import SliseBanner from './SliseBanner'; const feature = config.features.adsBanner; const AdBanner = ({ className, isLoading }: { className?: string; isLoading?: boolean }) => { + const provider = useAppContext().adBannerProvider; + const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED, useAppContext().cookies); - if (!feature.isEnabled || hasAdblockCookie) { + if (!feature.isEnabled || hasAdblockCookie || !provider) { return null; } const content = (() => { - switch (feature.provider) { + switch (provider) { case 'adbutler': return ; case 'coinzilla': return ; + case 'getit': + return ; case 'hype': return ; // case 'slise': @@ -37,7 +42,7 @@ const AdBanner = ({ className, isLoading }: { className?: string; isLoading?: bo className={ className } isLoaded={ !isLoading } borderRadius="none" - maxW={ feature.provider === 'adbutler' ? feature.adButler.config.desktop.width : '728px' } + maxW={ ('adButler' in feature && feature.adButler) ? feature.adButler.config.desktop.width : '728px' } w="100%" > { content } diff --git a/ui/shared/ad/AdbutlerBanner.tsx b/ui/shared/ad/AdbutlerBanner.tsx index b6f5f6e6bd..aa6db55ea4 100644 --- a/ui/shared/ad/AdbutlerBanner.tsx +++ b/ui/shared/ad/AdbutlerBanner.tsx @@ -13,8 +13,9 @@ const feature = config.features.adsBanner; const AdbutlerBanner = ({ className }: { className?: string }) => { const router = useRouter(); const isMobile = useIsMobile(); + React.useEffect(() => { - if (!feature.isEnabled || feature.provider !== 'adbutler') { + if (!('adButler' in feature)) { return; } diff --git a/ui/shared/ad/GetitBanner.tsx b/ui/shared/ad/GetitBanner.tsx new file mode 100644 index 0000000000..3f4537e376 --- /dev/null +++ b/ui/shared/ad/GetitBanner.tsx @@ -0,0 +1,29 @@ +import { Flex, chakra } from '@chakra-ui/react'; +import dynamic from 'next/dynamic'; +import React from 'react'; + +import useIsMobile from 'lib/hooks/useIsMobile'; +import useAccount from 'lib/web3/useAccount'; + +const GetitAdPlugin = dynamic(() => import('getit-sdk').then(module => module.GetitAdPlugin), { ssr: false }); + +const GETIT_API_KEY = 'ZmGXVvwYUAW4yXL8RzWQHNKmpSyQmt3TDXsXUxqFqXPdoaiSSFyca3BOyunDcWdyOwTkX3UVVQel28qbjoOoWPxYVpPdNzbUNkAHyFyJX7Lk9TVcPDZKTQmwHlSMzO3a'; + +const GetitBanner = ({ className }: { className?: string }) => { + const isMobile = Boolean(useIsMobile()); + const { address } = useAccount(); + + return ( + + + + ); +}; + +export default chakra(GetitBanner); diff --git a/ui/shared/ad/HypeBanner.tsx b/ui/shared/ad/HypeBanner.tsx index 7934ddc867..adcdeae993 100644 --- a/ui/shared/ad/HypeBanner.tsx +++ b/ui/shared/ad/HypeBanner.tsx @@ -2,15 +2,22 @@ import { Flex, chakra } from '@chakra-ui/react'; import { Banner, setWalletAddresses } from '@hypelab/sdk-react'; import Script from 'next/script'; import React from 'react'; -import { useAccount } from 'wagmi'; -import Web3ModalProvider from '../Web3ModalProvider'; +import useAccount from 'lib/web3/useAccount'; + import { hypeInit } from './hypeBannerScript'; const DESKTOP_BANNER_SLUG = 'b1559fc3e7'; const MOBILE_BANNER_SLUG = '668ed80a9e'; -const HypeBannerContent = ({ className }: { className?: string }) => { +const HypeBanner = ({ className }: { className?: string }) => { + const { address } = useAccount(); + + React.useEffect(() => { + if (address) { + setWalletAddresses([ address ]); + } + }, [ address ]); return ( <> @@ -28,28 +35,4 @@ const HypeBannerContent = ({ className }: { className?: string }) => { ); }; -const HypeBannerWithWalletAddress = ({ className }: { className?: string }) => { - const { address } = useAccount(); - React.useEffect(() => { - if (address) { - setWalletAddresses([ address ]); - } - }, [ address ]); - - return ; -}; - -const HypeBanner = ({ className }: { className?: string }) => { - - const fallback = React.useCallback(() => { - return ; - }, [ className ]); - - return ( - - - - ); -}; - export default chakra(HypeBanner); diff --git a/ui/shared/ad/adbutlerScript.ts b/ui/shared/ad/adbutlerScript.ts index 6228a231bf..81b057e679 100644 --- a/ui/shared/ad/adbutlerScript.ts +++ b/ui/shared/ad/adbutlerScript.ts @@ -8,7 +8,7 @@ export const connectAdbutler = `if (!window.AdButler){(function(){var s = docume export const placeAd = (() => { const feature = config.features.adsBanner; - if (!feature.isEnabled || feature.provider !== 'adbutler') { + if (!('adButler' in feature)) { return; } diff --git a/ui/shared/address/AddressFromTo.tsx b/ui/shared/address/AddressFromTo.tsx index 5024c657da..0a5d0efbd3 100644 --- a/ui/shared/address/AddressFromTo.tsx +++ b/ui/shared/address/AddressFromTo.tsx @@ -80,7 +80,7 @@ const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading const iconSize = 20; return ( - + { + const component = await mount( + + + , + ); + await expect(component).toHaveScreenshot(); +}); + +test('raw data', async({ mount }) => { + const component = await mount( + + + , + ); + await expect(component).toHaveScreenshot(); +}); + +test('text data', async({ mount }) => { + const component = await mount( + + + , + ); + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/shared/blob/BlobDataType.tsx b/ui/shared/blob/BlobDataType.tsx new file mode 100644 index 0000000000..7da87113f6 --- /dev/null +++ b/ui/shared/blob/BlobDataType.tsx @@ -0,0 +1,56 @@ +import { Flex, Skeleton, useColorModeValue } from '@chakra-ui/react'; +import React from 'react'; + +import * as blobUtils from 'lib/blob'; +import type { IconName } from 'ui/shared/IconSvg'; +import IconSvg from 'ui/shared/IconSvg'; + +interface Props { + data: string; + isLoading?: boolean; +} + +const TYPES: Record = { + image: { iconName: 'blobs/image', label: 'Image' }, + text: { iconName: 'blobs/text', label: 'Text' }, + raw: { iconName: 'blobs/raw', label: 'Raw' }, +}; + +const BlobDataType = ({ data, isLoading }: Props) => { + const iconColor = useColorModeValue('gray.500', 'gray.400'); + + const guessedType = React.useMemo(() => { + if (isLoading) { + return; + } + return blobUtils.guessDataType(data); + }, [ data, isLoading ]); + + const { iconName, label } = (() => { + if (guessedType?.mime?.startsWith('image/')) { + return TYPES.image; + } + + if ( + guessedType?.mime?.startsWith('text/') || + [ + 'application/json', + 'application/xml', + 'application/javascript', + ].includes(guessedType?.mime || '') + ) { + return TYPES.text; + } + + return TYPES.raw; + })(); + + return ( + + + { label } + + ); +}; + +export default React.memo(BlobDataType); diff --git a/ui/shared/blob/__screenshots__/BlobDataType.pw.tsx_default_image-data-1.png b/ui/shared/blob/__screenshots__/BlobDataType.pw.tsx_default_image-data-1.png new file mode 100644 index 0000000000..cd00441544 Binary files /dev/null and b/ui/shared/blob/__screenshots__/BlobDataType.pw.tsx_default_image-data-1.png differ diff --git a/ui/shared/blob/__screenshots__/BlobDataType.pw.tsx_default_raw-data-1.png b/ui/shared/blob/__screenshots__/BlobDataType.pw.tsx_default_raw-data-1.png new file mode 100644 index 0000000000..9f2de76f10 Binary files /dev/null and b/ui/shared/blob/__screenshots__/BlobDataType.pw.tsx_default_raw-data-1.png differ diff --git a/ui/shared/blob/__screenshots__/BlobDataType.pw.tsx_default_text-data-1.png b/ui/shared/blob/__screenshots__/BlobDataType.pw.tsx_default_text-data-1.png new file mode 100644 index 0000000000..fef618ba13 Binary files /dev/null and b/ui/shared/blob/__screenshots__/BlobDataType.pw.tsx_default_text-data-1.png differ diff --git a/ui/shared/chakra/Icon.tsx b/ui/shared/chakra/Icon.tsx new file mode 100644 index 0000000000..0e270a5d6a --- /dev/null +++ b/ui/shared/chakra/Icon.tsx @@ -0,0 +1,18 @@ +import { Skeleton, Icon as ChakraIcon } from '@chakra-ui/react'; +import type { IconProps, As } from '@chakra-ui/react'; +import React from 'react'; + +interface Props extends IconProps { + isLoading?: boolean; + as: As; +} + +const Icon = ({ isLoading, ...props }: Props, ref: React.LegacyRef) => { + return ( + + + + ); +}; + +export default React.memo(React.forwardRef(Icon)); diff --git a/ui/shared/chakra/Tag.tsx b/ui/shared/chakra/Tag.tsx index 08d2d6f130..f8e0cb762f 100644 --- a/ui/shared/chakra/Tag.tsx +++ b/ui/shared/chakra/Tag.tsx @@ -4,7 +4,7 @@ import React from 'react'; import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip'; -interface Props extends TagProps { +export interface Props extends TagProps { isLoading?: boolean; } diff --git a/ui/shared/chart/ChartWidget.tsx b/ui/shared/chart/ChartWidget.tsx index 86cc601221..4ff84d65cf 100644 --- a/ui/shared/chart/ChartWidget.tsx +++ b/ui/shared/chart/ChartWidget.tsx @@ -35,11 +35,12 @@ export type Props = { isLoading: boolean; className?: string; isError: boolean; + emptyText?: string; } const DOWNLOAD_IMAGE_SCALE = 5; -const ChartWidget = ({ items, title, description, isLoading, className, isError, units }: Props) => { +const ChartWidget = ({ items, title, description, isLoading, className, isError, units, emptyText }: Props) => { const ref = useRef(null); const [ isFullscreen, setIsFullscreen ] = useState(false); const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true); @@ -134,7 +135,7 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError, if (!hasItems) { return (
- No data + { emptyText || 'No data' }
); } diff --git a/ui/shared/chart/types.tsx b/ui/shared/chart/types.tsx index 7bb6b45e41..5810fe2fb5 100644 --- a/ui/shared/chart/types.tsx +++ b/ui/shared/chart/types.tsx @@ -1,3 +1,9 @@ +export interface TimeChartItemRaw { + date: Date; + dateLabel?: string; + value: number | string | null; +} + export interface TimeChartItem { date: Date; dateLabel?: string; diff --git a/ui/shared/chart/utils/sorts.tsx b/ui/shared/chart/utils/sorts.tsx index 521825398b..da290c0ce2 100644 --- a/ui/shared/chart/utils/sorts.tsx +++ b/ui/shared/chart/utils/sorts.tsx @@ -1,5 +1,5 @@ import type { TimeChartItem } from '../types'; -export const sortByDateDesc = (a: TimeChartItem, b: TimeChartItem) => { +export const sortByDateDesc = (a: Pick, b: Pick) => { return a.date.getTime() - b.date.getTime(); }; diff --git a/ui/shared/entities/address/AddressEntity.pw.tsx b/ui/shared/entities/address/AddressEntity.pw.tsx index 91ffb6d093..e62685c8c2 100644 --- a/ui/shared/entities/address/AddressEntity.pw.tsx +++ b/ui/shared/entities/address/AddressEntity.pw.tsx @@ -97,6 +97,18 @@ test('with ENS', async({ mount }) => { await expect(component).toHaveScreenshot(); }); +test('with name tag', async({ mount }) => { + const component = await mount( + + + , + ); + + await expect(component).toHaveScreenshot(); +}); + test('external link', async({ mount }) => { const component = await mount( diff --git a/ui/shared/entities/address/AddressEntity.tsx b/ui/shared/entities/address/AddressEntity.tsx index 94ff1ef7ea..a5b2bd95d9 100644 --- a/ui/shared/entities/address/AddressEntity.tsx +++ b/ui/shared/entities/address/AddressEntity.tsx @@ -1,5 +1,5 @@ import type { As } from '@chakra-ui/react'; -import { Box, Flex, Skeleton, Tooltip, chakra, VStack, useColorModeValue } from '@chakra-ui/react'; +import { Box, Flex, Skeleton, Tooltip, chakra, VStack } from '@chakra-ui/react'; import _omit from 'lodash/omit'; import React from 'react'; @@ -28,8 +28,9 @@ const Link = chakra((props: LinkProps) => { ); }); -type IconProps = Pick & { +type IconProps = Omit & Pick & { asProp?: As; + name?: EntityBase.IconBaseProps['name']; }; const Icon = (props: IconProps) => { @@ -99,19 +100,21 @@ const Icon = (props: IconProps) => { type ContentProps = Omit & Pick; const Content = chakra((props: ContentProps) => { - if (props.address.name || props.address.ens_domain_name) { - const text = props.address.ens_domain_name || props.address.name; + const nameTag = props.address.metadata?.tags.find(tag => tag.tagType === 'name')?.name; + const nameText = nameTag || props.address.ens_domain_name || props.address.name; + + if (nameText) { const label = ( - { text } + { nameText } { props.address.hash } ); return ( - + - { text } + { nameText } ); @@ -139,7 +142,7 @@ const Copy = (props: CopyProps) => { const Container = EntityBase.Container; export interface EntityProps extends EntityBase.EntityBaseProps { - address: Pick; + address: Pick; isSafeAddress?: boolean; } @@ -148,35 +151,18 @@ const AddressEntry = (props: EntityProps) => { const partsProps = _omit(props, [ 'className', 'onClick' ]); const context = useAddressHighlightContext(); - const highlightedBgColor = useColorModeValue('blue.50', 'blue.900'); - const highlightedBorderColor = useColorModeValue('blue.200', 'blue.600'); return ( - + diff --git a/ui/shared/entities/address/AddressIdenticon.tsx b/ui/shared/entities/address/AddressIdenticon.tsx index bdefbf66d2..a08fa5a4a9 100644 --- a/ui/shared/entities/address/AddressIdenticon.tsx +++ b/ui/shared/entities/address/AddressIdenticon.tsx @@ -3,6 +3,7 @@ import dynamic from 'next/dynamic'; import React from 'react'; import config from 'configs/app'; +import * as cookies from 'lib/cookies'; import IdenticonGithub from 'ui/shared/IdenticonGithub'; interface IconProps { @@ -12,7 +13,8 @@ interface IconProps { const Icon = dynamic( async() => { - switch (config.UI.views.address.identiconType) { + const type = cookies.get(cookies.NAMES.ADDRESS_IDENTICON_TYPE) || config.UI.views.address.identiconType; + switch (type) { case 'github': { // eslint-disable-next-line react/display-name return (props: IconProps) => ; diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_with-name-tag-1.png b/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_with-name-tag-1.png new file mode 100644 index 0000000000..b3b745e458 Binary files /dev/null and b/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_with-name-tag-1.png differ diff --git a/ui/shared/entities/base/components.tsx b/ui/shared/entities/base/components.tsx index 5f977ddcc3..ef2447f001 100644 --- a/ui/shared/entities/base/components.tsx +++ b/ui/shared/entities/base/components.tsx @@ -18,7 +18,9 @@ export type Truncation = 'constant' | 'constant_long' | 'dynamic' | 'tail' | 'no export interface EntityBaseProps { className?: string; href?: string; + iconName?: IconName; iconSize?: IconSize; + iconColor?: IconProps['color']; isExternal?: boolean; isLoading?: boolean; noCopy?: boolean; diff --git a/ui/shared/entities/blob/BlobEntity.tsx b/ui/shared/entities/blob/BlobEntity.tsx new file mode 100644 index 0000000000..13953eb86f --- /dev/null +++ b/ui/shared/entities/blob/BlobEntity.tsx @@ -0,0 +1,90 @@ +import { chakra } from '@chakra-ui/react'; +import _omit from 'lodash/omit'; +import React from 'react'; + +import { route } from 'nextjs-routes'; + +import * as EntityBase from 'ui/shared/entities/base/components'; + +type LinkProps = EntityBase.LinkBaseProps & Pick; + +const Link = chakra((props: LinkProps) => { + const defaultHref = route({ pathname: '/blobs/[hash]', query: { hash: props.hash } }); + + return ( + + { props.children } + + ); +}); + +type IconProps = Omit & { + name?: EntityBase.IconBaseProps['name']; +}; + +const Icon = (props: IconProps) => { + return ( + + ); +}; + +type ContentProps = Omit & Pick; + +const Content = chakra((props: ContentProps) => { + return ( + + ); +}); + +type CopyProps = Omit & Pick; + +const Copy = (props: CopyProps) => { + return ( + + ); +}; + +const Container = EntityBase.Container; + +export interface EntityProps extends EntityBase.EntityBaseProps { + hash: string; + text?: string; +} + +const BlobEntity = (props: EntityProps) => { + const linkProps = _omit(props, [ 'className' ]); + const partsProps = _omit(props, [ 'className', 'onClick' ]); + + return ( + + + + + + + + ); +}; + +export default React.memo(chakra(BlobEntity)); + +export { + Container, + Link, + Icon, + Content, + Copy, +}; diff --git a/ui/shared/entities/token/TokenEntity.tsx b/ui/shared/entities/token/TokenEntity.tsx index 99f6abb514..d7cd9f7500 100644 --- a/ui/shared/entities/token/TokenEntity.tsx +++ b/ui/shared/entities/token/TokenEntity.tsx @@ -57,6 +57,7 @@ const Icon = (props: IconProps) => { src={ props.token.icon_url ?? undefined } alt={ `${ props.token.name || 'token' } logo` } fallback={ } + fallbackStrategy={ props.token.icon_url ? 'onError' : 'beforeLoadOrError' } /> ); }; @@ -66,7 +67,7 @@ type ContentProps = Omit & Pick { const nameString = [ !props.onlySymbol && (props.token.name ?? 'Unnamed token'), - props.onlySymbol && (props.token.symbol ?? ''), + props.onlySymbol && (props.token.symbol ?? props.token.name ?? 'Unnamed token'), props.token.symbol && props.jointSymbol && !props.onlySymbol && `(${ props.token.symbol })`, ].filter(Boolean).join(' '); diff --git a/ui/shared/entities/tx/TxEntity.tsx b/ui/shared/entities/tx/TxEntity.tsx index 796d305ded..4a9b727cae 100644 --- a/ui/shared/entities/tx/TxEntity.tsx +++ b/ui/shared/entities/tx/TxEntity.tsx @@ -71,7 +71,7 @@ const TxEntity = (props: EntityProps) => { return ( - + diff --git a/ui/shared/filters/FilterButton.tsx b/ui/shared/filters/FilterButton.tsx index a61246587c..8868c27f1d 100644 --- a/ui/shared/filters/FilterButton.tsx +++ b/ui/shared/filters/FilterButton.tsx @@ -35,6 +35,7 @@ const FilterButton = ({ isActive, isLoading, appliedFiltersNum, onClick, as }: P px={ 1.5 } flexShrink={ 0 } as={ as } + pointerEvents="all" > { FilterIcon } Filter diff --git a/ui/shared/gas/GasInfoTooltipRow.tsx b/ui/shared/gas/GasInfoTooltipRow.tsx index 4df03189ac..3aae5220fb 100644 --- a/ui/shared/gas/GasInfoTooltipRow.tsx +++ b/ui/shared/gas/GasInfoTooltipRow.tsx @@ -16,7 +16,7 @@ const GasInfoTooltipRow = ({ name, info }: Props) => { <> { name } - { info && info.time && ( + { info && typeof info.time === 'number' && info.time > 0 && ( { space }{ (info.time / 1000).toLocaleString(undefined, { maximumFractionDigits: 1 }) }s diff --git a/ui/shared/layout/LayoutApp.tsx b/ui/shared/layout/LayoutApp.tsx index 7f02e34df8..3ac8e463d8 100644 --- a/ui/shared/layout/LayoutApp.tsx +++ b/ui/shared/layout/LayoutApp.tsx @@ -3,30 +3,33 @@ import React from 'react'; import type { Props } from './types'; import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary'; -import HeaderDesktop from 'ui/snippets/header/HeaderDesktop'; import HeaderMobile from 'ui/snippets/header/HeaderMobile'; import * as Layout from './components'; const LayoutDefault = ({ children }: Props) => { return ( - + - - + + - - + { children } - ); }; diff --git a/ui/shared/layout/LayoutHome.tsx b/ui/shared/layout/LayoutHome.tsx index 6274afeeea..b6786f2339 100644 --- a/ui/shared/layout/LayoutHome.tsx +++ b/ui/shared/layout/LayoutHome.tsx @@ -12,7 +12,7 @@ const LayoutHome = ({ children }: Props) => { return ( - + { +const Container = ({ children, className }: Props) => { return ( - + { children } ); }; -export default React.memo(Container); +export default React.memo(chakra(Container)); diff --git a/ui/shared/layout/components/Content.tsx b/ui/shared/layout/components/Content.tsx index 4f6ddf23c1..e17fcd8397 100644 --- a/ui/shared/layout/components/Content.tsx +++ b/ui/shared/layout/components/Content.tsx @@ -8,7 +8,7 @@ interface Props { const Content = ({ children, className }: Props) => { return ( - + { children } ); diff --git a/ui/shared/layout/components/MainArea.tsx b/ui/shared/layout/components/MainArea.tsx index 804577425f..0c92777246 100644 --- a/ui/shared/layout/components/MainArea.tsx +++ b/ui/shared/layout/components/MainArea.tsx @@ -1,16 +1,17 @@ -import { Flex } from '@chakra-ui/react'; +import { Flex, chakra } from '@chakra-ui/react'; import React from 'react'; interface Props { children: React.ReactNode; + className?: string; } -const MainArea = ({ children }: Props) => { +const MainArea = ({ children, className }: Props) => { return ( - + { children } ); }; -export default React.memo(MainArea); +export default React.memo(chakra(MainArea)); diff --git a/ui/shared/logs/LogDecodedInputDataHeader.tsx b/ui/shared/logs/LogDecodedInputDataHeader.tsx index 653382b18b..ec1680cce9 100644 --- a/ui/shared/logs/LogDecodedInputDataHeader.tsx +++ b/ui/shared/logs/LogDecodedInputDataHeader.tsx @@ -1,13 +1,15 @@ import { Divider, Flex, Skeleton, VStack } from '@chakra-ui/react'; import React from 'react'; +import Tag from 'ui/shared/chakra/Tag'; + interface Props { methodId: string; methodCall: string; isLoading?: boolean; } -const Item = ({ label, text, isLoading }: { label: string; text: string; isLoading?: boolean}) => { +const Item = ({ label, children, isLoading }: { label: string; children: React.ReactNode; isLoading?: boolean}) => { return ( { label }
- { text } + { children }
); }; @@ -32,8 +34,12 @@ const LogDecodedInputDataHeader = ({ methodId, methodCall, isLoading }: Props) = fontSize="sm" lineHeight={ 5 } > - - + + { methodId } + + + { methodCall } + ); }; diff --git a/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_dark-color-mode_with-indexed-fields-mobile-dark-mode-1.png b/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_dark-color-mode_with-indexed-fields-mobile-dark-mode-1.png index 98428228b9..5dff66795a 100644 Binary files a/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_dark-color-mode_with-indexed-fields-mobile-dark-mode-1.png and b/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_dark-color-mode_with-indexed-fields-mobile-dark-mode-1.png differ diff --git a/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_default_with-indexed-fields-mobile-dark-mode-1.png b/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_default_with-indexed-fields-mobile-dark-mode-1.png index 0e81521fd8..3c02866fc0 100644 Binary files a/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_default_with-indexed-fields-mobile-dark-mode-1.png and b/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_default_with-indexed-fields-mobile-dark-mode-1.png differ diff --git a/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_default_without-indexed-fields-mobile-1.png b/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_default_without-indexed-fields-mobile-1.png index 5b5efd2b32..5b743f9c47 100644 Binary files a/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_default_without-indexed-fields-mobile-1.png and b/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_default_without-indexed-fields-mobile-1.png differ diff --git a/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_mobile_with-indexed-fields-mobile-dark-mode-1.png b/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_mobile_with-indexed-fields-mobile-dark-mode-1.png index 173ef0fc4a..a406c70799 100644 Binary files a/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_mobile_with-indexed-fields-mobile-dark-mode-1.png and b/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_mobile_with-indexed-fields-mobile-dark-mode-1.png differ diff --git a/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_mobile_without-indexed-fields-mobile-1.png b/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_mobile_without-indexed-fields-mobile-1.png index 7bde892cd2..6f9b592c45 100644 Binary files a/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_mobile_without-indexed-fields-mobile-1.png and b/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_mobile_without-indexed-fields-mobile-1.png differ diff --git a/ui/shared/logs/__screenshots__/LogItem.pw.tsx_dark-color-mode_with-decoded-input-data-mobile-dark-mode-1.png b/ui/shared/logs/__screenshots__/LogItem.pw.tsx_dark-color-mode_with-decoded-input-data-mobile-dark-mode-1.png index 3902449e9a..29e17c619e 100644 Binary files a/ui/shared/logs/__screenshots__/LogItem.pw.tsx_dark-color-mode_with-decoded-input-data-mobile-dark-mode-1.png and b/ui/shared/logs/__screenshots__/LogItem.pw.tsx_dark-color-mode_with-decoded-input-data-mobile-dark-mode-1.png differ diff --git a/ui/shared/logs/__screenshots__/LogItem.pw.tsx_default_with-decoded-input-data-mobile-dark-mode-1.png b/ui/shared/logs/__screenshots__/LogItem.pw.tsx_default_with-decoded-input-data-mobile-dark-mode-1.png index 3063b71f5d..0e1e563737 100644 Binary files a/ui/shared/logs/__screenshots__/LogItem.pw.tsx_default_with-decoded-input-data-mobile-dark-mode-1.png and b/ui/shared/logs/__screenshots__/LogItem.pw.tsx_default_with-decoded-input-data-mobile-dark-mode-1.png differ diff --git a/ui/shared/logs/__screenshots__/LogItem.pw.tsx_mobile_with-decoded-input-data-mobile-dark-mode-1.png b/ui/shared/logs/__screenshots__/LogItem.pw.tsx_mobile_with-decoded-input-data-mobile-dark-mode-1.png index 3834fa7b4f..7ae98dec50 100644 Binary files a/ui/shared/logs/__screenshots__/LogItem.pw.tsx_mobile_with-decoded-input-data-mobile-dark-mode-1.png and b/ui/shared/logs/__screenshots__/LogItem.pw.tsx_mobile_with-decoded-input-data-mobile-dark-mode-1.png differ diff --git a/ui/shared/monaco/CodeEditor.tsx b/ui/shared/monaco/CodeEditor.tsx index 31182c8a9a..7a39734c95 100644 --- a/ui/shared/monaco/CodeEditor.tsx +++ b/ui/shared/monaco/CodeEditor.tsx @@ -19,6 +19,7 @@ import CodeEditorSideBar, { CONTAINER_WIDTH as SIDE_BAR_WIDTH } from './CodeEdit import CodeEditorTabs from './CodeEditorTabs'; import addExternalLibraryWarningDecoration from './utils/addExternalLibraryWarningDecoration'; import addFileImportDecorations from './utils/addFileImportDecorations'; +import addMainContractCodeDecoration from './utils/addMainContractCodeDecoration'; import getFullPathOfImportedFile from './utils/getFullPathOfImportedFile'; import * as themes from './utils/themes'; import useThemeColors from './utils/useThemeColors'; @@ -42,9 +43,10 @@ interface Props { libraries?: Array; language?: string; mainFile?: string; + contractName?: string; } -const CodeEditor = ({ data, remappings, libraries, language, mainFile }: Props) => { +const CodeEditor = ({ data, remappings, libraries, language, mainFile, contractName }: Props) => { const [ instance, setInstance ] = React.useState(); const [ editor, setEditor ] = React.useState(); const [ index, setIndex ] = React.useState(0); @@ -82,9 +84,10 @@ const CodeEditor = ({ data, remappings, libraries, language, mainFile }: Props) if (language === 'solidity') { loadedModels.concat(newModels) - .forEach((models) => { - addFileImportDecorations(models); - libraries?.length && addExternalLibraryWarningDecoration(models, libraries); + .forEach((model) => { + contractName && mainFile === model.uri.path && addMainContractCodeDecoration(model, contractName, editor); + addFileImportDecorations(model); + libraries?.length && addExternalLibraryWarningDecoration(model, libraries); }); } @@ -192,6 +195,13 @@ const CodeEditor = ({ data, remappings, libraries, language, mainFile }: Props) '.monaco-editor .overflow-guard': { 'border-bottom-left-radius': borderRadius, }, + '.monaco-editor .core-guide': { + zIndex: 1, + }, + // '.monaco-editor .currentFindMatch': // TODO: find a better way to style this + '.monaco-editor .findMatch': { + backgroundColor: themeColors['custom.findMatchHighlightBackground'], + }, '.highlight': { backgroundColor: themeColors['custom.findMatchHighlightBackground'], }, @@ -206,6 +216,18 @@ const CodeEditor = ({ data, remappings, libraries, language, mainFile }: Props) '.risk-warning': { backgroundColor: themeColors['custom.riskWarning.background'], }, + '.main-contract-header': { + backgroundColor: themeColors['custom.mainContract.header'], + }, + '.main-contract-body': { + backgroundColor: themeColors['custom.mainContract.body'], + }, + '.main-contract-glyph': { + zIndex: 1, + background: 'url(/static/contract_star.png) no-repeat center center', + backgroundSize: '12px', + cursor: 'pointer', + }, }), [ editorWidth, themeColors, borderRadius ]); const renderErrorScreen = React.useCallback(() => { diff --git a/ui/shared/monaco/icons/file.svg b/ui/shared/monaco/icons/file.svg new file mode 100644 index 0000000000..38b4a10fc4 --- /dev/null +++ b/ui/shared/monaco/icons/file.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/shared/monaco/icons/folder-open.svg b/ui/shared/monaco/icons/folder-open.svg new file mode 100644 index 0000000000..02989af8d2 --- /dev/null +++ b/ui/shared/monaco/icons/folder-open.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/shared/monaco/icons/folder.svg b/ui/shared/monaco/icons/folder.svg new file mode 100644 index 0000000000..70e98efa46 --- /dev/null +++ b/ui/shared/monaco/icons/folder.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/shared/monaco/icons/solidity.svg b/ui/shared/monaco/icons/solidity.svg new file mode 100644 index 0000000000..f1c51a4c69 --- /dev/null +++ b/ui/shared/monaco/icons/solidity.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/ui/shared/monaco/icons/vyper.svg b/ui/shared/monaco/icons/vyper.svg new file mode 100644 index 0000000000..cd7b34ad9a --- /dev/null +++ b/ui/shared/monaco/icons/vyper.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/ui/shared/monaco/utils/addExternalLibraryWarningDecoration.ts b/ui/shared/monaco/utils/addExternalLibraryWarningDecoration.ts index 81a490fa5f..9d77966303 100644 --- a/ui/shared/monaco/utils/addExternalLibraryWarningDecoration.ts +++ b/ui/shared/monaco/utils/addExternalLibraryWarningDecoration.ts @@ -2,6 +2,8 @@ import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import type { SmartContractExternalLibrary } from 'types/api/contract'; +import sortByEndLineNumberAsc from './sortByEndLineNumberAsc'; + export default function addExternalLibraryWarningDecoration(model: monaco.editor.ITextModel, libraries: Array) { const options: monaco.editor.IModelDecorationOptions = { isWholeLine: true, @@ -81,15 +83,3 @@ const getLibraryName = (model: monaco.editor.ITextModel) => (library: SmartContr return libraryName; }; - -const sortByEndLineNumberAsc = (a: monaco.editor.FindMatch, b: monaco.editor.FindMatch) => { - if (a.range.endLineNumber < b.range.endLineNumber) { - return -1; - } - - if (a.range.endLineNumber > b.range.endLineNumber) { - return 1; - } - - return 0; -}; diff --git a/ui/shared/monaco/utils/addMainContractCodeDecoration.ts b/ui/shared/monaco/utils/addMainContractCodeDecoration.ts new file mode 100644 index 0000000000..1c2e3c5e0c --- /dev/null +++ b/ui/shared/monaco/utils/addMainContractCodeDecoration.ts @@ -0,0 +1,66 @@ +import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; + +import sortByEndLineNumberAsc from './sortByEndLineNumberAsc'; + +export default function addMainContractCodeDecoration(model: monaco.editor.ITextModel, contractName: string, editor: monaco.editor.IStandaloneCodeEditor) { + const options: monaco.editor.IModelDecorationOptions = { + isWholeLine: true, + }; + + const contractBlockMatches = model.findMatches(`^contract\\s`, false, true, false, null, true); + + if (contractBlockMatches.length < 2) { + return; + } + + const [ firstLineMatch ] = model.findMatches(`(^contract ${ contractName })( is .+)?\\s?\\{`, false, true, false, null, true); + + if (!firstLineMatch) { + return; + } + + const firstLineDecoration: monaco.editor.IModelDeltaDecoration = { + range: { + startColumn: 1, + endColumn: 10, // doesn't really matter since isWholeLine is true + startLineNumber: firstLineMatch.range.startLineNumber, + endLineNumber: firstLineMatch.range.startLineNumber, + }, + options: { + ...options, + className: '.main-contract-header', + marginClassName: '.main-contract-header', + glyphMarginClassName: '.main-contract-glyph', + glyphMarginHoverMessage: [ + { value: 'Main contract' }, + ], + }, + }; + + const lastLineRange: monaco.IRange = { + startLineNumber: firstLineMatch.range.startLineNumber, + startColumn: 1, + endColumn: 10, + endLineNumber: model.getLineCount(), + }; + const [ lastLineMatch ] = model + .findMatches(`^\\}`, lastLineRange, true, false, null, true) + .sort(sortByEndLineNumberAsc); + + const restDecoration: monaco.editor.IModelDeltaDecoration = { + range: { + startLineNumber: firstLineMatch.range.startLineNumber + 1, + endLineNumber: lastLineMatch.range.startLineNumber, + startColumn: 1, + endColumn: 10, // doesn't really matter since isWholeLine is true + }, + options: { + ...options, + className: '.main-contract-body', + marginClassName: '.main-contract-body', + }, + }; + + editor.updateOptions({ glyphMargin: true }); + model.deltaDecorations([], [ firstLineDecoration, restDecoration ]); +} diff --git a/ui/shared/monaco/utils/sortByEndLineNumberAsc.ts b/ui/shared/monaco/utils/sortByEndLineNumberAsc.ts new file mode 100644 index 0000000000..08fc4536e6 --- /dev/null +++ b/ui/shared/monaco/utils/sortByEndLineNumberAsc.ts @@ -0,0 +1,13 @@ +import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; + +export default function sortByEndLineNumberAsc(a: monaco.editor.FindMatch, b: monaco.editor.FindMatch) { + if (a.range.endLineNumber < b.range.endLineNumber) { + return -1; + } + + if (a.range.endLineNumber > b.range.endLineNumber) { + return 1; + } + + return 0; +} diff --git a/ui/shared/monaco/utils/themes.ts b/ui/shared/monaco/utils/themes.ts index 238504c4eb..14a861a3df 100644 --- a/ui/shared/monaco/utils/themes.ts +++ b/ui/shared/monaco/utils/themes.ts @@ -37,6 +37,8 @@ export const light = { 'custom.fileLink.hoverForeground': '#4299E1', // blue.400 'custom.riskWarning.primaryBackground': '#FEEBCB', // orange.100 'custom.riskWarning.background': '#FFFAF0', // orange.50 + 'custom.mainContract.header': 'rgba(233, 216, 253, 1)', // purple.100 + 'custom.mainContract.body': 'rgba(250, 245, 255, 1)', // purple.50 } as const, }; @@ -79,5 +81,7 @@ export const dark = { 'custom.fileLink.hoverForeground': '#4299E1', // blue.400 'custom.riskWarning.primaryBackground': 'rgba(246, 173, 85, 0.3)', // orange.300 'custom.riskWarning.background': 'rgba(246, 173, 85, 0.1)', // orange.300 + 'custom.mainContract.header': 'rgba(183, 148, 244, 0.3)', // purple.300 + 'custom.mainContract.body': 'rgba(214, 188, 250, 0.1)', // purple.200 } as const, }; diff --git a/ui/shared/nft/NftMedia.pw.tsx b/ui/shared/nft/NftMedia.pw.tsx index 71d09de045..4b91def420 100644 --- a/ui/shared/nft/NftMedia.pw.tsx +++ b/ui/shared/nft/NftMedia.pw.tsx @@ -10,7 +10,54 @@ test.describe('no url', () => { test('preview +@dark-mode', async({ mount }) => { const component = await mount( - + + , + ); + + await expect(component).toHaveScreenshot(); + }); + + test('with fallback', async({ mount, page }) => { + const IMAGE_URL = 'https://localhost:3000/my-image.jpg'; + + await page.route(IMAGE_URL, (route) => { + return route.fulfill({ + status: 200, + path: './playwright/mocks/image_long.jpg', + }); + }); + + const component = await mount( + + + , + ); + + await expect(component).toHaveScreenshot(); + }); + + test('non-media url and fallback', async({ mount, page }) => { + const ANIMATION_URL = 'https://localhost:3000/my-animation.m3u8'; + const ANIMATION_MEDIA_TYPE_API_URL = `/node-api/media-type?url=${ encodeURIComponent(ANIMATION_URL) }`; + const IMAGE_URL = 'https://localhost:3000/my-image.jpg'; + + await page.route(ANIMATION_MEDIA_TYPE_API_URL, (route) => { + return route.fulfill({ + status: 200, + body: JSON.stringify({ type: undefined }), + }); + }); + + await page.route(IMAGE_URL, (route) => { + return route.fulfill({ + status: 200, + path: './playwright/mocks/image_long.jpg', + }); + }); + + const component = await mount( + + , ); @@ -35,7 +82,7 @@ test.describe('image', () => { test('preview +@dark-mode', async({ mount }) => { const component = await mount( - + , ); @@ -55,7 +102,7 @@ test('image preview hover', async({ mount, page }) => { const component = await mount( - + , ); @@ -75,7 +122,7 @@ test('image fullscreen +@dark-mode +@mobile', async({ mount, page }) => { }); const component = await mount( - + , ); @@ -107,7 +154,7 @@ test.describe('page', () => { test('preview +@dark-mode', async({ mount }) => { const component = await mount( - + , ); diff --git a/ui/shared/nft/NftMedia.tsx b/ui/shared/nft/NftMedia.tsx index e01569c314..51dd03b6e5 100644 --- a/ui/shared/nft/NftMedia.tsx +++ b/ui/shared/nft/NftMedia.tsx @@ -9,29 +9,31 @@ import NftImage from './NftImage'; import NftImageFullscreen from './NftImageFullscreen'; import NftVideo from './NftVideo'; import NftVideoFullscreen from './NftVideoFullscreen'; -import useNftMediaType from './useNftMediaType'; +import useNftMediaInfo from './useNftMediaInfo'; import { mediaStyleProps } from './utils'; interface Props { - url: string | null; + imageUrl: string | null; + animationUrl: string | null; className?: string; isLoading?: boolean; withFullscreen?: boolean; } -const NftMedia = ({ url, className, isLoading, withFullscreen }: Props) => { +const NftMedia = ({ imageUrl, animationUrl, className, isLoading, withFullscreen }: Props) => { const [ isMediaLoading, setIsMediaLoading ] = React.useState(true); const [ isLoadingError, setIsLoadingError ] = React.useState(false); const { ref, inView } = useInView({ triggerOnce: true }); - const type = useNftMediaType(url, !isLoading && inView); + const mediaInfo = useNftMediaInfo({ imageUrl, animationUrl, isEnabled: !isLoading && inView }); React.useEffect(() => { - if (!isLoading) { - setIsMediaLoading(Boolean(url)); + if (!isLoading && !mediaInfo) { + setIsMediaLoading(false); + setIsLoadingError(true); } - }, [ isLoading, url ]); + }, [ isLoading, mediaInfo ]); const handleMediaLoaded = React.useCallback(() => { setIsMediaLoading(false); @@ -45,11 +47,21 @@ const NftMedia = ({ url, className, isLoading, withFullscreen }: Props) => { const { isOpen, onOpen, onClose } = useDisclosure(); const content = (() => { - if (!url || isLoadingError) { + if (isLoading) { + return null; + } + + if (!mediaInfo || isLoadingError) { const styleProps = withFullscreen ? {} : mediaStyleProps; return ; } + const { type, url } = mediaInfo; + + if (!url) { + return null; + } + const props = { src: url, onLoad: handleMediaLoaded, @@ -70,7 +82,13 @@ const NftMedia = ({ url, className, isLoading, withFullscreen }: Props) => { })(); const modal = (() => { - if (!url || !withFullscreen) { + if (!mediaInfo || !withFullscreen || isLoading) { + return null; + } + + const { type, url } = mediaInfo; + + if (!url) { return null; } diff --git a/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_dark-color-mode_image-dark-mode-1.png b/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_dark-color-mode_image-dark-mode-1.png new file mode 100644 index 0000000000..2f38d9f7ae Binary files /dev/null and b/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_dark-color-mode_image-dark-mode-1.png differ diff --git a/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_dark-color-mode_no-url-dark-mode-1.png b/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_dark-color-mode_no-url-dark-mode-1.png new file mode 100644 index 0000000000..abe6596ebb Binary files /dev/null and b/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_dark-color-mode_no-url-dark-mode-1.png differ diff --git a/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_default_image-dark-mode-1.png b/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_default_image-dark-mode-1.png new file mode 100644 index 0000000000..2a4baa8e3a Binary files /dev/null and b/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_default_image-dark-mode-1.png differ diff --git a/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_default_no-url-dark-mode-1.png b/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_default_no-url-dark-mode-1.png new file mode 100644 index 0000000000..3fd8aaa96f Binary files /dev/null and b/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_default_no-url-dark-mode-1.png differ diff --git a/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_default_no-url-non-media-url-and-fallback-1.png b/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_default_no-url-non-media-url-and-fallback-1.png new file mode 100644 index 0000000000..4273139faf Binary files /dev/null and b/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_default_no-url-non-media-url-and-fallback-1.png differ diff --git a/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_default_no-url-with-fallback-1.png b/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_default_no-url-with-fallback-1.png new file mode 100644 index 0000000000..4273139faf Binary files /dev/null and b/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_default_no-url-with-fallback-1.png differ diff --git a/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_default_page-1.png b/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_default_page-1.png new file mode 100644 index 0000000000..b1b46ce520 Binary files /dev/null and b/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_default_page-1.png differ diff --git a/ui/shared/nft/useNftMediaInfo.tsx b/ui/shared/nft/useNftMediaInfo.tsx new file mode 100644 index 0000000000..7c36f828e9 --- /dev/null +++ b/ui/shared/nft/useNftMediaInfo.tsx @@ -0,0 +1,98 @@ +import { useQuery } from '@tanstack/react-query'; +import React from 'react'; + +import type { StaticRoute } from 'nextjs-routes'; +import { route } from 'nextjs-routes'; + +import type { ResourceError } from 'lib/api/resources'; +import useFetch from 'lib/hooks/useFetch'; + +import type { MediaType } from './utils'; +import { getPreliminaryMediaType } from './utils'; + +interface Params { + imageUrl: string | null; + animationUrl: string | null; + isEnabled: boolean; +} + +interface ReturnType { + type: MediaType | undefined; + url: string | null; +} + +export default function useNftMediaInfo({ imageUrl, animationUrl, isEnabled }: Params): ReturnType | null { + + const primaryQuery = useNftMediaTypeQuery(animationUrl, isEnabled); + const secondaryQuery = useNftMediaTypeQuery(imageUrl, !primaryQuery.isPending && !primaryQuery.data); + + return React.useMemo(() => { + if (primaryQuery.isPending) { + return { + type: undefined, + url: animationUrl, + }; + } + + if (primaryQuery.data) { + return primaryQuery.data; + } + + if (secondaryQuery.isPending) { + return { + type: undefined, + url: imageUrl, + }; + } + + if (secondaryQuery.data) { + return secondaryQuery.data; + } + + return null; + }, [ animationUrl, imageUrl, primaryQuery.data, primaryQuery.isPending, secondaryQuery.data, secondaryQuery.isPending ]); +} + +function useNftMediaTypeQuery(url: string | null, enabled: boolean) { + const fetch = useFetch(); + + return useQuery, ReturnType | null>({ + queryKey: [ 'nft-media-type', url ], + queryFn: async() => { + if (!url) { + return null; + } + + // media could be either image, gif, video or html-page + // so we pre-fetch the resources in order to get its content type + // have to do it via Node.js due to strict CSP for connect-src + // but in order not to abuse our server firstly we check file url extension + // and if it is valid we will trust it and display corresponding media component + + const preliminaryType = getPreliminaryMediaType(url); + + if (preliminaryType) { + return { type: preliminaryType, url }; + } + + const type = await (async() => { + try { + const mediaTypeResourceUrl = route({ pathname: '/node-api/media-type' as StaticRoute<'/api/media-type'>['pathname'], query: { url } }); + const response = await fetch<{ type: MediaType | undefined }, ResourceError>(mediaTypeResourceUrl, undefined, { resource: 'media-type' }); + + return 'type' in response ? response.type : undefined; + } catch (error) { + return; + } + })(); + + if (!type) { + return null; + } + + return { type, url }; + }, + enabled, + staleTime: Infinity, + }); +} diff --git a/ui/shared/nft/useNftMediaType.tsx b/ui/shared/nft/useNftMediaType.tsx deleted file mode 100644 index 4444b122e3..0000000000 --- a/ui/shared/nft/useNftMediaType.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; - -import type { StaticRoute } from 'nextjs-routes'; -import { route } from 'nextjs-routes'; - -import type { ResourceError } from 'lib/api/resources'; -import useFetch from 'lib/hooks/useFetch'; - -import type { MediaType } from './utils'; -import { getPreliminaryMediaType } from './utils'; - -export default function useNftMediaType(url: string | null, isEnabled: boolean) { - - const fetch = useFetch(); - - const { data } = useQuery, MediaType>({ - queryKey: [ 'nft-media-type', url ], - queryFn: async() => { - if (!url) { - return 'image'; - } - - // media could be either image, gif, video or html-page - // so we pre-fetch the resources in order to get its content type - // have to do it via Node.js due to strict CSP for connect-src - // but in order not to abuse our server firstly we check file url extension - // and if it is valid we will trust it and display corresponding media component - - const preliminaryType = getPreliminaryMediaType(url); - - if (preliminaryType) { - return preliminaryType; - } - - try { - const mediaTypeResourceUrl = route({ pathname: '/node-api/media-type' as StaticRoute<'/api/media-type'>['pathname'], query: { url } }); - const response = await fetch<{ type: MediaType | undefined }, ResourceError>(mediaTypeResourceUrl, undefined, { resource: 'media-type' }); - - return 'type' in response ? response.type ?? 'image' : 'image'; - } catch (error) { - return 'image'; - } - }, - enabled: isEnabled && Boolean(url), - staleTime: Infinity, - }); - - return data; -} diff --git a/ui/shared/pagination/useQueryWithPages.ts b/ui/shared/pagination/useQueryWithPages.ts index fd293b5aa6..b95782153e 100644 --- a/ui/shared/pagination/useQueryWithPages.ts +++ b/ui/shared/pagination/useQueryWithPages.ts @@ -148,7 +148,7 @@ export default function useQueryWithPages({ }, [ queryClient, resourceName, router, scrollToTop ]); const onFilterChange = useCallback((newFilters: PaginationFilters | undefined) => { - const newQuery = omit(router.query, 'next_page_params', 'page', resource.filterFields); + const newQuery = omit(router.query, 'next_page_params', 'page', 'filterFields' in resource ? resource.filterFields : []); if (newFilters) { Object.entries(newFilters).forEach(([ key, value ]) => { const isValidValue = typeof value === 'boolean' || (value && value.length); @@ -170,7 +170,7 @@ export default function useQueryWithPages({ setPage(1); setPageParams(INITIAL_PAGE_PARAMS); }); - }, [ router, resource.filterFields, scrollToTop ]); + }, [ router, resource, scrollToTop ]); const onSortingChange = useCallback((newSorting: PaginationSorting | undefined) => { const newQuery = { diff --git a/ui/shared/radioButtonGroup/RadioButtonGroup.tsx b/ui/shared/radioButtonGroup/RadioButtonGroup.tsx index e4eb0b2358..f110ef542f 100644 --- a/ui/shared/radioButtonGroup/RadioButtonGroup.tsx +++ b/ui/shared/radioButtonGroup/RadioButtonGroup.tsx @@ -9,6 +9,7 @@ type RadioItemProps = { title: string; icon?: IconName; onlyIcon: false | undefined; + contentAfter?: React.ReactNode; } | { title: string; icon: IconName; @@ -67,9 +68,11 @@ const RadioButton = (props: RadioButtonProps) => { > { props.title } + { props.contentAfter } ); @@ -80,15 +83,22 @@ type RadioButtonGroupProps = { name: string; defaultValue: string; options: Array<{ value: T } & RadioItemProps>; + autoWidth?: boolean; } -const RadioButtonGroup = ({ onChange, name, defaultValue, options }: RadioButtonGroupProps) => { +const RadioButtonGroup = ({ onChange, name, defaultValue, options, autoWidth = false }: RadioButtonGroupProps) => { const { getRootProps, getRadioProps } = useRadioGroup({ name, defaultValue, onChange }); const group = getRootProps(); return ( - + { options.map((option) => { const props = getRadioProps({ value: option.value }); return ; diff --git a/ui/shared/search/utils.ts b/ui/shared/search/utils.ts index 9f5953bb50..eff4ac90b0 100644 --- a/ui/shared/search/utils.ts +++ b/ui/shared/search/utils.ts @@ -3,7 +3,7 @@ import type { MarketplaceAppOverview } from 'types/client/marketplace'; import config from 'configs/app'; -export type ApiCategory = 'token' | 'nft' | 'address' | 'public_tag' | 'transaction' | 'block' | 'user_operation'; +export type ApiCategory = 'token' | 'nft' | 'address' | 'public_tag' | 'transaction' | 'block' | 'user_operation' | 'blob' | 'domain'; export type Category = ApiCategory | 'app'; export type ItemsCategoriesMap = @@ -17,12 +17,14 @@ export type SearchResultAppItem = { export const searchCategories: Array<{id: Category; title: string }> = [ { id: 'app', title: 'DApps' }, + { id: 'domain', title: 'Names' }, { id: 'token', title: 'Tokens (ERC-20)' }, { id: 'nft', title: 'NFTs (ERC-721 & 1155)' }, { id: 'address', title: 'Addresses' }, { id: 'public_tag', title: 'Public tags' }, { id: 'transaction', title: 'Transactions' }, { id: 'block', title: 'Blocks' }, + { id: 'blob', title: 'Blobs' }, ]; if (config.features.userOps.isEnabled) { @@ -31,6 +33,7 @@ if (config.features.userOps.isEnabled) { export const searchItemTitles: Record = { app: { itemTitle: 'DApp', itemTitleShort: 'App' }, + domain: { itemTitle: 'Name', itemTitleShort: 'Name' }, token: { itemTitle: 'Token', itemTitleShort: 'Token' }, nft: { itemTitle: 'NFT', itemTitleShort: 'NFT' }, address: { itemTitle: 'Address', itemTitleShort: 'Address' }, @@ -38,6 +41,7 @@ export const searchItemTitles: Record void; + label?: string; +} + +const SolidityscanReportButton = ( + { className, score, isLoading, height = '32px', onlyIcon, onClick, label = 'Security score' }: Props, + ref: React.ForwardedRef, +) => { + const { scoreColor } = useScoreLevelAndColor(score); + + return ( + + + + ); +}; + +export default React.forwardRef(SolidityscanReportButton); diff --git a/ui/shared/solidityscanReport/SolidityscanReportDetails.tsx b/ui/shared/solidityscanReport/SolidityscanReportDetails.tsx new file mode 100644 index 0000000000..2d2a5cf140 --- /dev/null +++ b/ui/shared/solidityscanReport/SolidityscanReportDetails.tsx @@ -0,0 +1,60 @@ +import { Box, Flex, Text, Grid, useColorModeValue, chakra } from '@chakra-ui/react'; +import React from 'react'; + +import type { SolidityscanReport } from 'types/api/contract'; + +type DistributionItem = { + id: keyof SolidityscanReport['scan_report']['scan_summary']['issue_severity_distribution']; + name: string; + color: string; +} + +const DISTRIBUTION_ITEMS: Array = [ + { id: 'critical', name: 'Critical', color: '#891F11' }, + { id: 'high', name: 'High', color: '#EC672C' }, + { id: 'medium', name: 'Medium', color: '#FBE74D' }, + { id: 'low', name: 'Low', color: '#68C88E' }, + { id: 'informational', name: 'Informational', color: '#A3AEBE' }, + { id: 'gas', name: 'Gas', color: '#A47585' }, +]; + +interface Props { + vulnerabilities: SolidityscanReport['scan_report']['scan_summary']['issue_severity_distribution']; + vulnerabilitiesCount: number; +} + +type ItemProps = { + item: DistributionItem; + vulnerabilities: SolidityscanReport['scan_report']['scan_summary']['issue_severity_distribution']; + vulnerabilitiesCount: number; +} + +const SolidityScanReportItem = ({ item, vulnerabilities, vulnerabilitiesCount }: ItemProps) => { + const bgBar = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); + const yetAnotherGrayColor = useColorModeValue('gray.400', 'gray.500'); + + return ( + <> + + + { item.name } + 0 ? 'text' : yetAnotherGrayColor }>{ vulnerabilities[item.id] } + + + + + + ); +}; + +const SolidityscanReportDetails = ({ vulnerabilities, vulnerabilitiesCount }: Props) => { + return ( + + { DISTRIBUTION_ITEMS.map(item => ( + + )) } + + ); +}; + +export default chakra(SolidityscanReportDetails); diff --git a/ui/shared/solidityscanReport/SolidityscanReportScore.tsx b/ui/shared/solidityscanReport/SolidityscanReportScore.tsx new file mode 100644 index 0000000000..56bd296630 --- /dev/null +++ b/ui/shared/solidityscanReport/SolidityscanReportScore.tsx @@ -0,0 +1,45 @@ +import { Box, Flex, Text, chakra, Center, useColorModeValue } from '@chakra-ui/react'; +import React from 'react'; + +import IconSvg from 'ui/shared/IconSvg'; + +import useScoreLevelAndColor from './useScoreLevelAndColor'; + +interface Props { + className?: string; + score: number; +} + +const SolidityscanReportScore = ({ className, score }: Props) => { + const { scoreLevel, scoreColor } = useScoreLevelAndColor(score); + + const chartGrayColor = useColorModeValue('gray.100', 'gray.700'); + const yetAnotherGrayColor = useColorModeValue('gray.400', 'gray.500'); + const popoverBgColor = useColorModeValue('white', 'gray.900'); + + return ( + + +
+ +
+
+ + + { score } + / 100 + + Security score is { scoreLevel } + +
+ ); +}; + +export default chakra(SolidityscanReportScore); diff --git a/ui/shared/solidityscanReport/useScoreLevelAndColor.ts b/ui/shared/solidityscanReport/useScoreLevelAndColor.ts new file mode 100644 index 0000000000..b61d2a9109 --- /dev/null +++ b/ui/shared/solidityscanReport/useScoreLevelAndColor.ts @@ -0,0 +1,21 @@ +import { useColorModeValue } from '@chakra-ui/react'; + +export default function useScoreLevelAndColor(score: number) { + const greatScoreColor = useColorModeValue('green.600', 'green.400'); + const averageScoreColor = useColorModeValue('purple.600', 'purple.400'); + const lowScoreColor = useColorModeValue('red.600', 'red.400'); + + let scoreColor; + let scoreLevel; + if (score >= 80) { + scoreColor = greatScoreColor; + scoreLevel = 'GREAT'; + } else if (score >= 30) { + scoreColor = averageScoreColor; + scoreLevel = 'AVERAGE'; + } else { + scoreColor = lowScoreColor; + scoreLevel = 'LOW'; + } + return { scoreColor, scoreLevel }; +} diff --git a/ui/shared/stats/StatsWidget.pw.tsx b/ui/shared/stats/StatsWidget.pw.tsx new file mode 100644 index 0000000000..cc9fad8da4 --- /dev/null +++ b/ui/shared/stats/StatsWidget.pw.tsx @@ -0,0 +1,61 @@ +import React from 'react'; + +import { test, expect } from 'playwright/lib'; + +import StatsWidget from './StatsWidget'; + +test.use({ viewport: { width: 300, height: 100 } }); + +test('with positive diff +@dark-mode', async({ render }) => { + const component = await render( + , + ); + + await expect(component).toHaveScreenshot(); +}); + +// according to current logic we don't show diff if it's negative +test('with negative diff', async({ render }) => { + const component = await render( + , + ); + + await expect(component).toHaveScreenshot(); +}); + +test('loading state', async({ render }) => { + const component = await render( + , + ); + + await expect(component).toHaveScreenshot(); +}); + +test('with period only', async({ render }) => { + const component = await render( + , + ); + + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/shared/stats/StatsWidget.tsx b/ui/shared/stats/StatsWidget.tsx index f95220bac9..077dd1c4e4 100644 --- a/ui/shared/stats/StatsWidget.tsx +++ b/ui/shared/stats/StatsWidget.tsx @@ -1,66 +1,94 @@ -import { Box, Flex, Text, Skeleton, useColorModeValue } from '@chakra-ui/react'; +import { Box, Flex, Text, Skeleton, useColorModeValue, chakra } from '@chakra-ui/react'; +import NextLink from 'next/link'; import React from 'react'; +import type { Route } from 'nextjs-routes'; + import Hint from 'ui/shared/Hint'; +import TruncatedValue from 'ui/shared/TruncatedValue'; type Props = { label: string; value: string; + valuePrefix?: string; + valuePostfix?: string; hint?: string; isLoading?: boolean; diff?: string | number; diffFormatted?: string; diffPeriod?: '24h'; + period?: '1h' | '24h'; + href?: Route; } -const StatsWidget = ({ label, value, isLoading, hint, diff, diffPeriod = '24h', diffFormatted }: Props) => { - const bgColor = useColorModeValue('blue.50', 'blue.800'); +const Container = ({ href, children }: { href?: Route; children: JSX.Element }) => { + if (href) { + return ( + + { children } + + ); + } + + return children; +}; + +const StatsWidget = ({ label, value, valuePrefix, valuePostfix, isLoading, hint, diff, diffPeriod = '24h', diffFormatted, period, href }: Props) => { + const bgColor = useColorModeValue('blue.50', 'whiteAlpha.100'); const skeletonBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); const hintColor = useColorModeValue('gray.600', 'gray.400'); return ( - - - - { label } - - - { value } - { diff && Number(diff) > 0 && ( - <> - - +{ diffFormatted || Number(diff).toLocaleString() } - - ({ diffPeriod }) - - ) } - - - { hint && ( - - - - ) } - + + + + + { label } + + + { valuePrefix && { valuePrefix } } + + { valuePostfix && { valuePostfix } } + { diff && Number(diff) > 0 && ( + <> + + +{ diffFormatted || Number(diff).toLocaleString() } + + ({ diffPeriod }) + + ) } + { period && ({ period }) } + + + { hint && ( + + + + ) } + + ); }; diff --git a/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_dark-color-mode_with-positive-diff-dark-mode-1.png b/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_dark-color-mode_with-positive-diff-dark-mode-1.png new file mode 100644 index 0000000000..72405549ae Binary files /dev/null and b/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_dark-color-mode_with-positive-diff-dark-mode-1.png differ diff --git a/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_default_loading-state-1.png b/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_default_loading-state-1.png new file mode 100644 index 0000000000..2e0a87888e Binary files /dev/null and b/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_default_loading-state-1.png differ diff --git a/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_default_with-negative-diff-1.png b/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_default_with-negative-diff-1.png new file mode 100644 index 0000000000..ce20bc6d5c Binary files /dev/null and b/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_default_with-negative-diff-1.png differ diff --git a/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_default_with-period-only-1.png b/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_default_with-period-only-1.png new file mode 100644 index 0000000000..0aedb396c2 Binary files /dev/null and b/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_default_with-period-only-1.png differ diff --git a/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_default_with-positive-diff-dark-mode-1.png b/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_default_with-positive-diff-dark-mode-1.png new file mode 100644 index 0000000000..11389c31ff Binary files /dev/null and b/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_default_with-positive-diff-dark-mode-1.png differ diff --git a/ui/shared/statusTag/StatusTag.tsx b/ui/shared/statusTag/StatusTag.tsx index fb9d59a97d..71f2727ca2 100644 --- a/ui/shared/statusTag/StatusTag.tsx +++ b/ui/shared/statusTag/StatusTag.tsx @@ -29,8 +29,6 @@ const StatusTag = ({ type, text, errorText, isLoading }: Props) => { break; case 'pending': icon = 'status/pending'; - // FIXME: it's not gray on mockups - // need to implement new color scheme or redefine colors here colorScheme = 'gray'; break; } @@ -38,7 +36,7 @@ const StatusTag = ({ type, text, errorText, isLoading }: Props) => { return ( - + { text } diff --git a/ui/shared/statusTag/ValidatorStatus.tsx b/ui/shared/statusTag/ValidatorStatus.tsx index fb98700bb8..4386b55af5 100644 --- a/ui/shared/statusTag/ValidatorStatus.tsx +++ b/ui/shared/statusTag/ValidatorStatus.tsx @@ -16,7 +16,7 @@ const ValidatorStatus = ({ state, isLoading }: Props) => { case 'probation': return ; case 'inactive': - return ; + return ; } }; diff --git a/ui/shared/statusTag/ZkSyncL2TxnBatchStatus.tsx b/ui/shared/statusTag/ZkSyncL2TxnBatchStatus.tsx new file mode 100644 index 0000000000..7e0536eff4 --- /dev/null +++ b/ui/shared/statusTag/ZkSyncL2TxnBatchStatus.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +import type { ZkSyncBatchStatus } from 'types/api/zkSyncL2'; + +import type { StatusTagType } from './StatusTag'; +import StatusTag from './StatusTag'; + +export interface Props { + status: ZkSyncBatchStatus; + isLoading?: boolean; +} + +const ZkSyncL2TxnBatchStatus = ({ status, isLoading }: Props) => { + let type: StatusTagType; + + switch (status) { + case 'Executed on L1': + type = 'ok'; + break; + default: + type = 'pending'; + break; + } + + return ; +}; + +export default ZkSyncL2TxnBatchStatus; diff --git a/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_error-status-1.png b/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_error-status-1.png index a5ab415a85..42b5ac17f2 100644 Binary files a/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_error-status-1.png and b/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_error-status-1.png differ diff --git a/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_ok-status-1.png b/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_ok-status-1.png index 3224ee3df2..24dd6061e8 100644 Binary files a/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_ok-status-1.png and b/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_ok-status-1.png differ diff --git a/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_pending-status-1.png b/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_pending-status-1.png index 1e4454c4ea..b9add0a99c 100644 Binary files a/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_pending-status-1.png and b/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_pending-status-1.png differ diff --git a/ui/tx/interpretation/TxInterpretation.tsx b/ui/shared/tx/interpretation/TxInterpretation.tsx similarity index 100% rename from ui/tx/interpretation/TxInterpretation.tsx rename to ui/shared/tx/interpretation/TxInterpretation.tsx diff --git a/ui/tx/interpretation/utils.test.ts b/ui/shared/tx/interpretation/utils.test.ts similarity index 100% rename from ui/tx/interpretation/utils.test.ts rename to ui/shared/tx/interpretation/utils.test.ts diff --git a/ui/tx/interpretation/utils.ts b/ui/shared/tx/interpretation/utils.ts similarity index 100% rename from ui/tx/interpretation/utils.ts rename to ui/shared/tx/interpretation/utils.ts diff --git a/ui/shared/userOps/UserOpSponsorType.tsx b/ui/shared/userOps/UserOpSponsorType.tsx index cd8d27327d..cc006ffdef 100644 --- a/ui/shared/userOps/UserOpSponsorType.tsx +++ b/ui/shared/userOps/UserOpSponsorType.tsx @@ -1,10 +1,10 @@ import { Tag } from '@chakra-ui/react'; import React from 'react'; -import { UserOpSponsorType } from 'types/api/userOps'; +import type { UserOpSponsorType as TUserOpSponsorType } from 'types/api/userOps'; type Props = { - sponsorType: UserOpSponsorType; + sponsorType: TUserOpSponsorType; } const UserOpSponsorType = ({ sponsorType }: Props) => { diff --git a/ui/snippets/footer/Footer.pw.tsx b/ui/snippets/footer/Footer.pw.tsx index 5749ebf485..3b8498e3b2 100644 --- a/ui/snippets/footer/Footer.pw.tsx +++ b/ui/snippets/footer/Footer.pw.tsx @@ -1,54 +1,28 @@ -import { test as base, expect } from '@playwright/experimental-ct-react'; import React from 'react'; -import type { WindowProvider } from 'wagmi'; -import { buildExternalAssetFilePath } from 'configs/app/utils'; import { FOOTER_LINKS } from 'mocks/config/footerLinks'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; -import * as app from 'playwright/utils/app'; -import buildApiUrl from 'playwright/utils/buildApiUrl'; +import { test, expect } from 'playwright/lib'; import * as configs from 'playwright/utils/configs'; import Footer from './Footer'; -const FOOTER_LINKS_URL = app.url + buildExternalAssetFilePath('NEXT_PUBLIC_FOOTER_LINKS', 'https://localhost:3000/footer-links.json') || ''; - -const BACKEND_VERSION_API_URL = buildApiUrl('config_backend_version'); -const INDEXING_ALERT_API_URL = buildApiUrl('homepage_indexing_status'); - -base.describe('with custom links, max cols', () => { - const test = base.extend({ - context: contextWithEnvs([ - { name: 'NEXT_PUBLIC_FOOTER_LINKS', value: FOOTER_LINKS_URL }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ]) as any, - }); - - test.beforeEach(async({ page, mount }) => { - await page.route(FOOTER_LINKS_URL, (route) => { - return route.fulfill({ - body: JSON.stringify(FOOTER_LINKS), - }); +const FOOTER_LINKS_URL = 'https://localhost:3000/footer-links.json'; + +test.describe('with custom links, max cols', () => { + test.beforeEach(async({ render, mockApiResponse, mockConfigResponse, injectMetaMaskProvider, mockEnvs }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_FOOTER_LINKS', FOOTER_LINKS_URL ], + ]); + await mockConfigResponse('NEXT_PUBLIC_FOOTER_LINKS', FOOTER_LINKS_URL, JSON.stringify(FOOTER_LINKS)); + await injectMetaMaskProvider(); + await mockApiResponse('homepage_indexing_status', { + finished_indexing: false, + finished_indexing_blocks: false, + indexed_internal_transactions_ratio: '0.1', + indexed_blocks_ratio: '0.1', }); - await page.evaluate(() => { - window.ethereum = { - isMetaMask: true, - _events: {}, - } as WindowProvider; - }); - - await page.route(INDEXING_ALERT_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify({ finished_indexing: false, indexed_internal_transactions_ratio: 0.1 }), - })); - - await mount( - -
- , - ); + await render(
); }); test('+@mobile +@dark-mode', async({ page }) => { @@ -64,74 +38,35 @@ base.describe('with custom links, max cols', () => { }); }); -base.describe('with custom links, min cols', () => { - const test = base.extend({ - context: contextWithEnvs([ - { name: 'NEXT_PUBLIC_FOOTER_LINKS', value: FOOTER_LINKS_URL }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ]) as any, - }); - - test('base view +@dark-mode +@mobile', async({ mount, page }) => { - await page.route(FOOTER_LINKS_URL, (route) => { - return route.fulfill({ - body: JSON.stringify([ FOOTER_LINKS[0] ]), - }); - }); - - await mount( - -
- , - ); - +test.describe('with custom links, min cols', () => { + test('base view +@dark-mode +@mobile', async({ render, page, mockConfigResponse, mockEnvs }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_FOOTER_LINKS', FOOTER_LINKS_URL ], + ]); + await mockConfigResponse('NEXT_PUBLIC_FOOTER_LINKS', FOOTER_LINKS_URL, JSON.stringify([ FOOTER_LINKS[0] ])); + await render(
); await expect(page).toHaveScreenshot(); }); }); -base.describe('without custom links', () => { - base('base view +@dark-mode +@mobile', async({ mount, page }) => { - await page.evaluate(() => { - window.ethereum = { - isMetaMask: true, - _events: {}, - } as WindowProvider; - }); - await page.route(BACKEND_VERSION_API_URL, (route) => { - return route.fulfill({ - body: JSON.stringify({ - backend_version: 'v5.2.0-beta.+commit.1ce1a355', - }), - }); - }); - - await mount( - -
- , - ); - +test.describe('without custom links', () => { + test('base view +@dark-mode +@mobile', async({ render, page, injectMetaMaskProvider, mockApiResponse }) => { + await injectMetaMaskProvider(); + await mockApiResponse('config_backend_version', { backend_version: 'v5.2.0-beta.+commit.1ce1a355' }); + await render(
); await expect(page).toHaveScreenshot(); }); - base('with indexing alert +@dark-mode +@mobile', async({ mount, page }) => { - await page.evaluate(() => { - window.ethereum = { - providers: [ { isMetaMask: true, _events: {} } ], - } as WindowProvider; + test('with indexing alert +@dark-mode +@mobile', async({ render, injectMetaMaskProvider, mockApiResponse }) => { + await injectMetaMaskProvider(); + await mockApiResponse('homepage_indexing_status', { + finished_indexing: false, + finished_indexing_blocks: false, + indexed_internal_transactions_ratio: '0.1', + indexed_blocks_ratio: '0.1', }); - await page.route(INDEXING_ALERT_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify({ finished_indexing: false, indexed_internal_transactions_ratio: 0.1 }), - })); - - const component = await mount( - -
- , - ); - + const component = await render(
); await expect(component).toHaveScreenshot(); }); }); diff --git a/ui/snippets/footer/Footer.tsx b/ui/snippets/footer/Footer.tsx index 95e372d678..a2613214b6 100644 --- a/ui/snippets/footer/Footer.tsx +++ b/ui/snippets/footer/Footer.tsx @@ -44,9 +44,9 @@ const Footer = () => { url: 'https://github.com/subspace', }, { - icon: 'social/tweet' as const, + icon: 'social/twitter' as const, iconSize: '18px', - text: 'Twitter', + text: 'X (ex-Twitter)', url: 'https://twitter.com/NetworkSubspace', }, { diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_with-custom-links-2-cols-base-view-dark-mode-mobile-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_with-custom-links-2-cols-base-view-dark-mode-mobile-1.png new file mode 100644 index 0000000000..2fae017e2e Binary files /dev/null and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_with-custom-links-2-cols-base-view-dark-mode-mobile-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_with-custom-links-4-cols-mobile-dark-mode-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_with-custom-links-4-cols-mobile-dark-mode-1.png new file mode 100644 index 0000000000..9992af8a55 Binary files /dev/null and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_with-custom-links-4-cols-mobile-dark-mode-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_with-custom-links-max-cols-mobile-dark-mode-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_with-custom-links-max-cols-mobile-dark-mode-1.png index 0fc5ef3bc9..cee8353482 100644 Binary files a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_with-custom-links-max-cols-mobile-dark-mode-1.png and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_with-custom-links-max-cols-mobile-dark-mode-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_with-custom-links-min-cols-base-view-dark-mode-mobile-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_with-custom-links-min-cols-base-view-dark-mode-mobile-1.png index ca21b198bd..2dc7fee18d 100644 Binary files a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_with-custom-links-min-cols-base-view-dark-mode-mobile-1.png and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_with-custom-links-min-cols-base-view-dark-mode-mobile-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_without-custom-links-base-view-dark-mode-mobile-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_without-custom-links-base-view-dark-mode-mobile-1.png index 8e7ccaa17f..c6fead1104 100644 Binary files a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_without-custom-links-base-view-dark-mode-mobile-1.png and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_without-custom-links-base-view-dark-mode-mobile-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_without-custom-links-with-indexing-alert-dark-mode-mobile-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_without-custom-links-with-indexing-alert-dark-mode-mobile-1.png index 30756d332a..7d72b3cce6 100644 Binary files a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_without-custom-links-with-indexing-alert-dark-mode-mobile-1.png and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_without-custom-links-with-indexing-alert-dark-mode-mobile-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-2-cols-base-view-dark-mode-mobile-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-2-cols-base-view-dark-mode-mobile-1.png new file mode 100644 index 0000000000..8753136808 Binary files /dev/null and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-2-cols-base-view-dark-mode-mobile-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-4-cols-mobile-dark-mode-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-4-cols-mobile-dark-mode-1.png new file mode 100644 index 0000000000..e65c1f29dc Binary files /dev/null and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-4-cols-mobile-dark-mode-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-4-cols-screen-xl-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-4-cols-screen-xl-1.png new file mode 100644 index 0000000000..598429bafd Binary files /dev/null and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-4-cols-screen-xl-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-max-cols-mobile-dark-mode-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-max-cols-mobile-dark-mode-1.png index 4ed0980dbd..0a7fd22261 100644 Binary files a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-max-cols-mobile-dark-mode-1.png and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-max-cols-mobile-dark-mode-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-max-cols-screen-xl-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-max-cols-screen-xl-1.png index 252ba0ea84..1131de2504 100644 Binary files a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-max-cols-screen-xl-1.png and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-max-cols-screen-xl-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-min-cols-base-view-dark-mode-mobile-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-min-cols-base-view-dark-mode-mobile-1.png index a8b838f541..44c4be8454 100644 Binary files a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-min-cols-base-view-dark-mode-mobile-1.png and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-min-cols-base-view-dark-mode-mobile-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_without-custom-links-base-view-dark-mode-mobile-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_without-custom-links-base-view-dark-mode-mobile-1.png index aada6b053a..d93aa584d9 100644 Binary files a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_without-custom-links-base-view-dark-mode-mobile-1.png and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_without-custom-links-base-view-dark-mode-mobile-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_without-custom-links-with-indexing-alert-dark-mode-mobile-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_without-custom-links-with-indexing-alert-dark-mode-mobile-1.png index 5681129337..6987a4f229 100644 Binary files a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_without-custom-links-with-indexing-alert-dark-mode-mobile-1.png and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_without-custom-links-with-indexing-alert-dark-mode-mobile-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_mobile_with-custom-links-2-cols-base-view-dark-mode-mobile-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_mobile_with-custom-links-2-cols-base-view-dark-mode-mobile-1.png new file mode 100644 index 0000000000..aa2714cbbb Binary files /dev/null and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_mobile_with-custom-links-2-cols-base-view-dark-mode-mobile-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_mobile_with-custom-links-4-cols-mobile-dark-mode-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_mobile_with-custom-links-4-cols-mobile-dark-mode-1.png new file mode 100644 index 0000000000..50afbdd952 Binary files /dev/null and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_mobile_with-custom-links-4-cols-mobile-dark-mode-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_mobile_with-custom-links-max-cols-mobile-dark-mode-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_mobile_with-custom-links-max-cols-mobile-dark-mode-1.png index edab8c4966..3c6775e03a 100644 Binary files a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_mobile_with-custom-links-max-cols-mobile-dark-mode-1.png and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_mobile_with-custom-links-max-cols-mobile-dark-mode-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_mobile_with-custom-links-min-cols-base-view-dark-mode-mobile-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_mobile_with-custom-links-min-cols-base-view-dark-mode-mobile-1.png index 483b96df6c..22f73abe34 100644 Binary files a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_mobile_with-custom-links-min-cols-base-view-dark-mode-mobile-1.png and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_mobile_with-custom-links-min-cols-base-view-dark-mode-mobile-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_mobile_without-custom-links-base-view-dark-mode-mobile-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_mobile_without-custom-links-base-view-dark-mode-mobile-1.png index ba074854d2..1214d88ab0 100644 Binary files a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_mobile_without-custom-links-base-view-dark-mode-mobile-1.png and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_mobile_without-custom-links-base-view-dark-mode-mobile-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_mobile_without-custom-links-with-indexing-alert-dark-mode-mobile-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_mobile_without-custom-links-with-indexing-alert-dark-mode-mobile-1.png index c519578200..ccae54fcc4 100644 Binary files a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_mobile_without-custom-links-with-indexing-alert-dark-mode-mobile-1.png and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_mobile_without-custom-links-with-indexing-alert-dark-mode-mobile-1.png differ diff --git a/ui/snippets/header/Burger.pw.tsx b/ui/snippets/header/Burger.pw.tsx index 9f64412b9b..ebda473be5 100644 --- a/ui/snippets/header/Burger.pw.tsx +++ b/ui/snippets/header/Burger.pw.tsx @@ -1,20 +1,16 @@ -import { test as base, expect, devices } from '@playwright/experimental-ct-react'; +import type { BrowserContext } from '@playwright/test'; import React from 'react'; -import { buildExternalAssetFilePath } from 'configs/app/utils'; import { FEATURED_NETWORKS_MOCK } from 'mocks/config/network'; -import authFixture from 'playwright/fixtures/auth'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import createContextWithStorage from 'playwright/fixtures/createContextWithStorage'; -import TestApp from 'playwright/TestApp'; -import * as app from 'playwright/utils/app'; +import { contextWithAuth } from 'playwright/fixtures/auth'; +import { test, expect, devices } from 'playwright/lib'; import Burger from './Burger'; -const FEATURED_NETWORKS_URL = app.url + buildExternalAssetFilePath('NEXT_PUBLIC_FEATURED_NETWORKS', 'https://localhost:3000/featured-networks.json') || ''; +const FEATURED_NETWORKS_URL = 'https://localhost:3000/featured-networks.json'; const LOGO_URL = 'https://localhost:3000/my-logo.png'; -base.use({ viewport: devices['iPhone 13 Pro'].viewport }); +test.use({ viewport: devices['iPhone 13 Pro'].viewport }); const hooksConfig = { router: { @@ -24,32 +20,16 @@ const hooksConfig = { }, }; -const test = base.extend({ - context: contextWithEnvs([ - { name: 'NEXT_PUBLIC_FEATURED_NETWORKS', value: FEATURED_NETWORKS_URL }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ]) as any, +test.beforeEach(async({ mockEnvs, mockConfigResponse, mockAssetResponse }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_FEATURED_NETWORKS', FEATURED_NETWORKS_URL ], + ]); + await mockConfigResponse('NEXT_PUBLIC_FEATURED_NETWORKS', FEATURED_NETWORKS_URL, FEATURED_NETWORKS_MOCK); + await mockAssetResponse(LOGO_URL, './playwright/mocks/image_s.jpg'); }); -test('base view', async({ mount, page }) => { - await page.route(FEATURED_NETWORKS_URL, (route) => { - return route.fulfill({ - body: FEATURED_NETWORKS_MOCK, - }); - }); - await page.route(LOGO_URL, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); - - const component = await mount( - - - , - { hooksConfig }, - ); +test('base view', async({ render, page }) => { + const component = await render(, { hooksConfig }); await component.locator('div[aria-label="Menu button"]').click(); await expect(page.locator('.chakra-modal__content-container')).toHaveScreenshot(); @@ -61,25 +41,8 @@ test('base view', async({ mount, page }) => { test.describe('dark mode', () => { test.use({ colorScheme: 'dark' }); - test('base view', async({ mount, page }) => { - await page.route(FEATURED_NETWORKS_URL, (route) => { - return route.fulfill({ - body: FEATURED_NETWORKS_MOCK, - }); - }); - await page.route(LOGO_URL, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); - - const component = await mount( - - - , - { hooksConfig }, - ); + test('base view', async({ render, page }) => { + const component = await render(, { hooksConfig }); await component.locator('div[aria-label="Menu button"]').click(); await expect(page).toHaveScreenshot(); @@ -89,39 +52,23 @@ test.describe('dark mode', () => { }); }); -test('submenu', async({ mount, page }) => { - const component = await mount( - - - , - { hooksConfig }, - ); +test('submenu', async({ render, page }) => { + const component = await render(, { hooksConfig }); await component.locator('div[aria-label="Menu button"]').click(); await page.locator('div[aria-label="Blockchain link group"]').click(); await expect(page).toHaveScreenshot(); }); -test.describe('auth', () => { - const extendedTest = base.extend({ - context: async({ browser }, use) => { - const context = await createContextWithStorage(browser, [ - { name: 'NEXT_PUBLIC_FEATURED_NETWORKS', value: FEATURED_NETWORKS_URL }, - ]); - authFixture(context); - use(context); - }, - }); +const authTest = test.extend<{ context: BrowserContext }>({ + context: contextWithAuth, +}); - extendedTest.use({ viewport: { width: devices['iPhone 13 Pro'].viewport.width, height: 800 } }); +authTest.describe('auth', () => { + authTest.use({ viewport: { width: devices['iPhone 13 Pro'].viewport.width, height: 800 } }); - extendedTest('base view', async({ mount, page }) => { - const component = await mount( - - - , - { hooksConfig }, - ); + authTest('base view', async({ render, page }) => { + const component = await render(, { hooksConfig }); await component.locator('div[aria-label="Menu button"]').click(); await expect(page).toHaveScreenshot(); diff --git a/ui/snippets/header/ColorModeToggler.tsx b/ui/snippets/header/ColorModeToggler.tsx new file mode 100644 index 0000000000..5ba097948c --- /dev/null +++ b/ui/snippets/header/ColorModeToggler.tsx @@ -0,0 +1,124 @@ +import type { UseCheckboxProps } from '@chakra-ui/checkbox'; +import { useCheckbox } from '@chakra-ui/checkbox'; +import { useColorMode, useColorModeValue, Icon } from '@chakra-ui/react'; +import type { + SystemStyleObject, + ThemingProps, + HTMLChakraProps, +} from '@chakra-ui/system'; +import { + chakra, + forwardRef, + omitThemingProps, +} from '@chakra-ui/system'; +import { dataAttr, __DEV__ } from '@chakra-ui/utils'; +import * as React from 'react'; + +import moonIcon from 'icons/moon.svg'; +import sunIcon from 'icons/sun.svg'; +import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; + +export interface ColorModeTogglerProps + extends Omit, + Omit, keyof UseCheckboxProps>, + ThemingProps<'Switch'> { + trackBg?: string; +} + +const ColorModeToggler = forwardRef((props, ref) => { + const ownProps = omitThemingProps(props); + const { toggleColorMode, colorMode } = useColorMode(); + + const { + state, + getInputProps, + getCheckboxProps, + getRootProps, + } = useCheckbox({ ...ownProps, isChecked: colorMode === 'light' }); + + const trackBg = useColorModeValue('blackAlpha.100', 'whiteAlpha.100'); + const thumbBg = 'white'; + const transitionProps = getDefaultTransitionProps(); + + const trackStyles: SystemStyleObject = React.useMemo(() => ({ + bgColor: props.trackBg || trackBg, + width: '72px', + height: '32px', + borderRadius: 'full', + display: 'inline-flex', + flexShrink: 0, + justifyContent: 'space-between', + boxSizing: 'content-box', + cursor: 'pointer', + ...transitionProps, + transitionDuration: 'ultra-slow', + }), [ props.trackBg, trackBg, transitionProps ]); + + const thumbStyles: SystemStyleObject = React.useMemo(() => ({ + bg: thumbBg, + boxShadow: 'md', + width: '24px', + height: '24px', + borderRadius: 'md', + position: 'absolute', + transform: state.isChecked ? 'translate(44px, 4px)' : 'translate(4px, 4px)', + ...transitionProps, + transitionProperty: 'background-color, transform', + transitionDuration: 'ultra-slow', + }), [ thumbBg, transitionProps, state.isChecked ]); + + return ( + + + + + + + + + ); +}); + +if (__DEV__) { + ColorModeToggler.displayName = 'ColorModeToggler'; +} + +export default ColorModeToggler; diff --git a/ui/snippets/header/Header.pw.tsx b/ui/snippets/header/Header.pw.tsx new file mode 100644 index 0000000000..fc8130ce69 --- /dev/null +++ b/ui/snippets/header/Header.pw.tsx @@ -0,0 +1,30 @@ +import { test, expect } from '@playwright/experimental-ct-react'; +import React from 'react'; + +import TestApp from 'playwright/TestApp'; + +import Header from './Header'; + +test('no auth +@mobile', async({ mount, page }) => { + await mount( + +
+ , + ); + + await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 150 } }); +}); + +test.describe('dark mode', () => { + test.use({ colorScheme: 'dark' }); + + test('+@mobile', async({ mount, page }) => { + await mount( + +
+ , + ); + + await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 150 } }); + }); +}); diff --git a/ui/snippets/header/Header.tsx b/ui/snippets/header/Header.tsx new file mode 100644 index 0000000000..4a69a6627a --- /dev/null +++ b/ui/snippets/header/Header.tsx @@ -0,0 +1,69 @@ +import { HStack, Box, Flex, useColorModeValue } from '@chakra-ui/react'; +import React from 'react'; + +import config from 'configs/app'; +import { useScrollDirection } from 'lib/contexts/scrollDirection'; +import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo'; +import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop'; +import ProfileMenuMobile from 'ui/snippets/profileMenu/ProfileMenuMobile'; +import SearchBar from 'ui/snippets/searchBar/SearchBar'; + +import Burger from './Burger'; + +type Props = { + isHomePage?: boolean; + renderSearchBar?: () => React.ReactNode; +} + +const Header = ({ isHomePage, renderSearchBar }: Props) => { + const bgColor = useColorModeValue('white', 'black'); + const scrollDirection = useScrollDirection(); + + const searchBar = renderSearchBar ? renderSearchBar() : ; + + return ( + <> + + + + + { config.features.account.isEnabled ? : } + + { !isHomePage && searchBar } + + + { !isHomePage && ( + + + { searchBar } + + { config.features.account.isEnabled && } + + ) } + + + ); +}; + +export default Header; diff --git a/ui/snippets/header/HeaderMobile.tsx b/ui/snippets/header/HeaderMobile.tsx index 570e0d157c..b1851dd3ba 100644 --- a/ui/snippets/header/HeaderMobile.tsx +++ b/ui/snippets/header/HeaderMobile.tsx @@ -11,16 +11,12 @@ import WalletMenuMobile from 'ui/snippets/walletMenu/WalletMenuMobile'; import Burger from './Burger'; -const LOGO_IMAGE_PROPS = { - margin: '0 auto', -}; - type Props = { - isHomePage?: boolean; + hideSearchBar?: boolean; renderSearchBar?: () => React.ReactNode; } -const HeaderMobile = ({ isHomePage, renderSearchBar }: Props) => { +const HeaderMobile = ({ hideSearchBar, renderSearchBar }: Props) => { const bgColor = useColorModeValue('white', 'black'); const scrollDirection = useScrollDirection(); const { ref, inView } = useInView({ threshold: 1 }); @@ -45,19 +41,18 @@ const HeaderMobile = ({ isHomePage, renderSearchBar }: Props) => { bgColor={ bgColor } width="100%" alignItems="center" - justifyContent="space-between" transitionProperty="box-shadow" transitionDuration="slow" boxShadow={ !inView && scrollDirection === 'down' ? 'md' : 'none' } > - + { config.features.account.isEnabled ? : } { config.features.blockchainInteraction.isEnabled && } - { !isHomePage && searchBar } + { !hideSearchBar && searchBar } ); }; diff --git a/ui/snippets/header/__screenshots__/HeaderDesktop.pw.tsx_dark-color-mode_default-view-dark-mode-1.png b/ui/snippets/header/__screenshots__/HeaderDesktop.pw.tsx_dark-color-mode_default-view-dark-mode-1.png index 20aa839385..bf29251fe5 100644 Binary files a/ui/snippets/header/__screenshots__/HeaderDesktop.pw.tsx_dark-color-mode_default-view-dark-mode-1.png and b/ui/snippets/header/__screenshots__/HeaderDesktop.pw.tsx_dark-color-mode_default-view-dark-mode-1.png differ diff --git a/ui/snippets/header/__screenshots__/HeaderDesktop.pw.tsx_default_default-view-dark-mode-1.png b/ui/snippets/header/__screenshots__/HeaderDesktop.pw.tsx_default_default-view-dark-mode-1.png index d143733147..66b62ef072 100644 Binary files a/ui/snippets/header/__screenshots__/HeaderDesktop.pw.tsx_default_default-view-dark-mode-1.png and b/ui/snippets/header/__screenshots__/HeaderDesktop.pw.tsx_default_default-view-dark-mode-1.png differ diff --git a/ui/snippets/header/__screenshots__/HeaderMobile.pw.tsx_dark-color-mode_default-view-dark-mode-1.png b/ui/snippets/header/__screenshots__/HeaderMobile.pw.tsx_dark-color-mode_default-view-dark-mode-1.png index 01dba03498..523ba2f0af 100644 Binary files a/ui/snippets/header/__screenshots__/HeaderMobile.pw.tsx_dark-color-mode_default-view-dark-mode-1.png and b/ui/snippets/header/__screenshots__/HeaderMobile.pw.tsx_dark-color-mode_default-view-dark-mode-1.png differ diff --git a/ui/snippets/header/__screenshots__/HeaderMobile.pw.tsx_default_default-view-dark-mode-1.png b/ui/snippets/header/__screenshots__/HeaderMobile.pw.tsx_default_default-view-dark-mode-1.png index 803a3f79ec..ea433cbee6 100644 Binary files a/ui/snippets/header/__screenshots__/HeaderMobile.pw.tsx_default_default-view-dark-mode-1.png and b/ui/snippets/header/__screenshots__/HeaderMobile.pw.tsx_default_default-view-dark-mode-1.png differ diff --git a/ui/snippets/navigation/NavLink.tsx b/ui/snippets/navigation/NavLink.tsx index 67dbbe1f6f..1efb4beb58 100644 --- a/ui/snippets/navigation/NavLink.tsx +++ b/ui/snippets/navigation/NavLink.tsx @@ -20,16 +20,17 @@ type Props = { px?: string | number; className?: string; onClick?: () => void; + disableActiveState?: boolean; } -const NavLink = ({ item, isCollapsed, px, className, onClick }: Props) => { +const NavLink = ({ item, isCollapsed, px, className, onClick, disableActiveState }: Props) => { const isMobile = useIsMobile(); const colors = useColors(); const isExpanded = isCollapsed === false; const isInternalLink = isInternalItem(item); - const styleProps = useNavLinkStyleProps({ isCollapsed, isExpanded, isActive: isInternalLink && item.isActive }); + const styleProps = useNavLinkStyleProps({ isCollapsed, isExpanded, isActive: isInternalLink && item.isActive && !disableActiveState }); const isXLScreen = useBreakpointValue({ base: false, xl: true }); const href = isInternalLink ? route(item.nextRoute) : item.url; @@ -61,7 +62,7 @@ const NavLink = ({ item, isCollapsed, px, className, onClick }: Props) => { > - + { item.text } { !isInternalLink && } diff --git a/ui/snippets/navigation/NavigationDesktop.pw.tsx b/ui/snippets/navigation/NavigationDesktop.pw.tsx index 9ba7520405..27eb280a88 100644 --- a/ui/snippets/navigation/NavigationDesktop.pw.tsx +++ b/ui/snippets/navigation/NavigationDesktop.pw.tsx @@ -1,15 +1,12 @@ import { Box, Flex } from '@chakra-ui/react'; -import { test as base, expect } from '@playwright/experimental-ct-react'; -import type { Locator } from '@playwright/test'; +import type { BrowserContext, Locator } from '@playwright/test'; import React from 'react'; -import { buildExternalAssetFilePath } from 'configs/app/utils'; +import config from 'configs/app'; import * as cookies from 'lib/cookies'; -import authFixture from 'playwright/fixtures/auth'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import createContextWithStorage from 'playwright/fixtures/createContextWithStorage'; -import TestApp from 'playwright/TestApp'; -import * as app from 'playwright/utils/app'; +import { FEATURED_NETWORKS_MOCK } from 'mocks/config/network'; +import { contextWithAuth } from 'playwright/fixtures/auth'; +import { test, expect } from 'playwright/lib'; import * as configs from 'playwright/utils/configs'; import NavigationDesktop from './NavigationDesktop'; @@ -22,26 +19,24 @@ const hooksConfig = { }, }; -const FEATURED_NETWORKS_URL = app.url + buildExternalAssetFilePath('NEXT_PUBLIC_FEATURED_NETWORKS', 'https://localhost:3000/config.json') || ''; +const FEATURED_NETWORKS_URL = 'https://localhost:3000/featured-networks.json'; -const test = base.extend({ - context: contextWithEnvs([ - { name: 'NEXT_PUBLIC_FEATURED_NETWORKS', value: FEATURED_NETWORKS_URL }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ]) as any, +test.beforeEach(async({ mockEnvs, mockConfigResponse }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_FEATURED_NETWORKS', FEATURED_NETWORKS_URL ], + ]); + await mockConfigResponse('NEXT_PUBLIC_FEATURED_NETWORKS', FEATURED_NETWORKS_URL, FEATURED_NETWORKS_MOCK); }); test.describe('no auth', () => { let component: Locator; - test.beforeEach(async({ mount }) => { - component = await mount( - - - - - - , + test.beforeEach(async({ render }) => { + component = await render( + + + + , { hooksConfig }, ); }); @@ -59,39 +54,31 @@ test.describe('no auth', () => { }); }); -base.describe('auth', () => { - const test = base.extend({ - context: async({ browser }, use) => { - const context = await createContextWithStorage(browser, [ - { name: 'NEXT_PUBLIC_FEATURED_NETWORKS', value: FEATURED_NETWORKS_URL }, - ]); - authFixture(context); - use(context); - }, - }); +const authTest = test.extend<{ context: BrowserContext }>({ + context: contextWithAuth, +}); +authTest.describe('auth', () => { let component: Locator; - test.beforeEach(async({ mount }) => { - component = await mount( - - - - - - , + authTest.beforeEach(async({ render }) => { + component = await render( + + + + , { hooksConfig }, ); }); - test('+@dark-mode', async() => { + authTest('+@dark-mode', async() => { await expect(component).toHaveScreenshot(); }); - test.describe('xl screen', () => { - test.use({ viewport: configs.viewport.xl }); + authTest.describe('xl screen', () => { + authTest.use({ viewport: configs.viewport.xl }); - test('+@dark-mode', async() => { + authTest('+@dark-mode', async() => { await expect(component).toHaveScreenshot(); }); }); @@ -100,14 +87,12 @@ base.describe('auth', () => { test.describe('with tooltips', () => { test.use({ viewport: configs.viewport.xl }); - test('', async({ mount, page }) => { - const component = await mount( - - - - - - , + test('', async({ render, page }) => { + const component = await render( + + + + , { hooksConfig }, ); @@ -122,14 +107,12 @@ test.describe('with tooltips', () => { test.describe('with submenu', () => { let component: Locator; - test.beforeEach(async({ mount, page }) => { - component = await mount( - - - - - - , + test.beforeEach(async({ render, page }) => { + component = await render( + + + + , { hooksConfig }, ); await page.locator('a[aria-label="Blockchain link group"]').hover(); @@ -148,65 +131,55 @@ test.describe('with submenu', () => { }); }); -base.describe('cookie set to false', () => { - const test = base.extend({ - context: async({ browser }, use) => { - const context = await createContextWithStorage(browser, [ - { name: 'NEXT_PUBLIC_FEATURED_NETWORKS', value: FEATURED_NETWORKS_URL }, - ]); - context.addCookies([ { name: cookies.NAMES.NAV_BAR_COLLAPSED, value: 'false', domain: app.domain, path: '/' } ]); - use(context); - }, - }); +const noSideBarCookieTest = test.extend({ + context: ({ context }, use) => { + context.addCookies([ { name: cookies.NAMES.NAV_BAR_COLLAPSED, value: 'false', domain: config.app.host, path: '/' } ]); + use(context); + }, +}); +noSideBarCookieTest.describe('cookie set to false', () => { let component: Locator; - test.beforeEach(async({ mount }) => { - component = await mount( - - - - - - , + noSideBarCookieTest.beforeEach(async({ render }) => { + component = await render( + + + + , { hooksConfig }, ); }); - test('', async() => { + noSideBarCookieTest('', async() => { const networkMenu = component.locator('button[aria-label="Network menu"]'); await expect(networkMenu).toBeVisible(); }); - test.describe('xl screen', () => { - test.use({ viewport: configs.viewport.xl }); + noSideBarCookieTest.describe('xl screen', () => { + noSideBarCookieTest.use({ viewport: configs.viewport.xl }); - test('', async() => { + noSideBarCookieTest('', async() => { const networkMenu = component.locator('button[aria-label="Network menu"]'); await expect(networkMenu).toBeVisible(); }); }); }); -base.describe('cookie set to true', () => { - const test = base.extend({ - context: async({ browser }, use) => { - const context = await createContextWithStorage(browser, [ - { name: 'NEXT_PUBLIC_FEATURED_NETWORKS', value: FEATURED_NETWORKS_URL }, - ]); - context.addCookies([ { name: cookies.NAMES.NAV_BAR_COLLAPSED, value: 'true', domain: 'localhost', path: '/' } ]); - use(context); - }, - }); +const sideBarCookieTest = test.extend({ + context: ({ context }, use) => { + context.addCookies([ { name: cookies.NAMES.NAV_BAR_COLLAPSED, value: 'true', domain: config.app.host, path: '/' } ]); + use(context); + }, +}); - test('navbar is collapsed', async({ mount }) => { - const component = await mount( - - - - - - , +sideBarCookieTest.describe('cookie set to true', () => { + sideBarCookieTest('navbar is collapsed', async({ render }) => { + const component = await render( + + + + , { hooksConfig }, ); @@ -215,14 +188,12 @@ base.describe('cookie set to true', () => { }); }); -test('hover +@dark-mode', async({ mount }) => { - const component = await mount( - - - - - - , +test('hover +@dark-mode', async({ render }) => { + const component = await render( + + + + , { hooksConfig }, ); @@ -233,14 +204,12 @@ test('hover +@dark-mode', async({ mount }) => { test.describe('hover xl screen', () => { test.use({ viewport: configs.viewport.xl }); - test('+@dark-mode', async({ mount }) => { - const component = await mount( - - - - - - , + test('+@dark-mode', async({ render }) => { + const component = await render( + + + + , { hooksConfig }, ); diff --git a/ui/snippets/networkMenu/NetworkLogo.pw.tsx b/ui/snippets/networkMenu/NetworkLogo.pw.tsx index 12c2d8da12..ad6172d5f7 100644 --- a/ui/snippets/networkMenu/NetworkLogo.pw.tsx +++ b/ui/snippets/networkMenu/NetworkLogo.pw.tsx @@ -1,30 +1,24 @@ -import { test as base, expect } from '@playwright/experimental-ct-react'; import type { Locator } from '@playwright/test'; import React from 'react'; -import { buildExternalAssetFilePath } from 'configs/app/utils'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; -import * as app from 'playwright/utils/app'; +import { test, expect } from 'playwright/lib'; import * as configs from 'playwright/utils/configs'; import NetworkLogo from './NetworkLogo'; -base.describe('placeholder logo', () => { - const test = base.extend({ - context: contextWithEnvs([ - { name: 'NEXT_PUBLIC_NETWORK_LOGO', value: '' }, - { name: 'NEXT_PUBLIC_NETWORK_ICON', value: '' }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ]) as any, +const LOGO_URL = 'https://localhost:3000/my-logo.png'; +const ICON_URL = 'https://localhost:3000/my-icon.png'; + +test.describe('placeholder logo', () => { + test.beforeEach(async({ mockEnvs }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_NETWORK_LOGO', '' ], + [ 'NEXT_PUBLIC_NETWORK_ICON', '' ], + ]); }); - test('+@dark-mode', async({ mount }) => { - const component = await mount( - - - , - ); + test('+@dark-mode', async({ render }) => { + const component = await render(); await expect(component.locator('a')).toHaveScreenshot(); }); @@ -32,50 +26,25 @@ base.describe('placeholder logo', () => { test.describe('screen xl', () => { test.use({ viewport: configs.viewport.xl }); - test('+@dark-mode', async({ mount }) => { - const component = await mount( - - - , - ); + test('+@dark-mode', async({ render }) => { + const component = await render(); await expect(component.locator('a')).toHaveScreenshot(); }); }); }); -base.describe('custom logo', () => { - const LOGO_URL = app.url + buildExternalAssetFilePath('NEXT_PUBLIC_NETWORK_LOGO', 'https://localhost:3000/my-logo.png') || ''; - const ICON_URL = app.url + buildExternalAssetFilePath('NEXT_PUBLIC_NETWORK_ICON', 'https://localhost:3000/my-icon.png') || ''; - const test = base.extend({ - context: contextWithEnvs([ - { name: 'NEXT_PUBLIC_NETWORK_LOGO', value: LOGO_URL }, - { name: 'NEXT_PUBLIC_NETWORK_ICON', value: ICON_URL }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ]) as any, - }); - +test.describe('custom logo', () => { let component: Locator; - test.beforeEach(async({ page, mount }) => { - await page.route(LOGO_URL, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/network-logo.svg', - }); - }); - await page.route(ICON_URL, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_svg.svg', - }); - }); - - component = await mount( - - - , - ); + test.beforeEach(async({ render, mockConfigResponse, mockEnvs }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_NETWORK_LOGO', LOGO_URL ], + [ 'NEXT_PUBLIC_NETWORK_ICON', ICON_URL ], + ]); + await mockConfigResponse('NEXT_PUBLIC_NETWORK_LOGO', LOGO_URL, './playwright/mocks/network-logo.svg', true); + await mockConfigResponse('NEXT_PUBLIC_NETWORK_ICON', ICON_URL, './playwright/mocks/image_svg.svg', true); + component = await render(); }); test('+@dark-mode', async() => { @@ -91,54 +60,22 @@ base.describe('custom logo', () => { }); }); -base.describe('custom logo with dark option -@default +@dark-mode', () => { - const LOGO_URL = app.url + buildExternalAssetFilePath('NEXT_PUBLIC_NETWORK_LOGO', 'https://localhost:3000/my-logo.png') || ''; - const LOGO_URL_DARK = app.url + buildExternalAssetFilePath('NEXT_PUBLIC_NETWORK_LOGO_DARK', 'https://localhost:3000/my-logo.png') || ''; - const ICON_URL = app.url + buildExternalAssetFilePath('NEXT_PUBLIC_NETWORK_ICON', 'https://localhost:3000/my-icon.png') || ''; - const ICON_URL_DARK = app.url + buildExternalAssetFilePath('NEXT_PUBLIC_NETWORK_ICON_DARK', 'https://localhost:3000/my-icon.png') || ''; - const test = base.extend({ - context: contextWithEnvs([ - { name: 'NEXT_PUBLIC_NETWORK_LOGO', value: LOGO_URL }, - { name: 'NEXT_PUBLIC_NETWORK_LOGO_DARK', value: LOGO_URL_DARK }, - { name: 'NEXT_PUBLIC_NETWORK_ICON', value: ICON_URL }, - { name: 'NEXT_PUBLIC_NETWORK_ICON_DARK', value: ICON_URL_DARK }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ]) as any, - }); - +test.describe('custom logo with dark option -@default +@dark-mode', () => { let component: Locator; - test.beforeEach(async({ page, mount }) => { - await page.route(LOGO_URL, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_long.jpg', - }); - }); - await page.route(LOGO_URL_DARK, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_long.jpg', - }); - }); - await page.route(ICON_URL, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); - await page.route(ICON_URL_DARK, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); - - component = await mount( - - - , - ); + test.beforeEach(async({ render, mockConfigResponse, mockEnvs }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_NETWORK_LOGO', LOGO_URL ], + [ 'NEXT_PUBLIC_NETWORK_LOGO_DARK', LOGO_URL ], + [ 'NEXT_PUBLIC_NETWORK_ICON', ICON_URL ], + [ 'NEXT_PUBLIC_NETWORK_ICON_DARK', ICON_URL ], + ]); + await mockConfigResponse('NEXT_PUBLIC_NETWORK_LOGO', LOGO_URL, './playwright/mocks/image_long.jpg', true); + await mockConfigResponse('NEXT_PUBLIC_NETWORK_LOGO_DARK', LOGO_URL, './playwright/mocks/image_long.jpg', true); + await mockConfigResponse('NEXT_PUBLIC_NETWORK_ICON', ICON_URL, './playwright/mocks/image_s.jpg', true); + await mockConfigResponse('NEXT_PUBLIC_NETWORK_ICON_DARK', ICON_URL, './playwright/mocks/image_s.jpg', true); + + component = await render(); }); test('', async() => { diff --git a/ui/snippets/networkMenu/NetworkLogo.tsx b/ui/snippets/networkMenu/NetworkLogo.tsx index c89029e58a..11e50c6d95 100644 --- a/ui/snippets/networkMenu/NetworkLogo.tsx +++ b/ui/snippets/networkMenu/NetworkLogo.tsx @@ -1,5 +1,4 @@ -import type { StyleProps } from '@chakra-ui/react'; -import { Box, Image, useColorModeValue, Skeleton } from '@chakra-ui/react'; +import { Box, Image, useColorModeValue, Skeleton, chakra } from '@chakra-ui/react'; import React from 'react'; import { route } from 'nextjs-routes'; @@ -10,10 +9,10 @@ import IconSvg from 'ui/shared/IconSvg'; interface Props { isCollapsed?: boolean; onClick?: (event: React.SyntheticEvent) => void; - imageProps?: StyleProps; + className?: string; } -const LogoFallback = ({ isCollapsed, isSmall, imageProps }: { isCollapsed?: boolean; isSmall?: boolean; imageProps?: StyleProps }) => { +const LogoFallback = ({ isCollapsed, isSmall }: { isCollapsed?: boolean; isSmall?: boolean }) => { const field = isSmall ? 'icon' : 'logo'; const logoColor = useColorModeValue('blue.600', 'white'); @@ -38,12 +37,11 @@ const LogoFallback = ({ isCollapsed, isSmall, imageProps }: { isCollapsed?: bool height="100%" color={ logoColor } display={ display } - { ...imageProps } /> ); }; -const NetworkLogo = ({ isCollapsed, onClick, imageProps }: Props) => { +const NetworkLogo = ({ isCollapsed, onClick, className }: Props) => { const logoSrc = useColorModeValue(config.UI.sidebar.logo.default, config.UI.sidebar.logo.dark || config.UI.sidebar.logo.default); const iconSrc = useColorModeValue(config.UI.sidebar.icon.default, config.UI.sidebar.icon.dark || config.UI.sidebar.icon.default); @@ -53,6 +51,7 @@ const NetworkLogo = ({ isCollapsed, onClick, imageProps }: Props) => { return ( { h="100%" src={ logoSrc } alt={ `${ config.chain.name } network logo` } - fallback={ } + fallback={ } display={{ base: 'block', lg: isCollapsed === false ? 'block' : 'none', xl: isCollapsed ? 'none' : 'block' }} style={ logoStyle } - { ...imageProps } /> { /* small logo */ } { h="100%" src={ iconSrc } alt={ `${ config.chain.name } network logo` } - fallback={ } + fallback={ } display={{ base: 'none', lg: isCollapsed === false ? 'none' : 'block', xl: isCollapsed ? 'block' : 'none' }} style={ iconStyle } - { ...imageProps } /> ); }; -export default React.memo(NetworkLogo); +export default React.memo(chakra(NetworkLogo)); diff --git a/ui/snippets/networkMenu/NetworkMenu.pw.tsx b/ui/snippets/networkMenu/NetworkMenu.pw.tsx index 808f198c2d..71159ec0f5 100644 --- a/ui/snippets/networkMenu/NetworkMenu.pw.tsx +++ b/ui/snippets/networkMenu/NetworkMenu.pw.tsx @@ -1,45 +1,23 @@ -import { test, expect } from '@playwright/experimental-ct-react'; import React from 'react'; -import { buildExternalAssetFilePath } from 'configs/app/utils'; import { FEATURED_NETWORKS_MOCK } from 'mocks/config/network'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; -import * as app from 'playwright/utils/app'; +import { test, expect } from 'playwright/lib'; import NetworkMenu from './NetworkMenu'; -const FEATURED_NETWORKS_URL = app.url + buildExternalAssetFilePath('NEXT_PUBLIC_FEATURED_NETWORKS', 'https://localhost:3000/featured-networks.json') || ''; +const FEATURED_NETWORKS_URL = 'https://localhost:3000/featured-networks.json'; +const LOGO_URL = 'https://localhost:3000/my-logo.png'; -const extendedTest = test.extend({ - context: contextWithEnvs([ - { name: 'NEXT_PUBLIC_FEATURED_NETWORKS', value: FEATURED_NETWORKS_URL }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ]) as any, -}); +test.use({ viewport: { width: 1600, height: 1000 } }); + +test('base view +@dark-mode', async({ render, page, mockConfigResponse, mockAssetResponse, mockEnvs }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_FEATURED_NETWORKS', FEATURED_NETWORKS_URL ], + ]); + await mockConfigResponse('NEXT_PUBLIC_FEATURED_NETWORKS', FEATURED_NETWORKS_URL, FEATURED_NETWORKS_MOCK); + await mockAssetResponse(LOGO_URL, './playwright/mocks/image_s.jpg'); -extendedTest.use({ viewport: { width: 1600, height: 1000 } }); - -extendedTest('base view +@dark-mode', async({ mount, page }) => { - const LOGO_URL = 'https://localhost:3000/my-logo.png'; - await page.route(LOGO_URL, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); - await page.route(FEATURED_NETWORKS_URL, (route) => { - return route.fulfill({ - status: 200, - body: FEATURED_NETWORKS_MOCK, - }); - }); - - const component = await mount( - - - , - ); + const component = await render(); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 36, height: 36 } }); diff --git a/ui/snippets/networkMenu/NetworkMenuContentMobile.tsx b/ui/snippets/networkMenu/NetworkMenuContentMobile.tsx index aa56ba83f6..2e1b5e454f 100644 --- a/ui/snippets/networkMenu/NetworkMenuContentMobile.tsx +++ b/ui/snippets/networkMenu/NetworkMenuContentMobile.tsx @@ -45,10 +45,12 @@ const NetworkMenuContentMobile = ({ items, tabs }: Props) => { ) : ( <> - - + { tabs.length > 1 && ( + + ) } + { items .filter(({ group }) => group === selectedTab) .map((network) => ( diff --git a/ui/snippets/profileMenu/ProfileMenuContent.tsx b/ui/snippets/profileMenu/ProfileMenuContent.tsx index 602fe2ed07..bfbea08981 100644 --- a/ui/snippets/profileMenu/ProfileMenuContent.tsx +++ b/ui/snippets/profileMenu/ProfileMenuContent.tsx @@ -1,4 +1,4 @@ -import { Box, Button, Text, VStack, useColorModeValue } from '@chakra-ui/react'; +import { Box, Button, VStack, chakra } from '@chakra-ui/react'; import React from 'react'; import type { UserInfo } from 'types/api/account'; @@ -18,7 +18,6 @@ type Props = { const ProfileMenuContent = ({ data, onNavLinkClick }: Props) => { const { accountNavItems, profileItem } = useNavItems(); - const primaryTextColor = useColorModeValue('blackAlpha.800', 'whiteAlpha.800'); const handleSingOutClick = React.useCallback(() => { mixpanel.logEvent( @@ -32,37 +31,28 @@ const ProfileMenuContent = ({ data, onNavLinkClick }: Props) => { return null; } + const userName = data?.email || data?.nickname || data?.name; + return ( - { (data?.name || data?.nickname) && ( - - Signed in as { data.name || data.nickname } - - ) } - { data?.email && ( - - { data.email } - + Signed in as + { userName } + ) } - + { accountNavItems.map((item) => ( { +test('no auth', async({ render, page }) => { const hooksConfig = { router: { asPath: '/', pathname: '/', }, }; - const component = await mount( - - - , - { hooksConfig }, - ); - + const component = await render(, { hooksConfig }); await component.locator('a').click(); - expect(page.url()).toBe(`${ app.url }/auth/auth0?path=%2F`); -}); -test.describe('auth', () => { - const extendedTest = test.extend({ - context: ({ context }, use) => { - authFixture(context); - use(context); - }, - }); + expect(page.url()).toBe(`${ config.app.baseUrl }/auth/auth0?path=%2F`); +}); - extendedTest('+@dark-mode', async({ mount, page }) => { - await page.route(buildApiUrl('user_info'), (route) => route.fulfill({ - status: 200, - body: JSON.stringify(profileMock.base), - })); - await page.route(profileMock.base.avatar, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); +const authTest = test.extend<{ context: BrowserContext }>({ + context: contextWithAuth, +}); +authTest('auth +@dark-mode', async({ render, page, mockApiResponse, mockAssetResponse }) => { + await mockApiResponse('user_info', profileMock.base); + await mockAssetResponse(profileMock.base.avatar, './playwright/mocks/image_s.jpg'); - const component = await mount( - - - , - ); + const component = await render(); + await component.getByAltText(/Profile picture/i).click(); - await component.getByAltText(/Profile picture/i).click(); - await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 250, height: 600 } }); - }); + await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 250, height: 600 } }); }); diff --git a/ui/snippets/profileMenu/ProfileMenuDesktop.tsx b/ui/snippets/profileMenu/ProfileMenuDesktop.tsx index 90e445ec47..32d9c54442 100644 --- a/ui/snippets/profileMenu/ProfileMenuDesktop.tsx +++ b/ui/snippets/profileMenu/ProfileMenuDesktop.tsx @@ -1,5 +1,5 @@ import type { IconButtonProps } from '@chakra-ui/react'; -import { Popover, PopoverContent, PopoverBody, PopoverTrigger, IconButton, Tooltip, Box } from '@chakra-ui/react'; +import { Popover, PopoverContent, PopoverBody, PopoverTrigger, IconButton, Tooltip, Box, chakra } from '@chakra-ui/react'; import React from 'react'; import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo'; @@ -12,9 +12,11 @@ import useMenuButtonColors from '../useMenuButtonColors'; type Props = { isHomePage?: boolean; + className?: string; + fallbackIconSize?: number; }; -const ProfileMenuDesktop = ({ isHomePage }: Props) => { +const ProfileMenuDesktop = ({ isHomePage, className, fallbackIconSize }: Props) => { const { data, error, isPending } = useFetchProfileInfo(); const loginUrl = useLoginUrl(); const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors(); @@ -81,8 +83,9 @@ const ProfileMenuDesktop = ({ isHomePage }: Props) => { } + icon={ } variant={ variant } colorScheme="blue" boxSize="40px" @@ -94,7 +97,7 @@ const ProfileMenuDesktop = ({ isHomePage }: Props) => { { hasMenu && ( - + @@ -104,4 +107,4 @@ const ProfileMenuDesktop = ({ isHomePage }: Props) => { ); }; -export default ProfileMenuDesktop; +export default chakra(ProfileMenuDesktop); diff --git a/ui/snippets/profileMenu/ProfileMenuMobile.pw.tsx b/ui/snippets/profileMenu/ProfileMenuMobile.pw.tsx index b5d3fcc373..0867bf2bd8 100644 --- a/ui/snippets/profileMenu/ProfileMenuMobile.pw.tsx +++ b/ui/snippets/profileMenu/ProfileMenuMobile.pw.tsx @@ -1,61 +1,40 @@ -import { test, expect, devices } from '@playwright/experimental-ct-react'; +import type { BrowserContext } from '@playwright/test'; import React from 'react'; +import config from 'configs/app'; import * as profileMock from 'mocks/user/profile'; -import authFixture from 'playwright/fixtures/auth'; -import TestApp from 'playwright/TestApp'; -import * as app from 'playwright/utils/app'; -import buildApiUrl from 'playwright/utils/buildApiUrl'; +import { contextWithAuth } from 'playwright/fixtures/auth'; +import { test, expect, devices } from 'playwright/lib'; import ProfileMenuMobile from './ProfileMenuMobile'; -test('no auth', async({ mount, page }) => { +test('no auth', async({ render, page }) => { const hooksConfig = { router: { asPath: '/', pathname: '/', }, }; - const component = await mount( - - - , - { hooksConfig }, - ); - + const component = await render(, { hooksConfig }); await component.locator('a').click(); - expect(page.url()).toBe(`${ app.url }/auth/auth0?path=%2F`); + + expect(page.url()).toBe(`${ config.app.baseUrl }/auth/auth0?path=%2F`); }); test.use({ viewport: devices['iPhone 13 Pro'].viewport }); -test.describe('auth', () => { - const extendedTest = test.extend({ - context: ({ context }, use) => { - authFixture(context); - use(context); - }, - }); +const authTest = test.extend<{ context: BrowserContext }>({ + context: contextWithAuth, +}); - extendedTest('base view', async({ mount, page }) => { - await page.route(buildApiUrl('user_info'), (route) => route.fulfill({ - status: 200, - body: JSON.stringify(profileMock.base), - })); - await page.route(profileMock.base.avatar, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); - - const component = await mount( - - - , - ); +authTest.describe('auth', () => { + authTest('base view', async({ render, page, mockApiResponse, mockAssetResponse }) => { + await mockApiResponse('user_info', profileMock.base); + await mockAssetResponse(profileMock.base.avatar, './playwright/mocks/image_s.jpg'); + const component = await render(); await component.getByAltText(/Profile picture/i).click(); + await expect(page).toHaveScreenshot(); }); }); diff --git a/ui/snippets/profileMenu/ProfileMenuMobile.tsx b/ui/snippets/profileMenu/ProfileMenuMobile.tsx index 0a1b97eda6..b50367595b 100644 --- a/ui/snippets/profileMenu/ProfileMenuMobile.tsx +++ b/ui/snippets/profileMenu/ProfileMenuMobile.tsx @@ -66,7 +66,7 @@ const ProfileMenuMobile = () => { autoFocus={ false } > - + diff --git a/ui/snippets/profileMenu/__screenshots__/ProfileMenuDesktop.pw.tsx_dark-color-mode_auth-dark-mode-1.png b/ui/snippets/profileMenu/__screenshots__/ProfileMenuDesktop.pw.tsx_dark-color-mode_auth-dark-mode-1.png index 430e738c37..ed2f6a4e4f 100644 Binary files a/ui/snippets/profileMenu/__screenshots__/ProfileMenuDesktop.pw.tsx_dark-color-mode_auth-dark-mode-1.png and b/ui/snippets/profileMenu/__screenshots__/ProfileMenuDesktop.pw.tsx_dark-color-mode_auth-dark-mode-1.png differ diff --git a/ui/snippets/profileMenu/__screenshots__/ProfileMenuDesktop.pw.tsx_default_auth-dark-mode-1.png b/ui/snippets/profileMenu/__screenshots__/ProfileMenuDesktop.pw.tsx_default_auth-dark-mode-1.png index b4ae10b93c..351421ee0f 100644 Binary files a/ui/snippets/profileMenu/__screenshots__/ProfileMenuDesktop.pw.tsx_default_auth-dark-mode-1.png and b/ui/snippets/profileMenu/__screenshots__/ProfileMenuDesktop.pw.tsx_default_auth-dark-mode-1.png differ diff --git a/ui/snippets/profileMenu/__screenshots__/ProfileMenuMobile.pw.tsx_default_auth-base-view-1.png b/ui/snippets/profileMenu/__screenshots__/ProfileMenuMobile.pw.tsx_default_auth-base-view-1.png index b5ad715272..aa5b64facf 100644 Binary files a/ui/snippets/profileMenu/__screenshots__/ProfileMenuMobile.pw.tsx_default_auth-base-view-1.png and b/ui/snippets/profileMenu/__screenshots__/ProfileMenuMobile.pw.tsx_default_auth-base-view-1.png differ diff --git a/ui/snippets/searchBar/SearchBar.pw.tsx b/ui/snippets/searchBar/SearchBar.pw.tsx index 36d78ba514..895b948153 100644 --- a/ui/snippets/searchBar/SearchBar.pw.tsx +++ b/ui/snippets/searchBar/SearchBar.pw.tsx @@ -1,302 +1,191 @@ -import { test as base, expect } from '@playwright/experimental-ct-react'; import React from 'react'; -import { buildExternalAssetFilePath } from 'configs/app/utils'; -import * as textAdMock from 'mocks/ad/textAd'; import { apps as appsMock } from 'mocks/apps/apps'; import * as searchMock from 'mocks/search/index'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; -import * as app from 'playwright/utils/app'; -import buildApiUrl from 'playwright/utils/buildApiUrl'; -import * as configs from 'playwright/utils/configs'; +import { test, expect } from 'playwright/lib'; import SearchBar from './SearchBar'; -const test = base.extend({ - context: contextWithEnvs([ - { name: 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', value: '' }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ]) as any, +test.beforeEach(async({ mockAssetResponse, mockEnvs, mockTextAd }) => { + await mockTextAd(); + await mockAssetResponse(searchMock.token1.icon_url as string, './playwright/mocks/image_s.jpg'); + await mockEnvs([ + [ 'NEXT_PUBLIC_MARKETPLACE_ENABLED', 'false' ], + ]); }); -test.beforeEach(async({ page }) => { - await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ - status: 200, - body: JSON.stringify(textAdMock.duck), - })); - await page.route(textAdMock.duck.ad.thumbnail, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); - await page.route(searchMock.token1.icon_url as string, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); -}); - -test('search by token name +@mobile +@dark-mode', async({ mount, page }) => { - const API_URL = buildApiUrl('quick_search') + '?q=o'; - await page.route(API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify([ - searchMock.token1, - searchMock.token2, - ]), - })); - - await mount( - - - , - ); - await page.getByPlaceholder(/search/i).type('o'); - await page.waitForResponse(API_URL); +test('search by token name +@mobile +@dark-mode', async({ render, page, mockApiResponse }) => { + const apiUrl = await mockApiResponse('quick_search', [ + searchMock.token1, + searchMock.token2, + ], { queryParams: { q: 'o' } }); + await render(); + await page.getByPlaceholder(/search/i).fill('o'); + await page.waitForResponse(apiUrl); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 500 } }); }); -test('search by contract name +@mobile +@dark-mode', async({ mount, page }) => { - const API_URL = buildApiUrl('quick_search') + '?q=o'; - await page.route(API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify([ - searchMock.contract1, - searchMock.address2, - ]), - })); - - await mount( - - - , - ); - await page.getByPlaceholder(/search/i).type('o'); - await page.waitForResponse(API_URL); +test('search by contract name +@mobile +@dark-mode', async({ render, page, mockApiResponse }) => { + const apiUrl = await mockApiResponse('quick_search', [ + searchMock.contract1, + searchMock.address2, + ], { queryParams: { q: 'o' } }); + + await render(); + await page.getByPlaceholder(/search/i).fill('o'); + await page.waitForResponse(apiUrl); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 500 } }); }); -test('search by name homepage +@dark-mode', async({ mount, page }) => { - const API_URL = buildApiUrl('quick_search') + '?q=o'; - await page.route(API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify([ - searchMock.token1, - searchMock.token2, - searchMock.contract1, - ]), - })); - - await mount( - - - , - ); - await page.getByPlaceholder(/search/i).type('o'); - await page.waitForResponse(API_URL); +test('search by name homepage +@dark-mode', async({ render, page, mockApiResponse }) => { + const apiUrl = await mockApiResponse('quick_search', [ + searchMock.token1, + searchMock.token2, + searchMock.contract1, + ], { queryParams: { q: 'o' } }); + await render(); + await page.getByPlaceholder(/search/i).fill('o'); + await page.waitForResponse(apiUrl); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 500 } }); }); -test('search by tag +@mobile +@dark-mode', async({ mount, page }) => { - const API_URL = buildApiUrl('quick_search') + '?q=o'; - await page.route(API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify([ - searchMock.label1, - ]), - })); - - await mount( - - - , - ); - await page.getByPlaceholder(/search/i).type('o'); - await page.waitForResponse(API_URL); +test('search by tag +@mobile +@dark-mode', async({ render, page, mockApiResponse }) => { + const apiUrl = await mockApiResponse('quick_search', [ + searchMock.label1, + ], { queryParams: { q: 'o' } }); + await render(); + await page.getByPlaceholder(/search/i).fill('o'); + await page.waitForResponse(apiUrl); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 500 } }); }); -test('search by address hash +@mobile', async({ mount, page }) => { - const API_URL = buildApiUrl('quick_search') + `?q=${ searchMock.address1.address }`; - await page.route(API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify([ - searchMock.address1, - ]), - })); - - await mount( - - - , - ); - await page.getByPlaceholder(/search/i).type(searchMock.address1.address); - await page.waitForResponse(API_URL); +test('search by address hash +@mobile', async({ render, page, mockApiResponse }) => { + const apiUrl = await mockApiResponse('quick_search', [ + searchMock.address1, + ], { queryParams: { q: searchMock.address1.address } }); + await render(); + await page.getByPlaceholder(/search/i).fill(searchMock.address1.address); + await page.waitForResponse(apiUrl); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 300 } }); }); -test('search by block number +@mobile', async({ mount, page }) => { - const API_URL = buildApiUrl('quick_search') + `?q=${ searchMock.block1.block_number }`; - await page.route(API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify([ - searchMock.block1, - searchMock.block2, - searchMock.block3, - ]), - })); - - await mount( - - - , - ); - await page.getByPlaceholder(/search/i).type(String(searchMock.block1.block_number)); - await page.waitForResponse(API_URL); +test('search by block number +@mobile', async({ render, page, mockApiResponse }) => { + const apiUrl = await mockApiResponse('quick_search', [ + searchMock.block1, + searchMock.block2, + searchMock.block3, + ], { queryParams: { q: searchMock.block1.block_number } }); + await render(); + await page.getByPlaceholder(/search/i).fill(String(searchMock.block1.block_number)); + await page.waitForResponse(apiUrl); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 600 } }); }); -test('search by block hash +@mobile', async({ mount, page }) => { - const API_URL = buildApiUrl('quick_search') + `?q=${ searchMock.block1.block_hash }`; - await page.route(API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify([ - searchMock.block1, - ]), - })); - - await mount( - - - , - ); - await page.getByPlaceholder(/search/i).type(searchMock.block1.block_hash); - await page.waitForResponse(API_URL); +test('search by block hash +@mobile', async({ render, page, mockApiResponse }) => { + const apiUrl = await mockApiResponse('quick_search', [ + searchMock.block1, + ], { queryParams: { q: searchMock.block1.block_hash } }); + await render(); + await page.getByPlaceholder(/search/i).fill(searchMock.block1.block_hash); + await page.waitForResponse(apiUrl); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 300 } }); }); -test('search by tx hash +@mobile', async({ mount, page }) => { - const API_URL = buildApiUrl('quick_search') + `?q=${ searchMock.tx1.tx_hash }`; - await page.route(API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify([ - searchMock.tx1, - ]), - })); - - await mount( - - - , - ); - await page.getByPlaceholder(/search/i).type(searchMock.tx1.tx_hash); - await page.waitForResponse(API_URL); +test('search by tx hash +@mobile', async({ render, page, mockApiResponse }) => { + const apiUrl = await mockApiResponse('quick_search', [ + searchMock.tx1, + ], { queryParams: { q: searchMock.tx1.tx_hash } }); + await render(); + await page.getByPlaceholder(/search/i).fill(searchMock.tx1.tx_hash); + await page.waitForResponse(apiUrl); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 300 } }); }); -const testWithUserOps = base.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.featureEnvs.userOps) as any, +test('search by blob hash +@mobile', async({ render, page, mockApiResponse }) => { + const apiUrl = await mockApiResponse('quick_search', [ + searchMock.blob1, + ], { queryParams: { q: searchMock.blob1.blob_hash } }); + await render(); + await page.getByPlaceholder(/search/i).fill(searchMock.blob1.blob_hash); + await page.waitForResponse(apiUrl); + + await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 300 } }); }); -testWithUserOps('search by user op hash +@mobile', async({ mount, page }) => { - await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ - status: 200, - body: JSON.stringify(textAdMock.duck), - })); - const API_URL = buildApiUrl('quick_search') + `?q=${ searchMock.tx1.tx_hash }`; - await page.route(API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify([ - searchMock.userOp1, - ]), - })); - - await mount( - - - , - ); - await page.getByPlaceholder(/search/i).type(searchMock.tx1.tx_hash); - await page.waitForResponse(API_URL); +test('search by domain name +@mobile', async({ render, page, mockApiResponse }) => { + const apiUrl = await mockApiResponse('quick_search', [ + searchMock.domain1, + ], { queryParams: { q: searchMock.domain1.ens_info.name } }); + + await render(); + await page.getByPlaceholder(/search/i).fill(searchMock.domain1.ens_info.name); + await page.waitForResponse(apiUrl); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 300 } }); }); -test('search with view all link', async({ mount, page }) => { - const API_URL = buildApiUrl('quick_search') + '?q=o'; - await page.route(API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify([ - searchMock.token1, - searchMock.token2, - searchMock.contract1, - ...Array(47).fill(searchMock.contract1), - ]), - })); +test('search by user op hash +@mobile', async({ render, page, mockApiResponse, mockEnvs }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_HAS_USER_OPS', 'true' ], + ]); + const apiUrl = await mockApiResponse('quick_search', [ + searchMock.userOp1, + ], { queryParams: { q: searchMock.tx1.tx_hash } }); + await render(); + await page.getByPlaceholder(/search/i).fill(searchMock.tx1.tx_hash); + await page.waitForResponse(apiUrl); - await mount( - - - , - ); - await page.getByPlaceholder(/search/i).type('o'); + await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 300 } }); +}); - await page.waitForResponse(API_URL); +test('search with view all link', async({ render, page, mockApiResponse }) => { + const apiUrl = await mockApiResponse('quick_search', [ + searchMock.token1, + searchMock.token2, + searchMock.contract1, + ...Array(47).fill(searchMock.contract1), + ], { queryParams: { q: 'o' } }); + await render(); + await page.getByPlaceholder(/search/i).fill('o'); + await page.waitForResponse(apiUrl); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 500 } }); }); -test('scroll suggest to category', async({ mount, page }) => { - const API_URL = buildApiUrl('quick_search') + '?q=o'; - await page.route(API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify([ - searchMock.token1, - searchMock.token2, - searchMock.contract1, - searchMock.token1, - searchMock.token2, - searchMock.contract1, - searchMock.token1, - searchMock.token2, - searchMock.contract1, - searchMock.token1, - searchMock.token2, - searchMock.contract1, - ]), - })); - - await mount( - - - , - ); - await page.getByPlaceholder(/search/i).type('o'); - await page.waitForResponse(API_URL); +test('scroll suggest to category', async({ render, page, mockApiResponse }) => { + const apiUrl = await mockApiResponse('quick_search', [ + searchMock.token1, + searchMock.token2, + searchMock.contract1, + searchMock.token1, + searchMock.token2, + searchMock.contract1, + searchMock.token1, + searchMock.token2, + searchMock.contract1, + searchMock.token1, + searchMock.token2, + searchMock.contract1, + ], { queryParams: { q: 'o' } }); + await render(); + await page.getByPlaceholder(/search/i).fill('o'); + await page.waitForResponse(apiUrl); await page.getByRole('tab', { name: 'Addresses' }).click(); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 500 } }); }); -test('recent keywords suggest +@mobile', async({ mount, page }) => { - await mount( - - - , - ); +test('recent keywords suggest +@mobile', async({ render, page }) => { + await render(); // eslint-disable-next-line max-len await page.evaluate(() => window.localStorage.setItem('recent_search_keywords', '["10x2d311959270e0bbdc1fc7bc6dbd8ad645c4dd8d6aa32f5f89d54629a924f112b","0x1d311959270e0bbdc1fc7bc6dbd8ad645c4dd8d6aa32f5f89d54629a924f112b","usd","bob"]')); await page.getByPlaceholder(/search/i).click(); @@ -304,50 +193,24 @@ test('recent keywords suggest +@mobile', async({ mount, page }) => { await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 500 } }); }); -base.describe('with apps', () => { - const MARKETPLACE_CONFIG_URL = app.url + buildExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', 'https://marketplace-config.json') || ''; - const test = base.extend({ - context: contextWithEnvs([ - { name: 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', value: MARKETPLACE_CONFIG_URL }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ]) as any, - }); +test.describe('with apps', () => { + const MARKETPLACE_CONFIG_URL = 'https://marketplace-config.json'; - test('default view +@mobile', async({ mount, page }) => { - const API_URL = buildApiUrl('quick_search') + '?q=o'; - await page.route(API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify([ - searchMock.token1, - ]), - })); - - await page.route(MARKETPLACE_CONFIG_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(appsMock), - })); - - await page.route(appsMock[0].logo, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); - await page.route(appsMock[1].logo as string, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); - - await mount( - - - , - ); - await page.getByPlaceholder(/search/i).type('o'); - - await page.waitForResponse(API_URL); + test('default view +@mobile', async({ render, page, mockApiResponse, mockConfigResponse, mockAssetResponse, mockEnvs }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_MARKETPLACE_ENABLED', 'true' ], + [ 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL ], + ]); + const apiUrl = await mockApiResponse('quick_search', [ + searchMock.token1, + ], { queryParams: { q: 'o' } }); + await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL, JSON.stringify(appsMock)); + await mockAssetResponse(appsMock[0].logo, './playwright/mocks/image_s.jpg'); + await mockAssetResponse(appsMock[1].logo, './playwright/mocks/image_s.jpg'); + + await render(); + await page.getByPlaceholder(/search/i).fill('o'); + await page.waitForResponse(apiUrl); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 500 } }); }); diff --git a/ui/snippets/searchBar/SearchBar.tsx b/ui/snippets/searchBar/SearchBar.tsx index 362f2e7639..9ceee88101 100644 --- a/ui/snippets/searchBar/SearchBar.tsx +++ b/ui/snippets/searchBar/SearchBar.tsx @@ -1,4 +1,14 @@ -import { Box, Portal, Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure, PopoverFooter, useOutsideClick } from '@chakra-ui/react'; +import { + Box, + Portal, + Popover, + PopoverTrigger, + PopoverContent, + PopoverBody, + PopoverFooter, + useDisclosure, + useOutsideClick, +} from '@chakra-ui/react'; import _debounce from 'lodash/debounce'; import { useRouter } from 'next/router'; import type { FormEvent } from 'react'; @@ -12,6 +22,7 @@ import * as mixpanel from 'lib/mixpanel/index'; import { getRecentSearchKeywords, saveToRecentKeywords } from 'lib/recentSearchKeywords'; import LinkInternal from 'ui/shared/LinkInternal'; +import SearchBarBackdrop from './SearchBarBackdrop'; import SearchBarInput from './SearchBarInput'; import SearchBarRecentKeywords from './SearchBarRecentKeywords'; import SearchBarSuggest from './SearchBarSuggest/SearchBarSuggest'; @@ -106,69 +117,73 @@ const SearchBar = ({ isHomepage }: Props) => { }, [ calculateMenuWidth ]); return ( - 0 || recentSearchKeywords.length > 0) } - autoFocus={ false } - onClose={ onClose } - placement="bottom-start" - offset={ isMobile && !isHomepage ? [ 16, -4 ] : undefined } - isLazy - > - - - - - - + 0 || recentSearchKeywords.length > 0) } + autoFocus={ false } + onClose={ onClose } + placement="bottom-start" + offset={ isMobile && !isHomepage ? [ 16, -4 ] : undefined } + isLazy + > + + + + + - - { searchTerm.trim().length === 0 && recentSearchKeywords.length > 0 && ( - - ) } - { searchTerm.trim().length > 0 && ( - - ) } - - - { searchTerm.trim().length > 0 && query.data && query.data.length >= 50 && ( - - - View all results - - - ) } - - - + { searchTerm.trim().length === 0 && recentSearchKeywords.length > 0 && ( + + ) } + { searchTerm.trim().length > 0 && ( + + ) } + + + { searchTerm.trim().length > 0 && query.data && query.data.length >= 50 && ( + + + View all results + + + ) } + + + + + ); }; diff --git a/ui/snippets/searchBar/SearchBarBackdrop.tsx b/ui/snippets/searchBar/SearchBarBackdrop.tsx new file mode 100644 index 0000000000..05d2702fe8 --- /dev/null +++ b/ui/snippets/searchBar/SearchBarBackdrop.tsx @@ -0,0 +1,25 @@ +import { Box, useColorModeValue } from '@chakra-ui/react'; +import React from 'react'; + +interface Props { + isOpen: boolean; +} + +const SearchBarBackdrop = ({ isOpen }: Props) => { + const backdropBgColor = useColorModeValue('blackAlpha.400', 'blackAlpha.600'); + + return ( + + ); +}; + +export default React.memo(SearchBarBackdrop); diff --git a/ui/snippets/searchBar/SearchBarInput.tsx b/ui/snippets/searchBar/SearchBarInput.tsx index d1eb066474..d4088e06a8 100644 --- a/ui/snippets/searchBar/SearchBarInput.tsx +++ b/ui/snippets/searchBar/SearchBarInput.tsx @@ -16,10 +16,14 @@ interface Props { onHide?: () => void; onClear: () => void; isHomepage?: boolean; + isSuggestOpen?: boolean; value: string; } -const SearchBarInput = ({ onChange, onSubmit, isHomepage, onFocus, onBlur, onHide, onClear, value }: Props, ref: React.ForwardedRef) => { +const SearchBarInput = ( + { onChange, onSubmit, isHomepage, isSuggestOpen, onFocus, onBlur, onHide, onClear, value }: Props, + ref: React.ForwardedRef, +) => { const innerRef = React.useRef(null); React.useImperativeHandle(ref, () => innerRef.current as HTMLFormElement, []); const [ isSticky, setIsSticky ] = React.useState(false); @@ -71,10 +75,10 @@ const SearchBarInput = ({ onChange, onSubmit, isHomepage, onFocus, onBlur, onHid w="100%" backgroundColor={ bgColor } borderRadius={{ base: isHomepage ? 'base' : 'none', lg: 'base' }} - position={{ base: isHomepage ? 'static' : 'absolute', lg: 'static' }} + position={{ base: isHomepage ? 'static' : 'absolute', lg: 'relative' }} top={{ base: isHomepage ? 0 : 55, lg: 0 }} left="0" - zIndex={{ base: isHomepage ? 'auto' : '-1', lg: 'auto' }} + zIndex={{ base: isHomepage ? 'auto' : '-1', lg: isSuggestOpen ? 'popover' : 'auto' }} paddingX={{ base: isHomepage ? 0 : 4, lg: 0 }} paddingTop={{ base: isHomepage ? 0 : 1, lg: 0 }} paddingBottom={{ base: isHomepage ? 0 : 2, lg: 0 }} diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestBlob.tsx b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestBlob.tsx new file mode 100644 index 0000000000..6c852d9b7c --- /dev/null +++ b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestBlob.tsx @@ -0,0 +1,25 @@ +import { chakra, Flex } from '@chakra-ui/react'; +import React from 'react'; + +import type { SearchResultBlob } from 'types/api/search'; + +import * as BlobEntity from 'ui/shared/entities/blob/BlobEntity'; +import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; + +interface Props { + data: SearchResultBlob; + searchTerm: string; +} + +const SearchBarSuggestBlob = ({ data }: Props) => { + return ( + + + + + + + ); +}; + +export default React.memo(SearchBarSuggestBlob); diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestDomain.tsx b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestDomain.tsx new file mode 100644 index 0000000000..2ec60ccc8d --- /dev/null +++ b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestDomain.tsx @@ -0,0 +1,82 @@ +import { Grid, Text, Flex } from '@chakra-ui/react'; +import React from 'react'; + +import type { SearchResultDomain } from 'types/api/search'; + +import dayjs from 'lib/date/dayjs'; +import highlightText from 'lib/highlightText'; +import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; +import IconSvg from 'ui/shared/IconSvg'; + +interface Props { + data: SearchResultDomain; + isMobile: boolean | undefined; + searchTerm: string; +} + +const SearchBarSuggestDomain = ({ data, isMobile, searchTerm }: Props) => { + const icon = ; + + const name = ( + + + + ); + + const address = ( + + + + ); + + const isContractVerified = data.is_smart_contract_verified && ; + + const expiresText = data.ens_info?.expiry_date ? ` expires ${ dayjs(data.ens_info.expiry_date).fromNow() }` : ''; + const ensNamesCount = data?.ens_info.names_count > 39 ? '40+' : `+${ data.ens_info.names_count - 1 }`; + const additionalInfo = ( + + { data?.ens_info.names_count > 1 ? ensNamesCount : expiresText } + + ); + + if (isMobile) { + return ( + <> + + { icon } + { name } + + + { address } + { isContractVerified } + + { additionalInfo } + + ); + } + + return ( + + + { icon } + { name } + + + { address } + { isContractVerified } + + { additionalInfo } + + ); +}; + +export default React.memo(SearchBarSuggestDomain); diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestItem.tsx b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestItem.tsx index e6dde430ab..b0eb20041d 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestItem.tsx +++ b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestItem.tsx @@ -7,7 +7,9 @@ import type { SearchResultItem } from 'types/api/search'; import { route } from 'nextjs-routes'; import SearchBarSuggestAddress from './SearchBarSuggestAddress'; +import SearchBarSuggestBlob from './SearchBarSuggestBlob'; import SearchBarSuggestBlock from './SearchBarSuggestBlock'; +import SearchBarSuggestDomain from './SearchBarSuggestDomain'; import SearchBarSuggestItemLink from './SearchBarSuggestItemLink'; import SearchBarSuggestLabel from './SearchBarSuggestLabel'; import SearchBarSuggestToken from './SearchBarSuggestToken'; @@ -42,6 +44,12 @@ const SearchBarSuggestItem = ({ data, isMobile, searchTerm, onClick }: Props) => case 'user_operation': { return route({ pathname: '/op/[hash]', query: { hash: data.user_operation_hash } }); } + case 'blob': { + return route({ pathname: '/blobs/[hash]', query: { hash: data.blob_hash } }); + } + case 'ens_domain': { + return route({ pathname: '/address/[hash]', query: { hash: data.address } }); + } } })(); @@ -67,6 +75,12 @@ const SearchBarSuggestItem = ({ data, isMobile, searchTerm, onClick }: Props) => case 'user_operation': { return ; } + case 'blob': { + return ; + } + case 'ens_domain': { + return ; + } } })(); diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_recent-keywords-suggest-mobile-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_recent-keywords-suggest-mobile-1.png index 9cfc914b85..4bf76fee17 100644 Binary files a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_recent-keywords-suggest-mobile-1.png and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_recent-keywords-suggest-mobile-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_scroll-suggest-to-category-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_scroll-suggest-to-category-1.png index 9017b6762c..97875b4515 100644 Binary files a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_scroll-suggest-to-category-1.png and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_scroll-suggest-to-category-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-address-hash-mobile-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-address-hash-mobile-1.png index 5943a7308a..cb71b52d02 100644 Binary files a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-address-hash-mobile-1.png and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-address-hash-mobile-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-blob-hash-mobile-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-blob-hash-mobile-1.png new file mode 100644 index 0000000000..79f6fbfd8e Binary files /dev/null and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-blob-hash-mobile-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-block-hash-mobile-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-block-hash-mobile-1.png index 4814677c87..64ff86056f 100644 Binary files a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-block-hash-mobile-1.png and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-block-hash-mobile-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-block-number-mobile-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-block-number-mobile-1.png index dae3d9949c..d92e5332a5 100644 Binary files a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-block-number-mobile-1.png and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-block-number-mobile-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-contract-name-mobile-dark-mode-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-contract-name-mobile-dark-mode-1.png index 89a811c836..eea4e39708 100644 Binary files a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-contract-name-mobile-dark-mode-1.png and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-contract-name-mobile-dark-mode-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-domain-name-mobile-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-domain-name-mobile-1.png new file mode 100644 index 0000000000..14c634cb94 Binary files /dev/null and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-domain-name-mobile-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-name-homepage-dark-mode-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-name-homepage-dark-mode-1.png index 548c9271b4..809aa9e90a 100644 Binary files a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-name-homepage-dark-mode-1.png and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-name-homepage-dark-mode-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-tag-mobile-dark-mode-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-tag-mobile-dark-mode-1.png index 486edadb4f..530ed3685f 100644 Binary files a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-tag-mobile-dark-mode-1.png and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-tag-mobile-dark-mode-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-token-name-mobile-dark-mode-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-token-name-mobile-dark-mode-1.png index 2bc3b994c7..4584baf91f 100644 Binary files a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-token-name-mobile-dark-mode-1.png and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-token-name-mobile-dark-mode-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-tx-hash-mobile-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-tx-hash-mobile-1.png index b14c0278b9..02c42937f1 100644 Binary files a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-tx-hash-mobile-1.png and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-tx-hash-mobile-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-user-op-hash-mobile-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-user-op-hash-mobile-1.png index 73f86b73a5..0a2c600ebc 100644 Binary files a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-user-op-hash-mobile-1.png and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-user-op-hash-mobile-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-with-view-all-link-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-with-view-all-link-1.png index bcee42809f..c58de52f87 100644 Binary files a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-with-view-all-link-1.png and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-with-view-all-link-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_with-apps-default-view-mobile-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_with-apps-default-view-mobile-1.png index 86fa94bf04..dd8fbb7f3a 100644 Binary files a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_with-apps-default-view-mobile-1.png and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_with-apps-default-view-mobile-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-address-hash-mobile-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-address-hash-mobile-1.png index 9cf179adb7..6a3942e179 100644 Binary files a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-address-hash-mobile-1.png and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-address-hash-mobile-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-blob-hash-mobile-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-blob-hash-mobile-1.png new file mode 100644 index 0000000000..4278a3990d Binary files /dev/null and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-blob-hash-mobile-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-block-hash-mobile-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-block-hash-mobile-1.png index bc1ada4ad3..c8e7e789fb 100644 Binary files a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-block-hash-mobile-1.png and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-block-hash-mobile-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-block-number-mobile-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-block-number-mobile-1.png index e0fbd6a953..cd5d6d84ef 100644 Binary files a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-block-number-mobile-1.png and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-block-number-mobile-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-domain-name-mobile-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-domain-name-mobile-1.png new file mode 100644 index 0000000000..f265295b03 Binary files /dev/null and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-domain-name-mobile-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-tx-hash-mobile-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-tx-hash-mobile-1.png index 4b15fdec32..24a3225cb2 100644 Binary files a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-tx-hash-mobile-1.png and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-tx-hash-mobile-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-user-op-hash-mobile-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-user-op-hash-mobile-1.png index 065edb5197..9abfcecc7c 100644 Binary files a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-user-op-hash-mobile-1.png and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-user-op-hash-mobile-1.png differ diff --git a/ui/snippets/topBar/ColorModeSwitch.tsx b/ui/snippets/topBar/ColorModeSwitch.tsx deleted file mode 100644 index aa7156a1e1..0000000000 --- a/ui/snippets/topBar/ColorModeSwitch.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { - IconButton, - Popover, - PopoverTrigger, - PopoverContent, - PopoverBody, - useColorMode, - useDisclosure, - Skeleton, -} from '@chakra-ui/react'; -import React from 'react'; - -import * as cookies from 'lib/cookies'; -import IconSvg from 'ui/shared/IconSvg'; - -import ColorModeSwitchTheme from './ColorModeSwitchTheme'; -import { COLOR_THEMES } from './utils'; - -const ColorModeSwitch = () => { - const { isOpen, onToggle, onClose } = useDisclosure(); - const { setColorMode, colorMode } = useColorMode(); - - const [ activeHex, setActiveHex ] = React.useState(); - - const setTheme = React.useCallback((hex: string) => { - const nextTheme = COLOR_THEMES.find((theme) => theme.colors.some((color) => color.hex === hex)); - - if (!nextTheme) { - return; - } - - setColorMode(nextTheme.colorMode); - - const varName = nextTheme.colorMode === 'light' ? '--chakra-colors-white' : '--chakra-colors-black'; - window.document.documentElement.style.setProperty(varName, hex); - - cookies.set(cookies.NAMES.COLOR_MODE_HEX, hex); - window.localStorage.setItem(cookies.NAMES.COLOR_MODE, nextTheme.colorMode); - }, [ setColorMode ]); - - React.useEffect(() => { - const cookieColorMode = cookies.get(cookies.NAMES.COLOR_MODE); - - const nextColorMode = (() => { - if (!cookieColorMode) { - return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; - } - - return colorMode; - })(); - - const fallbackHex = (COLOR_THEMES.find(theme => theme.colorMode === nextColorMode && theme.colors.length === 1) ?? COLOR_THEMES[0]).colors[0].hex; - const cookieHex = cookies.get(cookies.NAMES.COLOR_MODE_HEX) ?? fallbackHex; - setTheme(cookieHex); - setActiveHex(cookieHex); - // should run only on mount - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ ]); - - const handleSelect = React.useCallback((event: React.MouseEvent) => { - event.stopPropagation(); - - const hex = event.currentTarget.getAttribute('data-hex'); - - if (!hex) { - return; - } - - setTheme(hex); - setActiveHex(hex); - }, [ setTheme ]); - - const activeTheme = COLOR_THEMES.find((theme) => theme.colors.some((color) => color.hex === activeHex)); - - return ( - - - { activeTheme ? ( - } - boxSize={ 5 } - onClick={ onToggle } - /> - ) : } - - - - { COLOR_THEMES.map((theme) => ) } - - - - ); -}; - -export default ColorModeSwitch; diff --git a/ui/snippets/topBar/ColorModeSwitchSample.tsx b/ui/snippets/topBar/ColorModeSwitchSample.tsx deleted file mode 100644 index a1a8a5b4e4..0000000000 --- a/ui/snippets/topBar/ColorModeSwitchSample.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { - Box, - useColorModeValue, - useToken, -} from '@chakra-ui/react'; -import React from 'react'; - -import type { ColorThemeColor } from './utils'; - -interface Props extends ColorThemeColor { - onClick?: (event: React.MouseEvent) => void; - isActive: boolean; -} - -const ColorModeSwitchSample = ({ hex, sampleBg, onClick, isActive }: Props) => { - const bgColor = useColorModeValue('white', 'gray.900'); - const activeBgColor = useColorModeValue('blue.50', 'blackAlpha.800'); - - const activeBorderColor = useToken('colors', useColorModeValue('blackAlpha.800', 'gray.50')); - const hoverBorderColor = useToken('colors', 'link_hovered'); - - return ( - - ); -}; - -export default ColorModeSwitchSample; diff --git a/ui/snippets/topBar/ColorModeSwitchTheme.tsx b/ui/snippets/topBar/ColorModeSwitchTheme.tsx deleted file mode 100644 index d69608ed79..0000000000 --- a/ui/snippets/topBar/ColorModeSwitchTheme.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { - Flex, - useColorModeValue, - useToken, -} from '@chakra-ui/react'; -import React from 'react'; - -import IconSvg from 'ui/shared/IconSvg'; - -import ColorModeSwitchSample from './ColorModeSwitchSample'; -import type { ColorTheme } from './utils'; - -interface Props extends ColorTheme { - onClick?: (event: React.MouseEvent) => void; - activeHex: string | undefined; -} - -const ColorModeSwitchTheme = ({ icon, name, colors, onClick, activeHex }: Props) => { - const isActive = colors.some((sample) => sample.hex === activeHex); - const activeColor = useColorModeValue('blackAlpha.800', 'gray.50'); - const activeBgColor = useColorModeValue('blue.50', 'blackAlpha.800'); - const inactiveColor = useColorModeValue('blue.700', 'gray.400'); - const hoverBorderColor = useToken('colors', 'link_hovered'); - const hasOneColor = colors.length === 1; - - return ( - - - { name } - - { colors.map((sample) => ) } - - - ); -}; - -export default ColorModeSwitchTheme; diff --git a/ui/snippets/topBar/SwapButton.tsx b/ui/snippets/topBar/SwapButton.tsx index 3792ac90a1..b446455f9a 100644 --- a/ui/snippets/topBar/SwapButton.tsx +++ b/ui/snippets/topBar/SwapButton.tsx @@ -1,14 +1,24 @@ import { Button, Box } from '@chakra-ui/react'; +import { useRouter } from 'next/router'; import React from 'react'; import { route } from 'nextjs-routes'; import config from 'configs/app'; +import getPageType from 'lib/mixpanel/getPageType'; +import * as mixpanel from 'lib/mixpanel/index'; import IconSvg from 'ui/shared/IconSvg'; const feature = config.features.swapButton; const SwapButton = () => { + const router = useRouter(); + const source = getPageType(router.pathname); + + const handleClick = React.useCallback(() => { + mixpanel.logEvent(mixpanel.EventTypes.BUTTON_CLICK, { Content: 'Swap button', Source: source }); + }, [ source ]); + if (!feature.isEnabled) { return null; } @@ -27,6 +37,7 @@ const SwapButton = () => { borderRadius="sm" height={ 5 } px={ 1.5 } + onClick={ handleClick } > diff --git a/ui/snippets/topBar/TopBar.pw.tsx b/ui/snippets/topBar/TopBar.pw.tsx index 5d0d4d284e..1880147dfd 100644 --- a/ui/snippets/topBar/TopBar.pw.tsx +++ b/ui/snippets/topBar/TopBar.pw.tsx @@ -11,7 +11,8 @@ import TopBar from './TopBar'; const test = base.extend({ context: contextWithEnvs([ { name: 'NEXT_PUBLIC_SWAP_BUTTON_URL', value: 'uniswap' }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any + { name: 'NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL', value: 'DUCK' }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any ]) as any, }); @@ -28,8 +29,24 @@ test('default view +@dark-mode +@mobile', async({ mount, page }) => { ); await component.getByText(/\$1\.39/).click(); + await expect(page.getByText(/last update/i)).toBeVisible(); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 220 } }); - await component.getByLabel('color mode switch').click(); - await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 220 } }); + await component.getByLabel('User settings').click(); + await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 400 } }); +}); + +test('with secondary coin price +@mobile', async({ mount, page }) => { + await page.route(buildApiUrl('stats'), (route) => route.fulfill({ + status: 200, + body: JSON.stringify(statsMock.withSecondaryCoin), + })); + + const component = await mount( + + + , + ); + + await expect(component).toHaveScreenshot(); }); diff --git a/ui/snippets/topBar/TopBar.tsx b/ui/snippets/topBar/TopBar.tsx index 443af1b05b..0b97de2aff 100644 --- a/ui/snippets/topBar/TopBar.tsx +++ b/ui/snippets/topBar/TopBar.tsx @@ -3,7 +3,7 @@ import React from 'react'; import config from 'configs/app'; -import ColorModeSwitch from './ColorModeSwitch'; +import Settings from './settings/Settings'; import SwapButton from './SwapButton'; import TopBarStats from './TopBarStats'; @@ -28,7 +28,7 @@ const TopBar = () => { ) } - + ); diff --git a/ui/snippets/topBar/TopBarStats.tsx b/ui/snippets/topBar/TopBarStats.tsx index 7bc180f0c6..6d5770b184 100644 --- a/ui/snippets/topBar/TopBarStats.tsx +++ b/ui/snippets/topBar/TopBarStats.tsx @@ -4,12 +4,15 @@ import React from 'react'; import config from 'configs/app'; import useApiQuery from 'lib/api/useApiQuery'; import dayjs from 'lib/date/dayjs'; +import useIsMobile from 'lib/hooks/useIsMobile'; import { HOMEPAGE_STATS } from 'stubs/stats'; import GasInfoTooltip from 'ui/shared/gas/GasInfoTooltip'; import GasPrice from 'ui/shared/gas/GasPrice'; import TextSeparator from 'ui/shared/TextSeparator'; const TopBarStats = () => { + const isMobile = useIsMobile(); + const { data, isPlaceholderData, isError, refetch, dataUpdatedAt } = useApiQuery('stats', { queryOptions: { placeholderData: HOMEPAGE_STATS, @@ -51,7 +54,7 @@ const TopBarStats = () => { { data?.coin_price && ( - { config.chain.governanceToken.symbol || config.chain.currency.symbol } + { config.chain.currency.symbol } ${ Number(data.coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }) } { data.coin_price_change_percentage && ( @@ -63,6 +66,14 @@ const TopBarStats = () => { ) } ) } + { !isMobile && data?.secondary_coin_price && config.chain.secondaryCoin.symbol && ( + + + { config.chain.secondaryCoin.symbol } + ${ Number(data.secondary_coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }) } + + + ) } { data?.coin_price && config.features.gasTracker.isEnabled && } { data?.gas_prices && data.gas_prices.average !== null && config.features.gasTracker.isEnabled && ( diff --git a/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_dark-color-mode_default-view-dark-mode-mobile-1.png b/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_dark-color-mode_default-view-dark-mode-mobile-1.png index 5a124198ea..ef9ee9ef75 100644 Binary files a/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_dark-color-mode_default-view-dark-mode-mobile-1.png and b/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_dark-color-mode_default-view-dark-mode-mobile-1.png differ diff --git a/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_dark-color-mode_default-view-dark-mode-mobile-2.png b/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_dark-color-mode_default-view-dark-mode-mobile-2.png index 6cdde6c76b..81a796b202 100644 Binary files a/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_dark-color-mode_default-view-dark-mode-mobile-2.png and b/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_dark-color-mode_default-view-dark-mode-mobile-2.png differ diff --git a/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_default_default-view-dark-mode-mobile-1.png b/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_default_default-view-dark-mode-mobile-1.png index 2efa6dc26f..5d9431dffd 100644 Binary files a/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_default_default-view-dark-mode-mobile-1.png and b/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_default_default-view-dark-mode-mobile-1.png differ diff --git a/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_default_default-view-dark-mode-mobile-2.png b/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_default_default-view-dark-mode-mobile-2.png index 6fb7bcd102..05add27866 100644 Binary files a/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_default_default-view-dark-mode-mobile-2.png and b/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_default_default-view-dark-mode-mobile-2.png differ diff --git a/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_default_with-secondary-coin-price-mobile-1.png b/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_default_with-secondary-coin-price-mobile-1.png new file mode 100644 index 0000000000..1eb1aeccd9 Binary files /dev/null and b/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_default_with-secondary-coin-price-mobile-1.png differ diff --git a/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_mobile_default-view-dark-mode-mobile-1.png b/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_mobile_default-view-dark-mode-mobile-1.png index acbfe60a6f..e6c21cc8f1 100644 Binary files a/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_mobile_default-view-dark-mode-mobile-1.png and b/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_mobile_default-view-dark-mode-mobile-1.png differ diff --git a/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_mobile_default-view-dark-mode-mobile-2.png b/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_mobile_default-view-dark-mode-mobile-2.png index d9e7433445..10916853a2 100644 Binary files a/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_mobile_default-view-dark-mode-mobile-2.png and b/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_mobile_default-view-dark-mode-mobile-2.png differ diff --git a/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_mobile_with-secondary-coin-price-mobile-1.png b/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_mobile_with-secondary-coin-price-mobile-1.png new file mode 100644 index 0000000000..807843546d Binary files /dev/null and b/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_mobile_with-secondary-coin-price-mobile-1.png differ diff --git a/ui/snippets/topBar/settings/Settings.tsx b/ui/snippets/topBar/settings/Settings.tsx new file mode 100644 index 0000000000..664ec6461c --- /dev/null +++ b/ui/snippets/topBar/settings/Settings.tsx @@ -0,0 +1,36 @@ +import { Box, IconButton, Popover, PopoverBody, PopoverContent, PopoverTrigger, useDisclosure } from '@chakra-ui/react'; +import React from 'react'; + +import IconSvg from 'ui/shared/IconSvg'; + +import SettingsColorTheme from './SettingsColorTheme'; +import SettingsIdentIcon from './SettingsIdentIcon'; + +const Settings = () => { + const { isOpen, onToggle, onClose } = useDisclosure(); + + return ( + + + } + p="1px" + boxSize={ 5 } + onClick={ onToggle } + /> + + + + + + + + + + ); +}; + +export default React.memo(Settings); diff --git a/ui/snippets/topBar/settings/SettingsColorTheme.tsx b/ui/snippets/topBar/settings/SettingsColorTheme.tsx new file mode 100644 index 0000000000..a8ba9c580e --- /dev/null +++ b/ui/snippets/topBar/settings/SettingsColorTheme.tsx @@ -0,0 +1,85 @@ +import { Box, Flex, useColorMode } from '@chakra-ui/react'; +import React from 'react'; + +import * as cookies from 'lib/cookies'; + +import SettingsSample from './SettingsSample'; +import { COLOR_THEMES } from './utils'; + +const SettingsColorTheme = () => { + const { setColorMode } = useColorMode(); + + const [ activeHex, setActiveHex ] = React.useState(); + + const setTheme = React.useCallback((hex: string) => { + const nextTheme = COLOR_THEMES.find((theme) => theme.hex === hex); + + if (!nextTheme) { + return; + } + + setColorMode(nextTheme.colorMode); + + const varName = nextTheme.colorMode === 'light' ? '--chakra-colors-white' : '--chakra-colors-black'; + window.document.documentElement.style.setProperty(varName, hex); + + cookies.set(cookies.NAMES.COLOR_MODE_HEX, hex); + window.localStorage.setItem(cookies.NAMES.COLOR_MODE, nextTheme.colorMode); + }, [ setColorMode ]); + + React.useEffect(() => { + const cookieColorMode = cookies.get(cookies.NAMES.COLOR_MODE); + + const nextColorMode = (() => { + if (!cookieColorMode) { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + } + + return cookieColorMode; + })(); + + const colorModeThemes = COLOR_THEMES.filter(theme => theme.colorMode === nextColorMode); + const fallbackHex = colorModeThemes[colorModeThemes.length - 1].hex; + const cookieHex = cookies.get(cookies.NAMES.COLOR_MODE_HEX) ?? fallbackHex; + setTheme(cookieHex); + setActiveHex(cookieHex); + // should run only on mount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ ]); + + const handleSelect = React.useCallback((event: React.MouseEvent) => { + event.stopPropagation(); + + const hex = event.currentTarget.getAttribute('data-value'); + + if (!hex) { + return; + } + + setTheme(hex); + setActiveHex(hex); + }, [ setTheme ]); + + const activeTheme = COLOR_THEMES.find((theme) => theme.hex === activeHex); + + return ( +
+ Color theme + { activeTheme?.label } + + { COLOR_THEMES.map((theme) => ( + + )) } + +
+ ); +}; + +export default React.memo(SettingsColorTheme); diff --git a/ui/snippets/topBar/settings/SettingsIdentIcon.tsx b/ui/snippets/topBar/settings/SettingsIdentIcon.tsx new file mode 100644 index 0000000000..af8f4b3140 --- /dev/null +++ b/ui/snippets/topBar/settings/SettingsIdentIcon.tsx @@ -0,0 +1,53 @@ +import { Box, Flex } from '@chakra-ui/react'; +import React from 'react'; + +import config from 'configs/app'; +import * as cookies from 'lib/cookies'; + +import SettingsSample from './SettingsSample'; +import { IDENTICONS } from './utils'; + +const SettingsIdentIcon = () => { + const [ activeId, setActiveId ] = React.useState(); + + React.useEffect(() => { + const initialId = cookies.get(cookies.NAMES.ADDRESS_IDENTICON_TYPE) || config.UI.views.address.identiconType; + setActiveId(initialId); + }, []); + + const handleSelect = React.useCallback((event: React.MouseEvent) => { + event.stopPropagation(); + + const id = event.currentTarget.getAttribute('data-value'); + + if (!id) { + return; + } + + cookies.set(cookies.NAMES.ADDRESS_IDENTICON_TYPE, id); + window.location.reload(); + }, []); + + const activeIdenticon = IDENTICONS.find((identicon) => identicon.id === activeId); + + return ( +
+ Address identicon + { activeIdenticon?.label } + + { IDENTICONS.map((identicon) => ( + + )) } + +
+ ); +}; + +export default React.memo(SettingsIdentIcon); diff --git a/ui/snippets/topBar/settings/SettingsSample.tsx b/ui/snippets/topBar/settings/SettingsSample.tsx new file mode 100644 index 0000000000..b6986b1862 --- /dev/null +++ b/ui/snippets/topBar/settings/SettingsSample.tsx @@ -0,0 +1,63 @@ +import { + Box, + Tooltip, + useColorModeValue, + useToken, +} from '@chakra-ui/react'; +import React from 'react'; + +interface Props { + onClick?: (event: React.MouseEvent) => void; + isActive: boolean; + bg: string; + value: string; + label: string; +} + +const TOOLTIP_OFFSET: [ number, number ] = [ 0, 10 ]; + +const SettingsSample = ({ label, value, bg, onClick, isActive }: Props) => { + const bgColor = useColorModeValue('white', 'gray.900'); + const activeBgColor = useColorModeValue('blue.50', 'whiteAlpha.100'); + + const activeBorderColor = useToken('colors', useColorModeValue('blackAlpha.800', 'gray.50')); + const hoverBorderColor = useToken('colors', 'link_hovered'); + + return ( + + + + + + ); +}; + +export default React.memo(SettingsSample); diff --git a/ui/snippets/topBar/settings/utils.ts b/ui/snippets/topBar/settings/utils.ts new file mode 100644 index 0000000000..5346846a33 --- /dev/null +++ b/ui/snippets/topBar/settings/utils.ts @@ -0,0 +1,53 @@ +import type { IdenticonType } from 'types/views/address'; + +export const COLOR_THEMES = [ + { + label: 'Light', + colorMode: 'light', + hex: '#FFFFFF', + sampleBg: 'linear-gradient(154deg, #EFEFEF 50%, rgba(255, 255, 255, 0.00) 330.86%)', + }, + { + label: 'Dim', + colorMode: 'dark', + hex: '#232B37', + sampleBg: 'linear-gradient(152deg, #232B37 50%, rgba(255, 255, 255, 0.00) 290.71%)', + }, + { + label: 'Midnight', + colorMode: 'dark', + hex: '#1B2E48', + sampleBg: 'linear-gradient(148deg, #1B3F71 50%, rgba(255, 255, 255, 0.00) 312.35%)', + }, + { + label: 'Dark', + colorMode: 'dark', + hex: '#101112', + sampleBg: 'linear-gradient(161deg, #000 9.37%, #383838 92.52%)', + }, +]; + +export type ColorTheme = typeof COLOR_THEMES[number]; + +export const IDENTICONS: Array<{ label: string; id: IdenticonType; sampleBg: string }> = [ + { + label: 'GitHub', + id: 'github', + sampleBg: 'url("/static/identicon_logos/github.png") center / contain no-repeat', + }, + { + label: 'Metamask jazzicon', + id: 'jazzicon', + sampleBg: 'url("/static/identicon_logos/jazzicon.png") center / contain no-repeat', + }, + { + label: 'Ethereum blockies', + id: 'blockie', + sampleBg: 'url("/static/identicon_logos/blockies.png") center / contain no-repeat', + }, + { + label: 'Gradient avatar', + id: 'gradient_avatar', + sampleBg: 'url("/static/identicon_logos/gradient_avatar.png") center / contain no-repeat', + }, +]; diff --git a/ui/snippets/topBar/utils.ts b/ui/snippets/topBar/utils.ts deleted file mode 100644 index 0644fcc9b2..0000000000 --- a/ui/snippets/topBar/utils.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { IconName } from 'ui/shared/IconSvg'; - -export const COLOR_THEMES = [ - { - name: 'Light', - colorMode: 'light', - icon: 'sun' as IconName, - colors: [ - { hex: '#FFFFFF', sampleBg: 'linear-gradient(154deg, #EFEFEF 50%, rgba(255, 255, 255, 0.00) 330.86%)' }, - ], - }, - { - name: 'Dim', - colorMode: 'dark', - icon: 'moon-with-star' as IconName, - colors: [ - { hex: '#232B37', sampleBg: 'linear-gradient(152deg, #232B37 50%, rgba(255, 255, 255, 0.00) 290.71%)' }, - { hex: '#1B2E48', sampleBg: 'linear-gradient(150deg, #1B2E48 50%, rgba(255, 255, 255, 0.00) 312.75%)' }, - ], - }, - { - name: 'Dark', - colorMode: 'dark', - icon: 'moon' as IconName, - colors: [ - { hex: '#101112', sampleBg: 'linear-gradient(161deg, #000 9.37%, #383838 92.52%)' }, - ], - }, -]; - -export type ColorTheme = typeof COLOR_THEMES[number]; - -export type ColorThemeColor = ColorTheme['colors'][number]; diff --git a/ui/snippets/useMenuButtonColors.tsx b/ui/snippets/useMenuButtonColors.tsx index 1583e18fb5..cbd3e9bbd3 100644 --- a/ui/snippets/useMenuButtonColors.tsx +++ b/ui/snippets/useMenuButtonColors.tsx @@ -2,8 +2,9 @@ import { useColorModeValue } from '@chakra-ui/react'; export default function useMenuColors() { const themedBackground = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); + const themedBackgroundOrange = useColorModeValue('orange.100', 'orange.900'); const themedBorderColor = useColorModeValue('gray.300', 'gray.700'); const themedColor = useColorModeValue('blackAlpha.800', 'gray.400'); - return { themedBackground, themedBorderColor, themedColor }; + return { themedBackground, themedBackgroundOrange, themedBorderColor, themedColor }; } diff --git a/ui/snippets/walletMenu/WalletIdenticon.tsx b/ui/snippets/walletMenu/WalletIdenticon.tsx new file mode 100644 index 0000000000..95a10a7334 --- /dev/null +++ b/ui/snippets/walletMenu/WalletIdenticon.tsx @@ -0,0 +1,47 @@ +import { Box, Flex, chakra } from '@chakra-ui/react'; +import React from 'react'; + +import useIsMobile from 'lib/hooks/useIsMobile'; +import AddressIdenticon from 'ui/shared/entities/address/AddressIdenticon'; +import IconSvg from 'ui/shared/IconSvg'; + +import useMenuButtonColors from '../useMenuButtonColors'; + +type Props = { + address: string; + isAutoConnectDisabled?: boolean; + className?: string; +}; + +const WalletIdenticon = ({ address, isAutoConnectDisabled, className }: Props) => { + const { themedBackgroundOrange } = useMenuButtonColors(); + const isMobile = useIsMobile(); + + return ( + + + { isAutoConnectDisabled && ( + + + + ) } + + ); +}; + +export default chakra(WalletIdenticon); diff --git a/ui/snippets/walletMenu/WalletMenuContent.tsx b/ui/snippets/walletMenu/WalletMenuContent.tsx index 0578d3c6c5..35f03693d6 100644 --- a/ui/snippets/walletMenu/WalletMenuContent.tsx +++ b/ui/snippets/walletMenu/WalletMenuContent.tsx @@ -1,22 +1,57 @@ -import { Box, Button, Text } from '@chakra-ui/react'; +import { Box, Button, Text, Flex, IconButton } from '@chakra-ui/react'; import React from 'react'; import * as mixpanel from 'lib/mixpanel/index'; import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import IconSvg from 'ui/shared/IconSvg'; + +import useMenuButtonColors from '../useMenuButtonColors'; type Props = { address?: string; disconnect?: () => void; + isAutoConnectDisabled?: boolean; + openWeb3Modal: () => void; + closeWalletMenu: () => void; }; -const WalletMenuContent = ({ address, disconnect }: Props) => { +const WalletMenuContent = ({ address, disconnect, isAutoConnectDisabled, openWeb3Modal, closeWalletMenu }: Props) => { + const { themedBackgroundOrange } = useMenuButtonColors(); + const [ isModalOpening, setIsModalOpening ] = React.useState(false); + const onAddressClick = React.useCallback(() => { mixpanel.logEvent(mixpanel.EventTypes.WALLET_ACTION, { Action: 'Address click' }); }, []); + const handleOpenWeb3Modal = React.useCallback(async() => { + setIsModalOpening(true); + await openWeb3Modal(); + setTimeout(closeWalletMenu, 300); + }, [ openWeb3Modal, closeWalletMenu ]); + return ( + { isAutoConnectDisabled && ( + + + + Connect your wallet in the app below + + + ) } { > Your wallet is used to interact with apps and contracts in the explorer. - + + + } + variant="simple" + h="20px" + w="20px" + ml={ 1 } + onClick={ handleOpenWeb3Modal } + isLoading={ isModalOpening } + /> + diff --git a/ui/snippets/walletMenu/WalletMenuDesktop.tsx b/ui/snippets/walletMenu/WalletMenuDesktop.tsx index df19ef5cf9..2beaf3c212 100644 --- a/ui/snippets/walletMenu/WalletMenuDesktop.tsx +++ b/ui/snippets/walletMenu/WalletMenuDesktop.tsx @@ -1,26 +1,30 @@ import type { ButtonProps } from '@chakra-ui/react'; -import { Popover, PopoverContent, PopoverBody, PopoverTrigger, Button, Box, useBoolean } from '@chakra-ui/react'; +import { Popover, PopoverContent, PopoverBody, PopoverTrigger, Button, Box, useBoolean, chakra, useColorModeValue } from '@chakra-ui/react'; import React from 'react'; +import { useMarketplaceContext } from 'lib/contexts/marketplace'; import useIsMobile from 'lib/hooks/useIsMobile'; import * as mixpanel from 'lib/mixpanel/index'; -import AddressIdenticon from 'ui/shared/entities/address/AddressIdenticon'; import HashStringShorten from 'ui/shared/HashStringShorten'; import useWallet from 'ui/snippets/walletMenu/useWallet'; import WalletMenuContent from 'ui/snippets/walletMenu/WalletMenuContent'; import useMenuButtonColors from '../useMenuButtonColors'; +import WalletIdenticon from './WalletIdenticon'; import WalletTooltip from './WalletTooltip'; type Props = { isHomePage?: boolean; + className?: string; + size?: 'sm' | 'md'; }; -const WalletMenuDesktop = ({ isHomePage }: Props) => { - const { isWalletConnected, address, connect, disconnect, isModalOpening, isModalOpen } = useWallet({ source: 'Header' }); - const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors(); +const WalletMenuDesktop = ({ isHomePage, className, size = 'md' }: Props) => { + const { isWalletConnected, address, connect, disconnect, isModalOpening, isModalOpen, openModal } = useWallet({ source: 'Header' }); + const { themedBackground, themedBackgroundOrange, themedBorderColor, themedColor } = useMenuButtonColors(); const [ isPopoverOpen, setIsPopoverOpen ] = useBoolean(false); const isMobile = useIsMobile(); + const { isAutoConnectDisabled } = useMarketplaceContext(); const variant = React.useMemo(() => { if (isWalletConnected) { @@ -29,13 +33,16 @@ const WalletMenuDesktop = ({ isHomePage }: Props) => { return isHomePage ? 'solid' : 'outline'; }, [ isWalletConnected, isHomePage ]); + const themedColorForOrangeBg = useColorModeValue('blackAlpha.800', 'whiteAlpha.800'); let buttonStyles: Partial = {}; if (isWalletConnected) { + const backgroundColor = isAutoConnectDisabled ? themedBackgroundOrange : themedBackground; + const color = isAutoConnectDisabled ? themedColorForOrangeBg : themedColor; buttonStyles = { - bg: isHomePage ? 'blue.50' : themedBackground, - color: isHomePage ? 'blackAlpha.800' : themedColor, + bg: isHomePage ? 'blue.50' : backgroundColor, + color: isHomePage ? 'blackAlpha.800' : color, _hover: { - color: isHomePage ? 'blackAlpha.800' : themedColor, + color: isHomePage ? 'blackAlpha.800' : color, }, }; } else if (isHomePage) { @@ -51,7 +58,7 @@ const WalletMenuDesktop = ({ isHomePage }: Props) => { const openPopover = React.useCallback(() => { mixpanel.logEvent(mixpanel.EventTypes.WALLET_ACTION, { Action: 'Open' }); - setIsPopoverOpen.on(); + setIsPopoverOpen.toggle(); }, [ setIsPopoverOpen ]); return ( @@ -63,35 +70,45 @@ const WalletMenuDesktop = ({ isHomePage }: Props) => { isOpen={ isPopoverOpen } onClose={ setIsPopoverOpen.off } > - - - + + + - - - + + + { isWalletConnected && ( - + ) } @@ -99,4 +116,4 @@ const WalletMenuDesktop = ({ isHomePage }: Props) => { ); }; -export default WalletMenuDesktop; +export default chakra(WalletMenuDesktop); diff --git a/ui/snippets/walletMenu/WalletMenuMobile.tsx b/ui/snippets/walletMenu/WalletMenuMobile.tsx index e8e6bcb48a..9375facb4f 100644 --- a/ui/snippets/walletMenu/WalletMenuMobile.tsx +++ b/ui/snippets/walletMenu/WalletMenuMobile.tsx @@ -1,45 +1,54 @@ import { Drawer, DrawerOverlay, DrawerContent, DrawerBody, useDisclosure, IconButton } from '@chakra-ui/react'; import React from 'react'; +import { useMarketplaceContext } from 'lib/contexts/marketplace'; import useIsMobile from 'lib/hooks/useIsMobile'; import * as mixpanel from 'lib/mixpanel/index'; -import AddressIdenticon from 'ui/shared/entities/address/AddressIdenticon'; import IconSvg from 'ui/shared/IconSvg'; import useWallet from 'ui/snippets/walletMenu/useWallet'; import WalletMenuContent from 'ui/snippets/walletMenu/WalletMenuContent'; import useMenuButtonColors from '../useMenuButtonColors'; +import WalletIdenticon from './WalletIdenticon'; import WalletTooltip from './WalletTooltip'; const WalletMenuMobile = () => { const { isOpen, onOpen, onClose } = useDisclosure(); - const { isWalletConnected, address, connect, disconnect, isModalOpening, isModalOpen } = useWallet({ source: 'Header' }); - const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors(); + const { isWalletConnected, address, connect, disconnect, isModalOpening, isModalOpen, openModal } = useWallet({ source: 'Header' }); + const { themedBackground, themedBackgroundOrange, themedBorderColor, themedColor } = useMenuButtonColors(); const isMobile = useIsMobile(); + const { isAutoConnectDisabled } = useMarketplaceContext(); const openPopover = React.useCallback(() => { mixpanel.logEvent(mixpanel.EventTypes.WALLET_ACTION, { Action: 'Open' }); onOpen(); }, [ onOpen ]); + const themedBg = isAutoConnectDisabled ? themedBackgroundOrange : themedBackground; + return ( <> - + : + : } variant={ isWalletConnected ? 'subtle' : 'outline' } colorScheme="gray" boxSize="40px" flexShrink={ 0 } - bg={ isWalletConnected ? themedBackground : undefined } + bg={ isWalletConnected ? themedBg : undefined } color={ themedColor } borderColor={ !isWalletConnected ? themedBorderColor : undefined } onClick={ isWalletConnected ? openPopover : connect } - isLoading={ isModalOpening || isModalOpen } + isLoading={ (isModalOpening || isModalOpen) && !isWalletConnected } /> { isWalletConnected && ( @@ -52,7 +61,13 @@ const WalletMenuMobile = () => { - + diff --git a/ui/snippets/walletMenu/WalletTooltip.tsx b/ui/snippets/walletMenu/WalletTooltip.tsx index fc03e6dcee..d8ad0bcf79 100644 --- a/ui/snippets/walletMenu/WalletTooltip.tsx +++ b/ui/snippets/walletMenu/WalletTooltip.tsx @@ -1,4 +1,4 @@ -import { Tooltip, useBoolean, useOutsideClick } from '@chakra-ui/react'; +import { Tooltip, useBoolean, useOutsideClick, Box } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import React from 'react'; @@ -9,38 +9,46 @@ type Props = { children: React.ReactNode; isDisabled?: boolean; isMobile?: boolean; + isWalletConnected?: boolean; + isAutoConnectDisabled?: boolean; }; -const WalletTooltip = ({ children, isDisabled, isMobile }: Props) => { +const LOCAL_STORAGE_KEY = 'wallet-connect-tooltip-shown'; + +const WalletTooltip = ({ children, isDisabled, isMobile, isWalletConnected, isAutoConnectDisabled }: Props, ref: React.ForwardedRef) => { const router = useRouter(); const [ isTooltipShown, setIsTooltipShown ] = useBoolean(false); - const ref = React.useRef(null); - useOutsideClick({ ref, handler: setIsTooltipShown.off }); + const innerRef = React.useRef(null); + useOutsideClick({ ref: innerRef, handler: setIsTooltipShown.off }); + + const label = React.useMemo(() => { + if (isWalletConnected) { + if (isAutoConnectDisabled) { + return Your wallet is not
connected to this app.
Connect your wallet
in the app directly
; + } + return Your wallet is connected
with Blockscout
; + } + return Connect your wallet
to Blockscout for
full-featured access
; + }, [ isWalletConnected, isAutoConnectDisabled ]); - const { defaultLabel, label, localStorageKey } = React.useMemo(() => { - const isAppPage = router.pathname === '/apps/[id]'; - const defaultLabel = Your wallet is used to interact with
apps and contracts in the explorer
; - const label = isAppPage ? - Connect once to use your wallet with
all apps in the DAppscout marketplace!
: - defaultLabel; - const localStorageKey = `${ isAppPage ? 'dapp-' : '' }wallet-connect-tooltip-shown`; - return { defaultLabel, label, localStorageKey }; - }, [ router.pathname ]); + const isAppPage = router.pathname === '/apps/[id]'; React.useEffect(() => { - const wasShown = window.localStorage.getItem(localStorageKey); - const isMarketplacePage = [ '/apps', '/apps/[id]' ].includes(router.pathname); + const wasShown = window.localStorage.getItem(LOCAL_STORAGE_KEY); + const isMarketplacePage = router.pathname === '/apps'; const isTooltipShowAction = router.query.action === 'tooltip'; const isConnectWalletAction = router.query.action === 'connect'; - const needToShow = (!wasShown && !isConnectWalletAction) || isTooltipShowAction; + const needToShow = (isAppPage && !isConnectWalletAction) || isTooltipShowAction || (!wasShown && isMarketplacePage); let timer1: ReturnType; let timer2: ReturnType; - if (!isDisabled && isMarketplacePage && needToShow) { + if (!isDisabled && needToShow) { timer1 = setTimeout(() => { setIsTooltipShown.on(); - window.localStorage.setItem(localStorageKey, 'true'); timer2 = setTimeout(() => setIsTooltipShown.off(), 5 * SECOND); + if (!wasShown && isMarketplacePage) { + window.localStorage.setItem(LOCAL_STORAGE_KEY, 'true'); + } if (isTooltipShowAction) { removeQueryParam(router, 'action'); } @@ -51,23 +59,25 @@ const WalletTooltip = ({ children, isDisabled, isMobile }: Props) => { clearTimeout(timer1); clearTimeout(timer2); }; - }, [ setIsTooltipShown, localStorageKey, isDisabled, router ]); + }, [ setIsTooltipShown, isDisabled, router, isAppPage ]); return ( - - { children } - + + + { children } + + ); }; -export default WalletTooltip; +export default React.forwardRef(WalletTooltip); diff --git a/ui/snippets/walletMenu/useWallet.tsx b/ui/snippets/walletMenu/useWallet.tsx index cccdaa166b..6fcd209dd4 100644 --- a/ui/snippets/walletMenu/useWallet.tsx +++ b/ui/snippets/walletMenu/useWallet.tsx @@ -1,6 +1,6 @@ import { useWeb3Modal, useWeb3ModalState } from '@web3modal/wagmi/react'; import React from 'react'; -import { useAccount, useDisconnect } from 'wagmi'; +import { useAccount, useDisconnect, useAccountEffect } from 'wagmi'; import * as mixpanel from 'lib/mixpanel/index'; @@ -38,11 +38,14 @@ export default function useWallet({ source }: Params) { disconnect(); }, [ disconnect ]); - const { address, isDisconnected } = useAccount({ onConnect: handleAccountConnected }); + useAccountEffect({ onConnect: handleAccountConnected }); + + const { address, isDisconnected } = useAccount(); const isWalletConnected = isClientLoaded && !isDisconnected && address !== undefined; return { + openModal: open, isWalletConnected, address: address || '', connect: handleConnect, diff --git a/ui/stats/ChartsWidgetsList.tsx b/ui/stats/ChartsWidgetsList.tsx index 102ce73a7d..5002e599b5 100644 --- a/ui/stats/ChartsWidgetsList.tsx +++ b/ui/stats/ChartsWidgetsList.tsx @@ -15,16 +15,26 @@ import ChartWidgetContainer from './ChartWidgetContainer'; type Props = { filterQuery: string; + initialFilterQuery: string; isError: boolean; isPlaceholderData: boolean; charts?: Array; interval: StatsIntervalIds; } -const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, interval }: Props) => { +const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, interval, initialFilterQuery }: Props) => { const [ isSomeChartLoadingError, setIsSomeChartLoadingError ] = useState(false); const isAnyChartDisplayed = charts?.some((section) => section.charts.length > 0); const isEmptyChartList = Boolean(filterQuery) && !isAnyChartDisplayed; + const sectionRef = React.useRef(null); + + const shouldScrollToSection = Boolean(initialFilterQuery); + + React.useEffect(() => { + if (shouldScrollToSection) { + sectionRef.current?.scrollIntoView({ behavior: 'smooth' }); + } + }, [ shouldScrollToSection ]); const homeStatsQuery = useApiQuery('stats', { queryOptions: { @@ -50,7 +60,7 @@ const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, in ) } - + { charts?.map((section) => ( - + { section.title } { section.id === 'gas' && homeStatsQuery.data && homeStatsQuery.data.gas_prices && ( diff --git a/ui/stats/NumberWidget.tsx b/ui/stats/NumberWidget.tsx new file mode 100644 index 0000000000..05eaf90497 --- /dev/null +++ b/ui/stats/NumberWidget.tsx @@ -0,0 +1,42 @@ +import { Box, Skeleton, useColorModeValue } from '@chakra-ui/react'; +import React from 'react'; + +type Props = { + label: string; + value: string; + isLoading?: boolean; +} + +const NumberWidget = ({ label, value, isLoading }: Props) => { + const bgColor = useColorModeValue('blue.50', 'blue.800'); + const skeletonBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); + + return ( + + + { label } + + + + { value } + + + ); +}; + +export default NumberWidget; diff --git a/ui/stats/StatsFilters.tsx b/ui/stats/StatsFilters.tsx index 23f1bfce47..df96c3ac35 100644 --- a/ui/stats/StatsFilters.tsx +++ b/ui/stats/StatsFilters.tsx @@ -1,4 +1,4 @@ -import { Grid, GridItem } from '@chakra-ui/react'; +import { Grid, GridItem, Skeleton } from '@chakra-ui/react'; import React from 'react'; import type { StatsChartsSection } from 'types/api/stats'; @@ -21,6 +21,8 @@ type Props = { interval: StatsIntervalIds; onIntervalChange: (newInterval: StatsIntervalIds) => void; onFilterInputChange: (q: string) => void; + isLoading: boolean; + initialFilterValue: string; } const StatsFilters = ({ @@ -30,8 +32,9 @@ const StatsFilters = ({ interval, onIntervalChange, onFilterInputChange, + isLoading, + initialFilterValue, }: Props) => { - const sectionsList = [ { id: 'all', title: 'All', @@ -51,22 +54,26 @@ const StatsFilters = ({ w={{ base: '100%', lg: 'auto' }} area="section" > - + { isLoading ? : ( + + ) } - + { isLoading ? : ( + + ) } + placeholder="Find chart, metric..." + initialValue={ initialFilterValue } + /> ); diff --git a/ui/stats/useStats.tsx b/ui/stats/useStats.tsx index 565c413b11..96117b9622 100644 --- a/ui/stats/useStats.tsx +++ b/ui/stats/useStats.tsx @@ -1,10 +1,11 @@ +import { useRouter } from 'next/router'; import React, { useCallback, useMemo, useState } from 'react'; import type { StatsChartInfo, StatsChartsSection } from 'types/api/stats'; import type { StatsIntervalIds } from 'types/client/stats'; import useApiQuery from 'lib/api/useApiQuery'; -import useDebounce from 'lib/hooks/useDebounce'; +import getQueryParamString from 'lib/router/getQueryParamString'; import { STATS_CHARTS } from 'stubs/stats'; function isSectionMatches(section: StatsChartsSection, currentSection: string): boolean { @@ -16,6 +17,8 @@ function isChartNameMatches(q: string, chart: StatsChartInfo) { } export default function useStats() { + const router = useRouter(); + const { data, isPlaceholderData, isError } = useApiQuery('stats_lines', { queryOptions: { placeholderData: STATS_CHARTS, @@ -24,22 +27,35 @@ export default function useStats() { const [ currentSection, setCurrentSection ] = useState('all'); const [ filterQuery, setFilterQuery ] = useState(''); + const [ initialFilterQuery, setInitialFilterQuery ] = React.useState(''); const [ interval, setInterval ] = useState('oneMonth'); const sectionIds = useMemo(() => data?.sections?.map(({ id }) => id), [ data ]); - const debouncedFilterQuery = useDebounce(filterQuery, 500); + React.useEffect(() => { + if (!isPlaceholderData && !isError) { + const chartId = getQueryParamString(router.query.chartId); + const chartName = data?.sections.map((section) => section.charts.find((chart) => chart.id === chartId)).filter(Boolean)[0]?.title; + if (chartName) { + setInitialFilterQuery(chartName); + setFilterQuery(chartName); + router.replace({ pathname: '/stats' }, undefined, { scroll: false }); + } + } + // run only when data is loaded + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ isPlaceholderData ]); const displayedCharts = React.useMemo(() => { return data?.sections ?.map((section) => { - const charts = section.charts.filter((chart) => isSectionMatches(section, currentSection) && isChartNameMatches(debouncedFilterQuery, chart)); + const charts = section.charts.filter((chart) => isSectionMatches(section, currentSection) && isChartNameMatches(filterQuery, chart)); return { ...section, charts, }; }).filter((section) => section.charts.length > 0); - }, [ currentSection, data?.sections, debouncedFilterQuery ]); + }, [ currentSection, data?.sections, filterQuery ]); const handleSectionChange = useCallback((newSection: string) => { setCurrentSection(newSection); @@ -58,6 +74,7 @@ export default function useStats() { sectionIds, isPlaceholderData, isError, + initialFilterQuery, filterQuery, currentSection, handleSectionChange, @@ -70,6 +87,7 @@ export default function useStats() { sectionIds, isPlaceholderData, isError, + initialFilterQuery, filterQuery, currentSection, handleSectionChange, diff --git a/ui/token/TokenContractInfo.tsx b/ui/token/TokenContractInfo.tsx new file mode 100644 index 0000000000..0d1e8bdf49 --- /dev/null +++ b/ui/token/TokenContractInfo.tsx @@ -0,0 +1,37 @@ +import type { UseQueryResult } from '@tanstack/react-query'; +import React from 'react'; + +import type { Address } from 'types/api/address'; +import type { TokenInfo } from 'types/api/token'; + +import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo'; + +interface Props { + tokenQuery: UseQueryResult; + contractQuery: UseQueryResult
; +} + +const TokenContractInfo = ({ tokenQuery, contractQuery }: Props) => { + // we show error in parent component, this is only for TS + if (tokenQuery.isError) { + return null; + } + + const address = { + hash: tokenQuery.data?.address || '', + is_contract: true, + implementation_name: null, + watchlist_names: [], + watchlist_address_id: null, + }; + + return ( + + ); +}; + +export default React.memo(TokenContractInfo); diff --git a/ui/token/TokenDetails.tsx b/ui/token/TokenDetails.tsx index aeac6ac8e2..7ed84eb3e6 100644 --- a/ui/token/TokenDetails.tsx +++ b/ui/token/TokenDetails.tsx @@ -11,6 +11,7 @@ import type { ResourceError } from 'lib/api/resources'; import useApiQuery from 'lib/api/useApiQuery'; import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; import getCurrencyValue from 'lib/getCurrencyValue'; +import useIsMounted from 'lib/hooks/useIsMounted'; import { TOKEN_COUNTERS } from 'stubs/token'; import type { TokenTabs } from 'ui/pages/Token'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; @@ -24,6 +25,7 @@ interface Props { const TokenDetails = ({ tokenQuery }: Props) => { const router = useRouter(); + const isMounted = useIsMounted(); const hash = router.query.hash?.toString(); const tokenCountersQuery = useApiQuery('token_counters', { @@ -65,6 +67,10 @@ const TokenDetails = ({ tokenQuery }: Props) => { throwOnResourceLoadError(tokenQuery); + if (!isMounted) { + return null; + } + const { exchange_rate: exchangeRate, total_supply: totalSupply, @@ -76,7 +82,7 @@ const TokenDetails = ({ tokenQuery }: Props) => { let totalSupplyValue; - if (type === 'ERC-20') { + if (decimals) { const totalValue = totalSupply ? getCurrencyValue({ value: totalSupply, accuracy: 3, accuracyUsd: 2, exchangeRate, decimals }) : undefined; totalSupplyValue = totalValue?.valueStr; } else { diff --git a/ui/token/TokenHolders/TokenHolders.tsx b/ui/token/TokenHolders/TokenHolders.tsx index 5b912d5aa8..5c8dfe5933 100644 --- a/ui/token/TokenHolders/TokenHolders.tsx +++ b/ui/token/TokenHolders/TokenHolders.tsx @@ -4,6 +4,8 @@ import React from 'react'; import type { TokenInfo } from 'types/api/token'; import useIsMobile from 'lib/hooks/useIsMobile'; +import useIsMounted from 'lib/hooks/useIsMounted'; +import AddressCsvExportLink from 'ui/address/AddressCsvExportLink'; import ActionBar from 'ui/shared/ActionBar'; import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataListDisplay from 'ui/shared/DataListDisplay'; @@ -16,17 +18,28 @@ import TokenHoldersTable from './TokenHoldersTable'; type Props = { token?: TokenInfo; holdersQuery: QueryWithPagesResult<'token_holders'>; + shouldRender?: boolean; } -const TokenHoldersContent = ({ holdersQuery, token }: Props) => { - +const TokenHoldersContent = ({ holdersQuery, token, shouldRender = true }: Props) => { const isMobile = useIsMobile(); + const isMounted = useIsMounted(); + + if (!isMounted || !shouldRender) { + return null; + } + if (holdersQuery.isError) { return ; } const actionBar = isMobile && holdersQuery.pagination.isVisible && ( + ); diff --git a/ui/token/TokenHolders/TokenHoldersListItem.tsx b/ui/token/TokenHolders/TokenHoldersListItem.tsx index cfe3b31a76..65dc4b2f3a 100644 --- a/ui/token/TokenHolders/TokenHoldersListItem.tsx +++ b/ui/token/TokenHolders/TokenHoldersListItem.tsx @@ -29,7 +29,7 @@ const TokenHoldersListItem = ({ holder, token, isLoading }: Props) => { /> - { token.type === 'ERC-1155' && 'token_id' in holder && ( + { (token.type === 'ERC-1155' || token.type === 'ERC-404') && 'token_id' in holder && ( <> ID# @@ -47,7 +47,7 @@ const TokenHoldersListItem = ({ holder, token, isLoading }: Props) => { - { token.total_supply && ( + { token.total_supply && token.type !== 'ERC-404' && ( <> Percentage diff --git a/ui/token/TokenHolders/TokenHoldersTable.tsx b/ui/token/TokenHolders/TokenHoldersTable.tsx index e44e981bb6..22c229cd96 100644 --- a/ui/token/TokenHolders/TokenHoldersTable.tsx +++ b/ui/token/TokenHolders/TokenHoldersTable.tsx @@ -19,9 +19,9 @@ const TokenHoldersTable = ({ data, token, top, isLoading }: Props) => {
HolderID#ID#QuantityPercentagePercentage
{ 'token_id' in holder && holder.token_id } @@ -38,7 +38,7 @@ const TokenTransferTableItem = ({ holder, token, isLoading }: Props) => { { quantity } ; tokenQuery: UseQueryResult>; ownerFilter?: string; + shouldRender?: boolean; } -const TokenInventory = ({ inventoryQuery, tokenQuery, ownerFilter }: Props) => { +const TokenInventory = ({ inventoryQuery, tokenQuery, ownerFilter, shouldRender = true }: Props) => { const isMobile = useIsMobile(); + const isMounted = useIsMounted(); const resetOwnerFilter = React.useCallback(() => { inventoryQuery.onFilterChange({}); }, [ inventoryQuery ]); + if (!isMounted || !shouldRender) { + return null; + } + const isActionBarHidden = !ownerFilter && !inventoryQuery.data?.items.length; const ownerFilterComponent = ownerFilter && ( @@ -68,7 +75,7 @@ const TokenInventory = ({ inventoryQuery, tokenQuery, ownerFilter }: Props) => { > { items.map((item, index) => ( { const mediaElement = ( ); diff --git a/ui/token/TokenPageTitle.tsx b/ui/token/TokenPageTitle.tsx new file mode 100644 index 0000000000..cf22c633e7 --- /dev/null +++ b/ui/token/TokenPageTitle.tsx @@ -0,0 +1,150 @@ +import { Box, Flex, Tooltip, useToken } from '@chakra-ui/react'; +import type { UseQueryResult } from '@tanstack/react-query'; +import React from 'react'; + +import type { Address } from 'types/api/address'; +import type { TokenInfo } from 'types/api/token'; +import type { EntityTag } from 'ui/shared/EntityTags/types'; + +import config from 'configs/app'; +import useAddressMetadataInfoQuery from 'lib/address/useAddressMetadataInfoQuery'; +import type { ResourceError } from 'lib/api/resources'; +import useApiQuery from 'lib/api/useApiQuery'; +import { useAppContext } from 'lib/contexts/app'; +import AddressQrCode from 'ui/address/details/AddressQrCode'; +import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu'; +import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet'; +import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import * as TokenEntity from 'ui/shared/entities/token/TokenEntity'; +import EntityTags from 'ui/shared/EntityTags/EntityTags'; +import formatUserTags from 'ui/shared/EntityTags/formatUserTags'; +import sortEntityTags from 'ui/shared/EntityTags/sortEntityTags'; +import IconSvg from 'ui/shared/IconSvg'; +import NetworkExplorers from 'ui/shared/NetworkExplorers'; +import PageTitle from 'ui/shared/Page/PageTitle'; + +import TokenVerifiedInfo from './TokenVerifiedInfo'; + +interface Props { + tokenQuery: UseQueryResult>; + addressQuery: UseQueryResult>; + hash: string; +} + +const TokenPageTitle = ({ tokenQuery, addressQuery, hash }: Props) => { + const appProps = useAppContext(); + const addressHash = !tokenQuery.isPlaceholderData ? (tokenQuery.data?.address || '') : ''; + + const verifiedInfoQuery = useApiQuery('token_verified_info', { + pathParams: { hash: addressHash, chainId: config.chain.id }, + queryOptions: { enabled: Boolean(tokenQuery.data) && !tokenQuery.isPlaceholderData && config.features.verifiedTokens.isEnabled }, + }); + + const addressesForMetadataQuery = React.useMemo(() => ([ hash ].filter(Boolean)), [ hash ]); + const addressMetadataQuery = useAddressMetadataInfoQuery(addressesForMetadataQuery); + + const isLoading = tokenQuery.isPlaceholderData || + addressQuery.isPlaceholderData || + (config.features.verifiedTokens.isEnabled && verifiedInfoQuery.isPending); + + const tokenSymbolText = tokenQuery.data?.symbol ? ` (${ tokenQuery.data.symbol })` : ''; + + const backLink = React.useMemo(() => { + const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/tokens'); + + if (!hasGoBackLink) { + return; + } + + return { + label: 'Back to tokens list', + url: appProps.referrer, + }; + }, [ appProps.referrer ]); + + const bridgedTokenTagBgColor = useToken('colors', 'blue.500'); + const bridgedTokenTagTextColor = useToken('colors', 'white'); + + const tags: Array = React.useMemo(() => { + return [ + tokenQuery.data ? { slug: tokenQuery.data?.type, name: tokenQuery.data?.type, tagType: 'custom' as const, ordinal: -20 } : undefined, + config.features.bridgedTokens.isEnabled && tokenQuery.data?.is_bridged ? + { + slug: 'bridged', + name: 'Bridged', + tagType: 'custom' as const, + ordinal: -10, + meta: { bgColor: bridgedTokenTagBgColor, textColor: bridgedTokenTagTextColor }, + } : + undefined, + ...formatUserTags(addressQuery.data), + verifiedInfoQuery.data?.projectSector ? + { slug: verifiedInfoQuery.data.projectSector, name: verifiedInfoQuery.data.projectSector, tagType: 'custom' as const, ordinal: -30 } : + undefined, + ...(addressMetadataQuery.data?.addresses?.[hash.toLowerCase()]?.tags || []), + ].filter(Boolean).sort(sortEntityTags); + }, [ + addressMetadataQuery.data?.addresses, + addressQuery.data, + bridgedTokenTagBgColor, + bridgedTokenTagTextColor, + tokenQuery.data, + verifiedInfoQuery.data?.projectSector, + hash, + ]); + + const contentAfter = ( + <> + { verifiedInfoQuery.data?.tokenAddress && ( + + + + + + ) } + + + ); + + const secondRow = ( + + + { !isLoading && tokenQuery.data && } + + + + + + + + ); + + return ( + + ) : null } + contentAfter={ contentAfter } + secondRow={ secondRow } + /> + ); +}; + +export default TokenPageTitle; diff --git a/ui/token/TokenProjectInfo/Content.tsx b/ui/token/TokenProjectInfo/Content.tsx index 91a7d80704..16cdfb4d79 100644 --- a/ui/token/TokenProjectInfo/Content.tsx +++ b/ui/token/TokenProjectInfo/Content.tsx @@ -14,7 +14,7 @@ interface Props { const SOCIAL_LINKS: Array> = [ { field: 'github', icon: 'social/github_filled', title: 'Github' }, - { field: 'twitter', icon: 'social/twitter_filled', title: 'Twitter' }, + { field: 'twitter', icon: 'social/twitter_filled', title: 'X (ex-Twitter)' }, { field: 'telegram', icon: 'social/telegram_filled', title: 'Telegram' }, { field: 'openSea', icon: 'social/opensea_filled', title: 'OpenSea' }, { field: 'linkedin', icon: 'social/linkedin_filled', title: 'LinkedIn' }, diff --git a/ui/token/TokenProjectInfo/TriggerButton.tsx b/ui/token/TokenProjectInfo/TriggerButton.tsx index 5398df16e1..9a96e937d5 100644 --- a/ui/token/TokenProjectInfo/TriggerButton.tsx +++ b/ui/token/TokenProjectInfo/TriggerButton.tsx @@ -17,10 +17,12 @@ const TriggerButton = ({ onClick }: Props, ref: React.ForwardedRef - + Info ); diff --git a/ui/token/TokenTransfer/TokenTransfer.tsx b/ui/token/TokenTransfer/TokenTransfer.tsx index 5c28a3d129..d8653e520d 100644 --- a/ui/token/TokenTransfer/TokenTransfer.tsx +++ b/ui/token/TokenTransfer/TokenTransfer.tsx @@ -7,6 +7,7 @@ import type { TokenInfo } from 'types/api/token'; import useGradualIncrement from 'lib/hooks/useGradualIncrement'; import useIsMobile from 'lib/hooks/useIsMobile'; +import useIsMounted from 'lib/hooks/useIsMounted'; import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketMessage from 'lib/socket/useSocketMessage'; import ActionBar from 'ui/shared/ActionBar'; @@ -21,10 +22,12 @@ type Props = { transfersQuery: QueryWithPagesResult<'token_transfers'> | QueryWithPagesResult<'token_instance_transfers'>; tokenId?: string; token?: TokenInfo; + shouldRender?: boolean; } -const TokenTransfer = ({ transfersQuery, tokenId, token }: Props) => { +const TokenTransfer = ({ transfersQuery, tokenId, token, shouldRender = true }: Props) => { const isMobile = useIsMobile(); + const isMounted = useIsMounted(); const router = useRouter(); const { isError, isPlaceholderData, data, pagination } = transfersQuery; @@ -55,8 +58,11 @@ const TokenTransfer = ({ transfersQuery, tokenId, token }: Props) => { handler: handleNewTransfersMessage, }); - const content = data?.items ? ( + if (!isMounted || !shouldRender) { + return null; + } + const content = data?.items ? ( <> { const timeAgo = useTimeAgoIncrement(timestamp, true); - const { usd, valueStr } = 'value' in total ? getCurrencyValue({ + const { usd, valueStr } = 'value' in total && total.value !== null ? getCurrencyValue({ value: total.value, exchangeRate: token.exchange_rate, accuracy: 8, @@ -87,7 +88,7 @@ const TokenTransferListItem = ({ ) } ) } - { 'token_id' in total && (token.type === 'ERC-721' || token.type === 'ERC-1155') && total.token_id !== null && ( + { 'token_id' in total && (NFT_TOKEN_TYPE_IDS.includes(token.type)) && total.token_id !== null && ( Txn hash MethodFrom/ToToken IDFrom/ToToken ID + { (tokenType === 'ERC-20' || tokenType === 'ERC-1155' || tokenType === 'ERC-404') && ( + { 'token_id' in total && total.token_id !== null ? ( ) } - { (token.type === 'ERC-20' || token.type === 'ERC-1155') && ( + { (token.type === 'ERC-20' || token.type === 'ERC-1155' || token.type === 'ERC-404') && ( { valueStr && ( diff --git a/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_default_erc20-mobile-1.png b/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_default_erc20-mobile-1.png index 78333858fb..ce065a90f0 100644 Binary files a/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_default_erc20-mobile-1.png and b/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_default_erc20-mobile-1.png differ diff --git a/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_default_erc721-mobile-1.png b/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_default_erc721-mobile-1.png index 29f273fc2b..e74f0267bd 100644 Binary files a/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_default_erc721-mobile-1.png and b/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_default_erc721-mobile-1.png differ diff --git a/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_mobile_erc20-mobile-1.png b/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_mobile_erc20-mobile-1.png index 7270ffe8b1..976f010181 100644 Binary files a/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_mobile_erc20-mobile-1.png and b/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_mobile_erc20-mobile-1.png differ diff --git a/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_mobile_erc721-mobile-1.png b/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_mobile_erc721-mobile-1.png index 559d804613..c5e0e1564d 100644 Binary files a/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_mobile_erc721-mobile-1.png and b/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_mobile_erc721-mobile-1.png differ diff --git a/ui/token/useTokenQuery.tsx b/ui/token/useTokenQuery.tsx new file mode 100644 index 0000000000..4359f299b6 --- /dev/null +++ b/ui/token/useTokenQuery.tsx @@ -0,0 +1,16 @@ +import useApiQuery from 'lib/api/useApiQuery'; +import { useAppContext } from 'lib/contexts/app'; +import * as tokenStubs from 'stubs/token'; + +export default function useTokenQuery(hash: string) { + const { apiData } = useAppContext<'/token/[hash]'>(); + + return useApiQuery('token', { + pathParams: { hash }, + queryOptions: { + enabled: Boolean(hash), + placeholderData: tokenStubs.TOKEN_INFO_ERC_20, + initialData: apiData || undefined, + }, + }); +} diff --git a/ui/tokenInfo/__screenshots__/TokenInfoForm.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png b/ui/tokenInfo/__screenshots__/TokenInfoForm.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png index 8eef0d0de8..3addedffff 100644 Binary files a/ui/tokenInfo/__screenshots__/TokenInfoForm.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png and b/ui/tokenInfo/__screenshots__/TokenInfoForm.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png differ diff --git a/ui/tokenInfo/__screenshots__/TokenInfoForm.pw.tsx_default_base-view-mobile-dark-mode-1.png b/ui/tokenInfo/__screenshots__/TokenInfoForm.pw.tsx_default_base-view-mobile-dark-mode-1.png index d1737b30ec..476c4bccfb 100644 Binary files a/ui/tokenInfo/__screenshots__/TokenInfoForm.pw.tsx_default_base-view-mobile-dark-mode-1.png and b/ui/tokenInfo/__screenshots__/TokenInfoForm.pw.tsx_default_base-view-mobile-dark-mode-1.png differ diff --git a/ui/tokenInfo/__screenshots__/TokenInfoForm.pw.tsx_default_status-IN-PROCESS-1.png b/ui/tokenInfo/__screenshots__/TokenInfoForm.pw.tsx_default_status-IN-PROCESS-1.png index a05db20f6f..de6e7ad7b7 100644 Binary files a/ui/tokenInfo/__screenshots__/TokenInfoForm.pw.tsx_default_status-IN-PROCESS-1.png and b/ui/tokenInfo/__screenshots__/TokenInfoForm.pw.tsx_default_status-IN-PROCESS-1.png differ diff --git a/ui/tokenInfo/__screenshots__/TokenInfoForm.pw.tsx_mobile_base-view-mobile-dark-mode-1.png b/ui/tokenInfo/__screenshots__/TokenInfoForm.pw.tsx_mobile_base-view-mobile-dark-mode-1.png index b64f67d6ac..05cc4a0efb 100644 Binary files a/ui/tokenInfo/__screenshots__/TokenInfoForm.pw.tsx_mobile_base-view-mobile-dark-mode-1.png and b/ui/tokenInfo/__screenshots__/TokenInfoForm.pw.tsx_mobile_base-view-mobile-dark-mode-1.png differ diff --git a/ui/tokenInfo/fields/TokenInfoFieldSocialLink.tsx b/ui/tokenInfo/fields/TokenInfoFieldSocialLink.tsx index c0521edf66..bd404b4678 100644 --- a/ui/tokenInfo/fields/TokenInfoFieldSocialLink.tsx +++ b/ui/tokenInfo/fields/TokenInfoFieldSocialLink.tsx @@ -21,7 +21,7 @@ const SETTINGS: Record = { linkedin: { label: 'LinkedIn', icon: 'social/linkedin_filled', color: 'linkedin' }, discord: { label: 'Discord', icon: 'social/discord_filled', color: 'discord' }, slack: { label: 'Slack', icon: 'social/slack_filled', color: 'slack' }, - twitter: { label: 'Twitter', icon: 'social/twitter_filled', color: 'twitter' }, + twitter: { label: 'X (ex-Twitter)', icon: 'social/twitter_filled', color: 'inherit' }, opensea: { label: 'OpenSea', icon: 'social/opensea_filled', color: 'opensea' }, facebook: { label: 'Facebook', icon: 'social/facebook_filled', color: 'facebook' }, medium: { label: 'Medium', icon: 'social/medium_filled', color: 'inherit' }, diff --git a/ui/tokenInstance/TokenInstanceDetails.tsx b/ui/tokenInstance/TokenInstanceDetails.tsx index 96c69ed59a..985a0837ee 100644 --- a/ui/tokenInstance/TokenInstanceDetails.tsx +++ b/ui/tokenInstance/TokenInstanceDetails.tsx @@ -74,7 +74,8 @@ const TokenInstanceDetails = ({ data, token, scrollRef, isLoading }: Props) => { { + return ( + + ); +}; + +export default TokenInstanceDivider; diff --git a/ui/tokens/Tokens.pw.tsx b/ui/tokens/Tokens.pw.tsx new file mode 100644 index 0000000000..0ebd702b17 --- /dev/null +++ b/ui/tokens/Tokens.pw.tsx @@ -0,0 +1,40 @@ +import { Box } from '@chakra-ui/react'; +import { test, expect } from '@playwright/experimental-ct-react'; +import React from 'react'; + +import * as tokens from 'mocks/tokens/tokenInfo'; +import TestApp from 'playwright/TestApp'; +import buildApiUrl from 'playwright/utils/buildApiUrl'; + +import Tokens from './Tokens'; + +const API_URL_TOKENS = buildApiUrl('tokens'); + +const tokensResponse = { + items: [ + tokens.tokenInfoERC20a, tokens.tokenInfoERC20b, tokens.tokenInfoERC20c, tokens.tokenInfoERC20d, + tokens.tokenInfoERC721a, tokens.tokenInfoERC721b, tokens.tokenInfoERC721c, + tokens.tokenInfoERC1155a, tokens.tokenInfoERC1155b, tokens.tokenInfoERC1155WithoutName, + ], + next_page_params: { + holder_count: 1, + items_count: 1, + name: 'a', + }, +}; + +test('base view +@mobile +@dark-mode', async({ mount, page }) => { + await page.route(API_URL_TOKENS, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(tokensResponse), + })); + + const component = await mount( + + + + , + ); + + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/tokens/__screenshots__/Tokens.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png b/ui/tokens/__screenshots__/Tokens.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..08e48b2317 Binary files /dev/null and b/ui/tokens/__screenshots__/Tokens.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png differ diff --git a/ui/tokens/__screenshots__/Tokens.pw.tsx_default_base-view-mobile-dark-mode-1.png b/ui/tokens/__screenshots__/Tokens.pw.tsx_default_base-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..afd886699c Binary files /dev/null and b/ui/tokens/__screenshots__/Tokens.pw.tsx_default_base-view-mobile-dark-mode-1.png differ diff --git a/ui/tokens/__screenshots__/Tokens.pw.tsx_mobile_base-view-mobile-dark-mode-1.png b/ui/tokens/__screenshots__/Tokens.pw.tsx_mobile_base-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..d8eb1e89c7 Binary files /dev/null and b/ui/tokens/__screenshots__/Tokens.pw.tsx_mobile_base-view-mobile-dark-mode-1.png differ diff --git a/ui/tx/FtTokenTransferSnippet.tsx b/ui/tx/FtTokenTransferSnippet.tsx new file mode 100644 index 0000000000..282eb406aa --- /dev/null +++ b/ui/tx/FtTokenTransferSnippet.tsx @@ -0,0 +1,37 @@ +import { chakra } from '@chakra-ui/react'; +import React from 'react'; + +import type { TokenInfo } from 'types/api/token'; + +import getCurrencyValue from 'lib/getCurrencyValue'; +import TokenEntity from 'ui/shared/entities/token/TokenEntity'; + +interface Props { + token: TokenInfo; + value: string; + decimals: string | null; +} +const FtTokenTransferSnippet = ({ token, value, decimals }: Props) => { + const { valueStr, usd } = getCurrencyValue({ + value: value, + exchangeRate: token.exchange_rate, + accuracyUsd: 2, + decimals: decimals, + }); + + return ( + <> + for + { valueStr } + + { usd && (${ usd }) } + + ); +}; + +export default React.memo(FtTokenTransferSnippet); diff --git a/ui/tx/NftTokenTransferSnippet.tsx b/ui/tx/NftTokenTransferSnippet.tsx index ece1fa15ef..be304f70db 100644 --- a/ui/tx/NftTokenTransferSnippet.tsx +++ b/ui/tx/NftTokenTransferSnippet.tsx @@ -15,6 +15,29 @@ interface Props { const NftTokenTransferSnippet = ({ value, token, tokenId }: Props) => { const num = value === '1' ? '' : value; + const tokenIdContent = (() => { + if (tokenId === null) { + // ERC-404 may not have an ID + if (token.type === 'ERC-404') { + return null; + } + return N/A ; + } + + return ( + + ); + + })(); + return ( <> { num ? ( @@ -26,18 +49,7 @@ const NftTokenTransferSnippet = ({ value, token, tokenId }: Props) => { ) : ( for token ID ) } - { tokenId !== null ? ( - - ) : N/A - } + { tokenIdContent } of (1); + + const ViewData = useMemo(() => (queryData ? generateFlowViewData(queryData) : []), [ queryData ]); + const chunkedViewData = _.chunk(ViewData, 50); + + const paginationProps: PaginationParams = useMemo(() => ({ + onNextPageClick: () => setPage(page + 1), + onPrevPageClick: () => setPage(page - 1), + resetPage: () => setPage(1), + canGoBackwards: page > 1, + isLoading: isPlaceholderData, + page: page, + hasNextPage: Boolean(chunkedViewData[page]), + hasPages: Boolean(chunkedViewData[1]), + isVisible: Boolean(chunkedViewData[1]), + }), [ chunkedViewData, page, isPlaceholderData ]); + + const data = chunkedViewData [page - 1]; + + const actionBar = ( + + + + + Wallet + + + + + + + + ); + + const content = ( + <> + + { data?.map((item, i) => ( + + )) } + + + + + + + + + + + + { data?.map((item, i) => ( + + )) } + +
+ Actions + + From/To +
+
+ + ); + + return ( + + ); +} diff --git a/ui/tx/TxBlobs.pw.tsx b/ui/tx/TxBlobs.pw.tsx new file mode 100644 index 0000000000..dcec52a170 --- /dev/null +++ b/ui/tx/TxBlobs.pw.tsx @@ -0,0 +1,39 @@ +import { test, expect } from '@playwright/experimental-ct-react'; +import React from 'react'; + +import * as blobsMock from 'mocks/blobs/blobs'; +import * as txMock from 'mocks/txs/tx'; +import TestApp from 'playwright/TestApp'; +import buildApiUrl from 'playwright/utils/buildApiUrl'; + +import TxBlobs from './TxBlobs'; +import type { TxQuery } from './useTxQuery'; + +const TX_BLOBS_API_URL = buildApiUrl('tx_blobs', { hash: txMock.base.hash }); +const hooksConfig = { + router: { + query: { hash: txMock.base.hash }, + }, +}; + +test('base view +@mobile', async({ mount, page }) => { + await page.route(TX_BLOBS_API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(blobsMock.txBlobs), + })); + + const txQuery = { + data: txMock.base, + isPlaceholderData: false, + isError: false, + } as TxQuery; + + const component = await mount( + + + , + { hooksConfig }, + ); + + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/tx/TxBlobs.tsx b/ui/tx/TxBlobs.tsx new file mode 100644 index 0000000000..fd10854681 --- /dev/null +++ b/ui/tx/TxBlobs.tsx @@ -0,0 +1,63 @@ +import { Hide, Show } from '@chakra-ui/react'; +import React from 'react'; + +import { TX_BLOB } from 'stubs/blobs'; +import { generateListStub } from 'stubs/utils'; +import ActionBar from 'ui/shared/ActionBar'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import Pagination from 'ui/shared/pagination/Pagination'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; + +import TxBlobsList from './blobs/TxBlobsList'; +import TxBlobsTable from './blobs/TxBlobsTable'; +import TxPendingAlert from './TxPendingAlert'; +import TxSocketAlert from './TxSocketAlert'; +import type { TxQuery } from './useTxQuery'; + +interface Props { + txQuery: TxQuery; +} + +const TxBlobs = ({ txQuery }: Props) => { + const { data, isPlaceholderData, isError, pagination } = useQueryWithPages({ + resourceName: 'tx_blobs', + pathParams: { hash: txQuery.data?.hash }, + options: { + enabled: !txQuery.isPlaceholderData && Boolean(txQuery.data?.hash) && Boolean(txQuery.data?.status), + placeholderData: generateListStub<'tx_blobs'>(TX_BLOB, 3, { next_page_params: null }), + }, + }); + + if (!txQuery.isPending && !txQuery.isPlaceholderData && !txQuery.isError && !txQuery.data.status) { + return txQuery.socketStatus ? : ; + } + + const content = data ? ( + <> + + + + + + + + ) : null; + + const actionBar = pagination.isVisible ? ( + + + + ) : null; + + return ( + + ); +}; + +export default TxBlobs; diff --git a/ui/tx/TxDetails.pw.tsx b/ui/tx/TxDetails.pw.tsx new file mode 100644 index 0000000000..b1f973d37f --- /dev/null +++ b/ui/tx/TxDetails.pw.tsx @@ -0,0 +1,204 @@ +import { test, expect } from '@playwright/experimental-ct-react'; +import React from 'react'; + +import * as txMock from 'mocks/txs/tx'; +import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; +import TestApp from 'playwright/TestApp'; +import buildApiUrl from 'playwright/utils/buildApiUrl'; +import * as configs from 'playwright/utils/configs'; + +import TxDetails from './TxDetails'; + +const API_URL = buildApiUrl('tx', { hash: '1' }); +const hooksConfig = { + router: { + query: { hash: 1 }, + }, +}; + +test('between addresses +@mobile +@dark-mode', async({ mount, page }) => { + await page.route(API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(txMock.base), + })); + + const component = await mount( + + + , + { hooksConfig }, + ); + + await page.getByText('View details').click(); + + await expect(component).toHaveScreenshot({ + mask: [ page.locator(configs.adsBannerSelector) ], + maskColor: configs.maskColor, + }); +}); + +test('creating contact', async({ mount, page }) => { + await page.route(API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(txMock.withContractCreation), + })); + + const component = await mount( + + + , + { hooksConfig }, + ); + + await expect(component).toHaveScreenshot({ + mask: [ page.locator(configs.adsBannerSelector) ], + maskColor: configs.maskColor, + }); +}); + +test('with token transfer +@mobile', async({ mount, page }) => { + await page.route(API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(txMock.withTokenTransfer), + })); + + const component = await mount( + + + , + { hooksConfig }, + ); + + await expect(component).toHaveScreenshot({ + mask: [ page.locator(configs.adsBannerSelector) ], + maskColor: configs.maskColor, + }); +}); + +test('with decoded revert reason', async({ mount, page }) => { + await page.route(API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(txMock.withDecodedRevertReason), + })); + + const component = await mount( + + + , + { hooksConfig }, + ); + + await expect(component).toHaveScreenshot({ + mask: [ page.locator(configs.adsBannerSelector) ], + maskColor: configs.maskColor, + }); +}); + +test('with decoded raw reason', async({ mount, page }) => { + await page.route(API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(txMock.withRawRevertReason), + })); + + const component = await mount( + + + , + { hooksConfig }, + ); + + await expect(component).toHaveScreenshot({ + mask: [ page.locator(configs.adsBannerSelector) ], + maskColor: configs.maskColor, + }); +}); + +test('pending', async({ mount, page }) => { + await page.route(API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(txMock.pending), + })); + + const component = await mount( + + + , + { hooksConfig }, + ); + + await page.getByText('View details').click(); + + await expect(component).toHaveScreenshot({ + mask: [ page.locator(configs.adsBannerSelector) ], + maskColor: configs.maskColor, + }); +}); + +test('with actions uniswap +@mobile +@dark-mode', async({ mount, page }) => { + await page.route(API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(txMock.withActionsUniswap), + })); + + const component = await mount( + + + , + { hooksConfig }, + ); + + await expect(component).toHaveScreenshot({ + mask: [ page.locator(configs.adsBannerSelector) ], + maskColor: configs.maskColor, + }); +}); + +const l2Test = test.extend({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + context: contextWithEnvs(configs.featureEnvs.rollup) as any, +}); + +l2Test('l2', async({ mount, page }) => { + await page.route(API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(txMock.l2tx), + })); + + const component = await mount( + + + , + { hooksConfig }, + ); + + await expect(component).toHaveScreenshot({ + mask: [ page.locator(configs.adsBannerSelector) ], + maskColor: configs.maskColor, + }); +}); + +const mainnetTest = test.extend({ + context: contextWithEnvs([ + { name: 'NEXT_PUBLIC_IS_TESTNET', value: 'false' }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ]) as any, +}); + +mainnetTest('without testnet warning', async({ mount, page }) => { + await page.route(API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(txMock.l2tx), + })); + + const component = await mount( + + + , + { hooksConfig }, + ); + + await expect(component).toHaveScreenshot({ + mask: [ page.locator(configs.adsBannerSelector) ], + maskColor: configs.maskColor, + }); +}); diff --git a/ui/tx/TxDetailsDegraded.tsx b/ui/tx/TxDetailsDegraded.tsx index d67441350c..e1bace40d9 100644 --- a/ui/tx/TxDetailsDegraded.tsx +++ b/ui/tx/TxDetailsDegraded.tsx @@ -7,12 +7,14 @@ import type { Transaction } from 'types/api/transaction'; import { SECOND } from 'lib/consts'; import dayjs from 'lib/date/dayjs'; +import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; import hexToDecimal from 'lib/hexToDecimal'; import { publicClient } from 'lib/web3/client'; import { GET_BLOCK, GET_TRANSACTION, GET_TRANSACTION_RECEIPT, GET_TRANSACTION_CONFIRMATIONS } from 'stubs/RPC'; import { unknownAddress } from 'ui/shared/address/utils'; import ServiceDegradationWarning from 'ui/shared/alerts/ServiceDegradationWarning'; import TestnetWarning from 'ui/shared/alerts/TestnetWarning'; +import isCustomAppError from 'ui/shared/AppError/isCustomAppError'; import DataFetchAlert from 'ui/shared/DataFetchAlert'; import TxInfo from './details/TxInfo'; @@ -141,8 +143,8 @@ const TxDetailsDegraded = ({ hash, txQuery }: Props) => { }, [ txQuery.setRefetchOnError ]); if (!query.data) { - if (originalError?.status === 404) { - throw Error('Not found', { cause: { status: 404 } as unknown as Error }); + if (originalError && isCustomAppError(originalError)) { + throwOnResourceLoadError({ resource: 'tx', error: originalError, isError: true }); } return ; diff --git a/ui/tx/TxSubHeading.pw.tsx b/ui/tx/TxSubHeading.pw.tsx index 93f51906fb..ce17a7b0fe 100644 --- a/ui/tx/TxSubHeading.pw.tsx +++ b/ui/tx/TxSubHeading.pw.tsx @@ -1,102 +1,61 @@ -import { test, expect } from '@playwright/experimental-ct-react'; import React from 'react'; import * as txMock from 'mocks/txs/tx'; import { txInterpretation } from 'mocks/txs/txInterpretation'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; -import buildApiUrl from 'playwright/utils/buildApiUrl'; -import * as configs from 'playwright/utils/configs'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import { test, expect } from 'playwright/lib'; import TxSubHeading from './TxSubHeading'; import type { TxQuery } from './useTxQuery'; const hash = '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193'; -const TX_INTERPRETATION_API_URL = buildApiUrl('tx_interpretation', { hash }); - const txQuery = { data: txMock.base, isPlaceholderData: false, isError: false, } as TxQuery; -test('no interpretation +@mobile', async({ mount }) => { - const component = await mount( - - - , - ); - - await expect(component).toHaveScreenshot(); -}); - -const bsInterpretationTest = test.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.featureEnvs.txInterpretation) as any, -}); - -bsInterpretationTest('with interpretation +@mobile +@dark-mode', async({ mount, page }) => { - await page.route(TX_INTERPRETATION_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(txInterpretation), - })); - - const component = await mount( - - - , - ); - - await expect(component).toHaveScreenshot(); -}); - -bsInterpretationTest('with interpretation and view all link +@mobile', async({ mount, page }) => { - await page.route(TX_INTERPRETATION_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify({ data: { summaries: [ ...txInterpretation.data.summaries, ...txInterpretation.data.summaries ] } }), - })); - - const component = await mount( - - - , - ); - +test('no interpretation +@mobile', async({ render }) => { + const component = await render(); await expect(component).toHaveScreenshot(); }); -bsInterpretationTest('no interpretation, has method called', async({ mount, page }) => { - await page.route(TX_INTERPRETATION_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify({ data: { summaries: [] } }), - })); - - const component = await mount( - - - , - ); - - await expect(component).toHaveScreenshot(); -}); - -bsInterpretationTest('no interpretation', async({ mount, page }) => { - const txPendingQuery = { - data: txMock.pending, - isPlaceholderData: false, - isError: false, - } as TxQuery; - await page.route(TX_INTERPRETATION_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify({ data: { summaries: [] } }), - })); - - const component = await mount( - - - , - ); - - await expect(component).toHaveScreenshot(); +test.describe('blockscout provider', () => { + test.beforeEach(async({ mockEnvs }) => { + await mockEnvs(ENVS_MAP.txInterpretation); + }); + + test('with interpretation +@mobile +@dark-mode', async({ render, mockApiResponse }) => { + await mockApiResponse('tx_interpretation', txInterpretation, { pathParams: { hash } }); + const component = await render(); + await expect(component).toHaveScreenshot(); + }); + + test('with interpretation and view all link +@mobile', async({ render, mockApiResponse }) => { + await mockApiResponse( + 'tx_interpretation', + { data: { summaries: [ ...txInterpretation.data.summaries, ...txInterpretation.data.summaries ] } }, + { pathParams: { hash } }, + ); + const component = await render(); + await expect(component).toHaveScreenshot(); + }); + + test('no interpretation, has method called', async({ render, mockApiResponse }) => { + await mockApiResponse('tx_interpretation', { data: { summaries: [] } }, { pathParams: { hash } }); + const component = await render(); + await expect(component).toHaveScreenshot(); + }); + + test('no interpretation', async({ render, mockApiResponse }) => { + const txPendingQuery = { + data: txMock.pending, + isPlaceholderData: false, + isError: false, + } as TxQuery; + await mockApiResponse('tx_interpretation', { data: { summaries: [] } }, { pathParams: { hash } }); + const component = await render(); + await expect(component).toHaveScreenshot(); + }); }); diff --git a/ui/tx/TxSubHeading.tsx b/ui/tx/TxSubHeading.tsx index e495a0ebee..7ee1336512 100644 --- a/ui/tx/TxSubHeading.tsx +++ b/ui/tx/TxSubHeading.tsx @@ -3,13 +3,15 @@ import React from 'react'; import config from 'configs/app'; import useApiQuery from 'lib/api/useApiQuery'; +import { NOVES_TRANSLATE } from 'stubs/noves/NovesTranslate'; import { TX_INTERPRETATION } from 'stubs/txInterpretation'; import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu'; +import { TX_ACTIONS_BLOCK_ID } from 'ui/shared/DetailsActionsWrapper'; import TxEntity from 'ui/shared/entities/tx/TxEntity'; import NetworkExplorers from 'ui/shared/NetworkExplorers'; -import { TX_ACTIONS_BLOCK_ID } from 'ui/tx/details/txDetailsActions/TxDetailsActionsWrapper'; -import TxInterpretation from 'ui/tx/interpretation/TxInterpretation'; +import TxInterpretation from 'ui/shared/tx/interpretation/TxInterpretation'; +import { createNovesSummaryObject } from './assetFlows/utils/createNovesSummaryObject'; import type { TxQuery } from './useTxQuery'; type Props = { @@ -18,25 +20,50 @@ type Props = { txQuery: TxQuery; } +const feature = config.features.txInterpretation; + const TxSubHeading = ({ hash, hasTag, txQuery }: Props) => { - const hasInterpretationFeature = config.features.txInterpretation.isEnabled; + const hasInterpretationFeature = feature.isEnabled; + const isNovesInterpretation = hasInterpretationFeature && feature.provider === 'noves'; const txInterpretationQuery = useApiQuery('tx_interpretation', { pathParams: { hash }, queryOptions: { - enabled: Boolean(hash) && hasInterpretationFeature, + enabled: Boolean(hash) && (hasInterpretationFeature && !isNovesInterpretation), placeholderData: TX_INTERPRETATION, }, }); + const novesInterpretationQuery = useApiQuery('noves_transaction', { + pathParams: { hash }, + queryOptions: { + enabled: Boolean(hash) && isNovesInterpretation, + placeholderData: NOVES_TRANSLATE, + }, + }); + const content = (() => { - const hasInterpretation = hasInterpretationFeature && + const hasNovesInterpretation = isNovesInterpretation && + (novesInterpretationQuery.isPlaceholderData || Boolean(novesInterpretationQuery.data?.classificationData.description)); + + const hasInternalInterpretation = (hasInterpretationFeature && !isNovesInterpretation) && (txInterpretationQuery.isPlaceholderData || Boolean(txInterpretationQuery.data?.data.summaries.length)); const hasViewAllInterpretationsLink = !txInterpretationQuery.isPlaceholderData && txInterpretationQuery.data?.data.summaries && txInterpretationQuery.data?.data.summaries.length > 1; - if (hasInterpretation) { + if (hasNovesInterpretation && novesInterpretationQuery.data) { + const novesSummary = createNovesSummaryObject(novesInterpretationQuery.data); + + return ( + + ); + } else if (hasInternalInterpretation) { return ( { + + return ( + + + + + + + + Action + + + + + + + + + + + + ); +}; + +export default React.memo(TxAssetFlowsListItem); diff --git a/ui/tx/assetFlows/TxAssetFlowsTableItem.tsx b/ui/tx/assetFlows/TxAssetFlowsTableItem.tsx new file mode 100644 index 0000000000..0e2056b038 --- /dev/null +++ b/ui/tx/assetFlows/TxAssetFlowsTableItem.tsx @@ -0,0 +1,28 @@ +import { Td, Tr } from '@chakra-ui/react'; +import React from 'react'; + +import NovesFromTo from 'ui/shared/Noves/NovesFromTo'; + +import NovesActionSnippet from './components/NovesActionSnippet'; +import type { NovesFlowViewItem } from './utils/generateFlowViewData'; + +type Props = { + isPlaceholderData: boolean; + item: NovesFlowViewItem; +}; + +const TxAssetFlowsTableItem = (props: Props) => { + + return ( +
+ + + +
+ + + + + + + + + { data.map((item, index) => ( + + )) } + +
Blob hashData typeSize, bytes
+ ); +}; + +export default TxInternalsTable; diff --git a/ui/tx/blobs/TxBlobsTableItem.tsx b/ui/tx/blobs/TxBlobsTableItem.tsx new file mode 100644 index 0000000000..4c82a784dd --- /dev/null +++ b/ui/tx/blobs/TxBlobsTableItem.tsx @@ -0,0 +1,34 @@ +import { Tr, Td, Skeleton } from '@chakra-ui/react'; +import React from 'react'; + +import type { TxBlob } from 'types/api/blobs'; + +import BlobDataType from 'ui/shared/blob/BlobDataType'; +import BlobEntity from 'ui/shared/entities/blob/BlobEntity'; + +interface Props { + data: TxBlob; + isLoading?: boolean; +} + +const TxBlobsTableItem = ({ data, isLoading }: Props) => { + const size = data.blob_data ? data.blob_data.replace('0x', '').length / 2 : '-'; + + return ( + + + + + + { data.blob_data ? : '-' } + + + + { size.toLocaleString() } + + + + ); +}; + +export default React.memo(TxBlobsTableItem); diff --git a/ui/tx/details/TxDetailsAction.tsx b/ui/tx/details/TxDetailsAction.tsx new file mode 100644 index 0000000000..61e4cbc2ba --- /dev/null +++ b/ui/tx/details/TxDetailsAction.tsx @@ -0,0 +1,146 @@ +import { Flex, Link, Icon, chakra } from '@chakra-ui/react'; +import BigNumber from 'bignumber.js'; +import React from 'react'; + +import type { TxAction, TxActionGeneral } from 'types/api/txAction'; + +import { route } from 'nextjs-routes'; + +import config from 'configs/app'; +import uniswapIcon from 'icons/uniswap.svg'; +import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import TokenEntity from 'ui/shared/entities/token/TokenEntity'; + +interface Props { + action: TxAction; +} + +function getActionText(actionType: TxActionGeneral['type']) { + switch (actionType) { + case 'mint': return [ 'Add', 'Liquidity to' ]; + case 'burn': return [ 'Remove', 'Liquidity from' ]; + case 'collect': return [ 'Collect', 'From' ]; + case 'swap': return [ 'Swap', 'On' ]; + } +} + +const TxDetailsAction = ({ action }: Props) => { + const { protocol, type, data } = action; + + if (protocol !== 'uniswap_v3') { + return null; + } + + switch (type) { + case 'mint': + case 'burn': + case 'collect': + case 'swap': { + const amount0 = BigNumber(data.amount0).toFormat(); + const amount1 = BigNumber(data.amount1).toFormat(); + const [ text0, text1 ] = getActionText(type); + const token0 = { + address: data.symbol0 === 'Ether' ? '' : data.address0, + name: data.symbol0 === 'Ether' ? config.chain.currency.symbol || null : data.symbol0, + type: 'ERC-20', + symbol: null, + icon_url: null, + }; + const token1 = { + address: data.symbol1 === 'Ether' ? '' : data.address1, + name: data.symbol1 === 'Ether' ? config.chain.currency.symbol || null : data.symbol1, + type: 'ERC-20', + symbol: null, + icon_url: null, + }; + + return ( + + { text0 }: + + { amount0 } + + + { type === 'swap' ? 'For' : 'And' }: + + { amount1 } + + + { text1 } + + + Uniswap V3 + + + ); + } + + case 'mint_nft' : { + const token = { + address: data.address, + name: data.name, + type: 'ERC-20', + symbol: null, + icon_url: null, + }; + + return ( +
+ + Mint of + + to + + + + + { + data.ids.map((id: string) => { + const url = route({ pathname: '/token/[hash]/instance/[id]', query: { hash: data.address, id } }); + return ( + + 1 of + Token ID [ + { id } + ] + + ); + }) + } + +
+ ); + } + + default: + return null; + } +}; + +export default React.memo(TxDetailsAction); diff --git a/ui/tx/details/TxDetailsActions.tsx b/ui/tx/details/TxDetailsActions.tsx new file mode 100644 index 0000000000..c3c031edc6 --- /dev/null +++ b/ui/tx/details/TxDetailsActions.tsx @@ -0,0 +1,61 @@ +import { Flex, useColorModeValue } from '@chakra-ui/react'; +import React from 'react'; + +import type { TxAction } from 'types/api/txAction'; + +import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; + +import TxDetailsAction from './TxDetailsAction'; + +interface Props { + actions: Array; +} + +const TxDetailsActions = ({ actions }: Props) => { + const containerRef = React.useRef(null); + const [ hasScroll, setHasScroll ] = React.useState(false); + + const gradientStartColor = useColorModeValue('whiteAlpha.600', 'blackAlpha.600'); + const gradientEndColor = useColorModeValue('whiteAlpha.900', 'blackAlpha.900'); + + React.useEffect(() => { + if (!containerRef.current) { + return; + } + setHasScroll(containerRef.current.scrollHeight > containerRef.current.clientHeight); + }, []); + + return ( + + + { actions.map((action, index: number) => ) } + + + ); +}; + +export default React.memo(TxDetailsActions); diff --git a/ui/tx/details/TxDetailsBurntFees.tsx b/ui/tx/details/TxDetailsBurntFees.tsx new file mode 100644 index 0000000000..dc613035c5 --- /dev/null +++ b/ui/tx/details/TxDetailsBurntFees.tsx @@ -0,0 +1,54 @@ +import BigNumber from 'bignumber.js'; +import React from 'react'; + +import type { Transaction } from 'types/api/transaction'; + +import config from 'configs/app'; +import { ZERO } from 'lib/consts'; +import { currencyUnits } from 'lib/units'; +import CurrencyValue from 'ui/shared/CurrencyValue'; +import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; +import IconSvg from 'ui/shared/IconSvg'; + +const rollupFeature = config.features.rollup; + +interface Props { + data: Transaction; + isLoading?: boolean; +} + +const TxDetailsBurntFees = ({ data, isLoading }: Props) => { + + if (config.UI.views.tx.hiddenFields?.burnt_fees || (rollupFeature.isEnabled && rollupFeature.type === 'optimistic')) { + return null; + } + + const value = BigNumber(data.tx_burnt_fee || 0).plus(BigNumber(data.blob_gas_used || 0).multipliedBy(BigNumber(data.blob_gas_price || 0))); + + if (value.isEqualTo(ZERO)) { + return null; + } + + return ( + + + + + ); +}; + +export default React.memo(TxDetailsBurntFees); diff --git a/ui/tx/details/TxDetailsOther.tsx b/ui/tx/details/TxDetailsOther.tsx index 97f5d4114a..2ca2450978 100644 --- a/ui/tx/details/TxDetailsOther.tsx +++ b/ui/tx/details/TxDetailsOther.tsx @@ -21,6 +21,7 @@ const TxDetailsOther = ({ nonce, type, position }: Props) => { Txn type: { type } { type === 2 && (EIP-1559) } + { type === 3 && (EIP-4844) } ), diff --git a/ui/tx/details/TxDetailsTokenTransfer.tsx b/ui/tx/details/TxDetailsTokenTransfer.tsx index 43bfbde8d2..5c5762b184 100644 --- a/ui/tx/details/TxDetailsTokenTransfer.tsx +++ b/ui/tx/details/TxDetailsTokenTransfer.tsx @@ -1,13 +1,19 @@ -import { Flex, chakra } from '@chakra-ui/react'; +import { Flex } from '@chakra-ui/react'; import React from 'react'; -import type { TokenTransfer as TTokenTransfer, Erc20TotalPayload, Erc721TotalPayload, Erc1155TotalPayload } from 'types/api/tokenTransfer'; +import type { + TokenTransfer as TTokenTransfer, + Erc20TotalPayload, + Erc721TotalPayload, + Erc1155TotalPayload, + Erc404TotalPayload, +} from 'types/api/tokenTransfer'; -import getCurrencyValue from 'lib/getCurrencyValue'; import AddressFromTo from 'ui/shared/address/AddressFromTo'; -import TokenEntity from 'ui/shared/entities/token/TokenEntity'; import NftTokenTransferSnippet from 'ui/tx/NftTokenTransferSnippet'; +import FtTokenTransferSnippet from '../FtTokenTransferSnippet'; + interface Props { data: TTokenTransfer; } @@ -18,26 +24,7 @@ const TxDetailsTokenTransfer = ({ data }: Props) => { switch (data.token.type) { case 'ERC-20': { const total = data.total as Erc20TotalPayload; - const { valueStr, usd } = getCurrencyValue({ - value: total.value, - exchangeRate: data.token.exchange_rate, - accuracyUsd: 2, - decimals: total.decimals, - }); - - return ( - <> - for - { valueStr } - - { usd && (${ usd }) } - - ); + return ; } case 'ERC-721': { @@ -62,6 +49,26 @@ const TxDetailsTokenTransfer = ({ data }: Props) => { /> ); } + + case 'ERC-404': { + const total = data.total as Erc404TotalPayload; + + if (total.token_id !== null) { + return ( + + ); + } else { + if (total.value === null) { + return null; + } + + return ; + } + } } })(); diff --git a/ui/tx/details/TxDetailsWithdrawalStatus.pw.tsx b/ui/tx/details/TxDetailsWithdrawalStatus.pw.tsx index b4cc8c6171..4a3e58473f 100644 --- a/ui/tx/details/TxDetailsWithdrawalStatus.pw.tsx +++ b/ui/tx/details/TxDetailsWithdrawalStatus.pw.tsx @@ -1,12 +1,10 @@ import { Box } from '@chakra-ui/react'; -import { test as base, expect } from '@playwright/experimental-ct-react'; import React from 'react'; import type { OptimisticL2WithdrawalStatus } from 'types/api/optimisticL2'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; -import * as configs from 'playwright/utils/configs'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import { test, expect } from 'playwright/lib'; import TxDetailsWithdrawalStatus from './TxDetailsWithdrawalStatus'; @@ -16,20 +14,13 @@ const statuses: Array = [ 'Relayed', ]; -const test = base.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.featureEnvs.optimisticRollup) as any, -}); - statuses.forEach((status) => { - test(`status="${ status }"`, async({ mount }) => { - - const component = await mount( - - - - - , + test(`status="${ status }"`, async({ render, mockEnvs }) => { + await mockEnvs(ENVS_MAP.optimisticRollup); + const component = await render( + + + , ); await expect(component).toHaveScreenshot(); diff --git a/ui/tx/details/TxDetailsWithdrawalStatus.tsx b/ui/tx/details/TxDetailsWithdrawalStatus.tsx index b2b7c88ce4..a1b4af7d8a 100644 --- a/ui/tx/details/TxDetailsWithdrawalStatus.tsx +++ b/ui/tx/details/TxDetailsWithdrawalStatus.tsx @@ -4,6 +4,7 @@ import React from 'react'; import type { OptimisticL2WithdrawalStatus } from 'types/api/optimisticL2'; import { WITHDRAWAL_STATUSES } from 'types/api/optimisticL2'; +import config from 'configs/app'; import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1'; import VerificationSteps from 'ui/shared/verificationSteps/VerificationSteps'; @@ -12,8 +13,10 @@ interface Props { l1TxHash: string | undefined; } +const rollupFeature = config.features.rollup; + const TxDetailsWithdrawalStatus = ({ status, l1TxHash }: Props) => { - if (!status || !WITHDRAWAL_STATUSES.includes(status)) { + if (!status || !WITHDRAWAL_STATUSES.includes(status) || !rollupFeature.isEnabled || rollupFeature.type !== 'optimistic') { return null; } @@ -46,7 +49,7 @@ const TxDetailsWithdrawalStatus = ({ status, l1TxHash }: Props) => { variant="outline" size="sm" as="a" - href="https://app.optimism.io/bridge/withdraw" + href={ rollupFeature.L2WithdrawalUrl } target="_blank" > Claim funds diff --git a/ui/tx/details/TxInfo.pw.tsx b/ui/tx/details/TxInfo.pw.tsx index 262b6189af..2a1d6449d3 100644 --- a/ui/tx/details/TxInfo.pw.tsx +++ b/ui/tx/details/TxInfo.pw.tsx @@ -1,26 +1,14 @@ -import { test, expect } from '@playwright/experimental-ct-react'; import React from 'react'; import * as txMock from 'mocks/txs/tx'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import { test, expect } from 'playwright/lib'; import * as configs from 'playwright/utils/configs'; import TxInfo from './TxInfo'; -const hooksConfig = { - router: { - query: { hash: 1 }, - }, -}; - -test('between addresses +@mobile +@dark-mode', async({ mount, page }) => { - const component = await mount( - - - , - { hooksConfig }, - ); +test('between addresses +@mobile +@dark-mode', async({ render, page }) => { + const component = await render(); await page.getByText('View details').click(); @@ -30,13 +18,8 @@ test('between addresses +@mobile +@dark-mode', async({ mount, page }) => { }); }); -test('creating contact', async({ mount, page }) => { - const component = await mount( - - - , - { hooksConfig }, - ); +test('creating contact', async({ render, page }) => { + const component = await render(); await expect(component).toHaveScreenshot({ mask: [ page.locator(configs.adsBannerSelector) ], @@ -44,13 +27,8 @@ test('creating contact', async({ mount, page }) => { }); }); -test('with token transfer +@mobile', async({ mount, page }) => { - const component = await mount( - - - , - { hooksConfig }, - ); +test('with token transfer +@mobile', async({ render, page }) => { + const component = await render(); await expect(component).toHaveScreenshot({ mask: [ page.locator(configs.adsBannerSelector) ], @@ -58,13 +36,8 @@ test('with token transfer +@mobile', async({ mount, page }) => { }); }); -test('with decoded revert reason', async({ mount, page }) => { - const component = await mount( - - - , - { hooksConfig }, - ); +test('with decoded revert reason', async({ render, page }) => { + const component = await render(); await expect(component).toHaveScreenshot({ mask: [ page.locator(configs.adsBannerSelector) ], @@ -72,13 +45,8 @@ test('with decoded revert reason', async({ mount, page }) => { }); }); -test('with decoded raw reason', async({ mount, page }) => { - const component = await mount( - - - , - { hooksConfig }, - ); +test('with decoded raw reason', async({ render, page }) => { + const component = await render(); await expect(component).toHaveScreenshot({ mask: [ page.locator(configs.adsBannerSelector) ], @@ -86,13 +54,8 @@ test('with decoded raw reason', async({ mount, page }) => { }); }); -test('pending', async({ mount, page }) => { - const component = await mount( - - - , - { hooksConfig }, - ); +test('pending', async({ render, page }) => { + const component = await render(); await page.getByText('View details').click(); @@ -102,13 +65,8 @@ test('pending', async({ mount, page }) => { }); }); -test('with actions uniswap +@mobile +@dark-mode', async({ mount, page }) => { - const component = await mount( - - - , - { hooksConfig }, - ); +test('with actions uniswap +@mobile +@dark-mode', async({ render, page }) => { + const component = await render(); await expect(component).toHaveScreenshot({ mask: [ page.locator(configs.adsBannerSelector) ], @@ -116,18 +74,10 @@ test('with actions uniswap +@mobile +@dark-mode', async({ mount, page }) => { }); }); -const l2Test = test.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.featureEnvs.optimisticRollup) as any, -}); +test('with blob', async({ render, page }) => { + const component = await render(); -l2Test('l2', async({ mount, page }) => { - const component = await mount( - - - , - { hooksConfig }, - ); + await page.getByText('View details').click(); await expect(component).toHaveScreenshot({ mask: [ page.locator(configs.adsBannerSelector) ], @@ -135,20 +85,20 @@ l2Test('l2', async({ mount, page }) => { }); }); -const mainnetTest = test.extend({ - context: contextWithEnvs([ - { name: 'NEXT_PUBLIC_IS_TESTNET', value: 'false' }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ]) as any, +test('l2', async({ render, page, mockEnvs }) => { + await mockEnvs(ENVS_MAP.optimisticRollup); + const component = await render(); + await expect(component).toHaveScreenshot({ + mask: [ page.locator(configs.adsBannerSelector) ], + maskColor: configs.maskColor, + }); }); -mainnetTest('without testnet warning', async({ mount, page }) => { - const component = await mount( - - - , - { hooksConfig }, - ); +test('without testnet warning', async({ render, page, mockEnvs }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_IS_TESTNET', 'false' ], + ]); + const component = await render(); await expect(component).toHaveScreenshot({ mask: [ page.locator(configs.adsBannerSelector) ], @@ -156,18 +106,9 @@ mainnetTest('without testnet warning', async({ mount, page }) => { }); }); -const stabilityTest = test.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.stabilityEnvs) as any, -}); - -stabilityTest('stability customization', async({ mount, page }) => { - const component = await mount( - - - , - { hooksConfig }, - ); +test('stability customization', async({ render, page, mockEnvs }) => { + await mockEnvs(ENVS_MAP.stabilityEnvs); + const component = await render(); await expect(component).toHaveScreenshot({ mask: [ page.locator(configs.adsBannerSelector) ], diff --git a/ui/tx/details/TxInfo.tsx b/ui/tx/details/TxInfo.tsx index cabd988134..f002a39071 100644 --- a/ui/tx/details/TxInfo.tsx +++ b/ui/tx/details/TxInfo.tsx @@ -17,6 +17,7 @@ import { scroller, Element } from 'react-scroll'; import type { Transaction } from 'types/api/transaction'; import { ZKEVM_L2_TX_STATUSES } from 'types/api/transaction'; +import { ZKSYNC_L2_TX_BATCH_STATUSES } from 'types/api/zkSyncL2'; import { route } from 'nextjs-routes'; @@ -45,6 +46,7 @@ import TxFeeStability from 'ui/shared/tx/TxFeeStability'; import Utilization from 'ui/shared/Utilization/Utilization'; import VerificationSteps from 'ui/shared/verificationSteps/VerificationSteps'; import TxDetailsActions from 'ui/tx/details/txDetailsActions/TxDetailsActions'; +import TxDetailsBurntFees from 'ui/tx/details/TxDetailsBurntFees'; import TxDetailsFeePerGas from 'ui/tx/details/TxDetailsFeePerGas'; import TxDetailsGasPrice from 'ui/tx/details/TxDetailsGasPrice'; import TxDetailsOther from 'ui/tx/details/TxDetailsOther'; @@ -53,6 +55,7 @@ import TxDetailsWithdrawalStatus from 'ui/tx/details/TxDetailsWithdrawalStatus'; import TxRevertReason from 'ui/tx/details/TxRevertReason'; import TxAllowedPeekers from 'ui/tx/TxAllowedPeekers'; import TxSocketAlert from 'ui/tx/TxSocketAlert'; +import ZkSyncL2TxnBatchHashesInfo from 'ui/txnBatches/zkSyncL2/ZkSyncL2TxnBatchHashesInfo'; const rollupFeature = config.features.rollup; @@ -108,6 +111,15 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { return ( + + { config.features.metasuites.isEnabled && ( + <> + + + + + ) } + { socketStatus && ( @@ -124,9 +136,20 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
+ + { config.features.metasuites.isEnabled && ( + <> + + + + ) } @@ -137,7 +160,8 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { ) } - { rollupFeature.isEnabled && rollupFeature.type === 'optimistic' && data.op_withdrawals && data.op_withdrawals.length > 0 && ( + { rollupFeature.isEnabled && rollupFeature.type === 'optimistic' && data.op_withdrawals && data.op_withdrawals.length > 0 && + !config.UI.views.tx.hiddenFields?.L1_status && ( { ) } - { data.zkevm_status && ( + { data.zkevm_status && !config.UI.views.tx.hiddenFields?.L1_status && ( { ) } + { data.zksync && !config.UI.views.tx.hiddenFields?.L1_status && ( + + + + ) } { ) } - { data.zkevm_batch_number && ( + { data.zkevm_batch_number && !config.UI.views.tx.hiddenFields?.batch && ( { /> ) } + { data.zksync && !config.UI.views.tx.hiddenFields?.batch && ( + + { data.zksync.batch_number ? ( + + ) : Pending } + + ) } { data.timestamp && ( { { !config.UI.views.tx.hiddenFields?.tx_fee && ( { data.stability_fee ? ( @@ -422,21 +469,7 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { ) } ) } - { data.tx_burnt_fee && !config.UI.views.tx.hiddenFields?.burnt_fees && !(rollupFeature.isEnabled && rollupFeature.type === 'optimistic') && ( - - - - - ) } + { rollupFeature.isEnabled && rollupFeature.type === 'optimistic' && ( <> { data.l1_gas_used && ( @@ -502,6 +535,50 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { { isExpanded && ( <> + { (data.blob_gas_used || data.max_fee_per_blob_gas || data.blob_gas_price) && ( + <> + { data.blob_gas_used && data.blob_gas_price && ( + + + + ) } + { data.blob_gas_used && ( + + { BigNumber(data.blob_gas_used).toFormat() } + + ) } + { (data.max_fee_per_blob_gas || data.blob_gas_price) && ( + + { data.blob_gas_price && ( + { BigNumber(data.blob_gas_price).dividedBy(WEI_IN_GWEI).toFixed() } + ) } + { (data.max_fee_per_blob_gas && data.blob_gas_price) && } + { data.max_fee_per_blob_gas && ( + <> + Max: + { BigNumber(data.max_fee_per_blob_gas).dividedBy(WEI_IN_GWEI).toFixed() } + + ) } + + ) } + + + ) } { ) } + { data.zksync && } ) } diff --git a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_dark-color-mode_between-addresses-mobile-dark-mode-1.png b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_dark-color-mode_between-addresses-mobile-dark-mode-1.png index 91953a5e74..b5e623d053 100644 Binary files a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_dark-color-mode_between-addresses-mobile-dark-mode-1.png and b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_dark-color-mode_between-addresses-mobile-dark-mode-1.png differ diff --git a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_dark-color-mode_with-actions-uniswap-mobile-dark-mode-1.png b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_dark-color-mode_with-actions-uniswap-mobile-dark-mode-1.png index 8a71ebe581..345cf10948 100644 Binary files a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_dark-color-mode_with-actions-uniswap-mobile-dark-mode-1.png and b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_dark-color-mode_with-actions-uniswap-mobile-dark-mode-1.png differ diff --git a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_between-addresses-mobile-dark-mode-1.png b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_between-addresses-mobile-dark-mode-1.png index 1e3bee9a6c..5e9f01bc8b 100644 Binary files a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_between-addresses-mobile-dark-mode-1.png and b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_between-addresses-mobile-dark-mode-1.png differ diff --git a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_creating-contact-1.png b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_creating-contact-1.png index 5fb55385df..3dbaa56157 100644 Binary files a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_creating-contact-1.png and b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_creating-contact-1.png differ diff --git a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_l2-1.png b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_l2-1.png index e86df21951..4bdb91a0bc 100644 Binary files a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_l2-1.png and b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_l2-1.png differ diff --git a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_pending-1.png b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_pending-1.png index 560ea1cf2e..de3f4e9191 100644 Binary files a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_pending-1.png and b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_pending-1.png differ diff --git a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_stability-customization-1.png b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_stability-customization-1.png index 8dffcc2499..93e060022d 100644 Binary files a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_stability-customization-1.png and b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_stability-customization-1.png differ diff --git a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-actions-uniswap-mobile-dark-mode-1.png b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-actions-uniswap-mobile-dark-mode-1.png index cef58d0109..11f5bbf5ea 100644 Binary files a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-actions-uniswap-mobile-dark-mode-1.png and b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-actions-uniswap-mobile-dark-mode-1.png differ diff --git a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-blob-1.png b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-blob-1.png new file mode 100644 index 0000000000..a2f39211a5 Binary files /dev/null and b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-blob-1.png differ diff --git a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-decoded-raw-reason-1.png b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-decoded-raw-reason-1.png index e95db83c7c..0e942b7d8d 100644 Binary files a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-decoded-raw-reason-1.png and b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-decoded-raw-reason-1.png differ diff --git a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-decoded-revert-reason-1.png b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-decoded-revert-reason-1.png index 6ad9c3e012..c4bd37fd61 100644 Binary files a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-decoded-revert-reason-1.png and b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-decoded-revert-reason-1.png differ diff --git a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-token-transfer-mobile-1.png b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-token-transfer-mobile-1.png index 14b7765c09..60899f1733 100644 Binary files a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-token-transfer-mobile-1.png and b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-token-transfer-mobile-1.png differ diff --git a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_without-testnet-warning-1.png b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_without-testnet-warning-1.png index 736a3bcad7..ec1255d1ad 100644 Binary files a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_without-testnet-warning-1.png and b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_without-testnet-warning-1.png differ diff --git a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_mobile_between-addresses-mobile-dark-mode-1.png b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_mobile_between-addresses-mobile-dark-mode-1.png index 0aadfa46a6..5eff7e6a3e 100644 Binary files a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_mobile_between-addresses-mobile-dark-mode-1.png and b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_mobile_between-addresses-mobile-dark-mode-1.png differ diff --git a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_mobile_with-actions-uniswap-mobile-dark-mode-1.png b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_mobile_with-actions-uniswap-mobile-dark-mode-1.png index 647188a354..f4cd1283de 100644 Binary files a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_mobile_with-actions-uniswap-mobile-dark-mode-1.png and b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_mobile_with-actions-uniswap-mobile-dark-mode-1.png differ diff --git a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_mobile_with-token-transfer-mobile-1.png b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_mobile_with-token-transfer-mobile-1.png index 0c959f9478..7eaca093a8 100644 Binary files a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_mobile_with-token-transfer-mobile-1.png and b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_mobile_with-token-transfer-mobile-1.png differ diff --git a/ui/tx/details/txDetailsActions/TxDetailsActionsInterpretation.tsx b/ui/tx/details/txDetailsActions/TxDetailsActionsInterpretation.tsx index ade6ac072a..00e7401d84 100644 --- a/ui/tx/details/txDetailsActions/TxDetailsActionsInterpretation.tsx +++ b/ui/tx/details/txDetailsActions/TxDetailsActionsInterpretation.tsx @@ -2,10 +2,9 @@ import React from 'react'; import useApiQuery from 'lib/api/useApiQuery'; import { TX_INTERPRETATION } from 'stubs/txInterpretation'; +import DetailsActionsWrapper from 'ui/shared/DetailsActionsWrapper'; import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider'; -import TxInterpretation from 'ui/tx/interpretation/TxInterpretation'; - -import TxDetailsActionsWrapper from './TxDetailsActionsWrapper'; +import TxInterpretation from 'ui/shared/tx/interpretation/TxInterpretation'; interface Props { hash?: string; @@ -30,7 +29,7 @@ const TxDetailsActionsInterpretation = ({ hash, isTxDataLoading }: Props) => { return ( <> - + { actions.map((action, index: number) => ( { /> ), ) } - + ); diff --git a/ui/tx/details/txDetailsActions/TxDetailsActionsRaw.tsx b/ui/tx/details/txDetailsActions/TxDetailsActionsRaw.tsx index a024347519..b2afdb2043 100644 --- a/ui/tx/details/txDetailsActions/TxDetailsActionsRaw.tsx +++ b/ui/tx/details/txDetailsActions/TxDetailsActionsRaw.tsx @@ -2,10 +2,10 @@ import React from 'react'; import type { TxAction } from 'types/api/txAction'; +import DetailsActionsWrapper from 'ui/shared/DetailsActionsWrapper'; import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider'; import TxDetailsAction from './TxDetailsAction'; -import TxDetailsActionsWrapper from './TxDetailsActionsWrapper'; interface Props { actions: Array; @@ -15,9 +15,9 @@ interface Props { const TxDetailsActionsRaw = ({ actions, isLoading }: Props) => { return ( <> - + { actions.map((action, index: number) => ) } - + ); diff --git a/ui/tx/internals/TxInternalsList.tsx b/ui/tx/internals/TxInternalsList.tsx index f756c12eab..b057015525 100644 --- a/ui/tx/internals/TxInternalsList.tsx +++ b/ui/tx/internals/TxInternalsList.tsx @@ -8,7 +8,7 @@ import TxInternalsListItem from 'ui/tx/internals/TxInternalsListItem'; const TxInternalsList = ({ data, isLoading }: { data: Array; isLoading?: boolean }) => { return ( - { data.map((item, index) => ) } + { data.map((item, index) => ) } ); }; diff --git a/ui/tx/internals/TxInternalsTable.tsx b/ui/tx/internals/TxInternalsTable.tsx index 2a833310c6..aeb8ce7e48 100644 --- a/ui/tx/internals/TxInternalsTable.tsx +++ b/ui/tx/internals/TxInternalsTable.tsx @@ -44,7 +44,7 @@ const TxInternalsTable = ({ data, sort, onSortToggle, top, isLoading }: Props) = { data.map((item, index) => ( - + )) } diff --git a/ui/tx/state/TxStateTableItem.tsx b/ui/tx/state/TxStateTableItem.tsx index 7c885a4f5e..a963242429 100644 --- a/ui/tx/state/TxStateTableItem.tsx +++ b/ui/tx/state/TxStateTableItem.tsx @@ -28,7 +28,7 @@ const TxStateTableItem = ({ data, isLoading }: Props) => { isLoading={ isLoading } truncation="constant" my="7px" - w="min-content" + w="100%" /> { before } diff --git a/ui/tx/useFetchTxInfo.tsx b/ui/tx/useFetchTxInfo.tsx new file mode 100644 index 0000000000..41c716bbb0 --- /dev/null +++ b/ui/tx/useFetchTxInfo.tsx @@ -0,0 +1,74 @@ +import type { UseQueryResult } from '@tanstack/react-query'; +import { useQueryClient } from '@tanstack/react-query'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { SocketMessage } from 'lib/socket/types'; +import type { Transaction } from 'types/api/transaction'; + +import type { ResourceError } from 'lib/api/resources'; +import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; +import delay from 'lib/delay'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import useSocketChannel from 'lib/socket/useSocketChannel'; +import useSocketMessage from 'lib/socket/useSocketMessage'; +import { TX } from 'stubs/tx'; + +interface Params { + onTxStatusUpdate?: () => void; + updateDelay?: number; +} + +type ReturnType = UseQueryResult> & { + socketStatus: 'close' | 'error' | undefined; +} + +export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay }: Params | undefined = {}): ReturnType { + const router = useRouter(); + const queryClient = useQueryClient(); + const [ socketStatus, setSocketStatus ] = React.useState<'close' | 'error'>(); + const hash = getQueryParamString(router.query.hash); + + const queryResult = useApiQuery<'tx', { status: number }>('tx', { + pathParams: { hash }, + queryOptions: { + enabled: Boolean(hash), + refetchOnMount: false, + placeholderData: TX, + }, + }); + const { data, isError, isLoading } = queryResult; + + const handleStatusUpdateMessage: SocketMessage.TxStatusUpdate['handler'] = React.useCallback(async() => { + updateDelay && await delay(updateDelay); + queryClient.invalidateQueries({ + queryKey: getResourceKey('tx', { pathParams: { hash } }), + }); + onTxStatusUpdate?.(); + }, [ onTxStatusUpdate, queryClient, hash, updateDelay ]); + + const handleSocketClose = React.useCallback(() => { + setSocketStatus('close'); + }, []); + + const handleSocketError = React.useCallback(() => { + setSocketStatus('error'); + }, []); + + const channel = useSocketChannel({ + topic: `transactions:${ hash }`, + onSocketClose: handleSocketClose, + onSocketError: handleSocketError, + isDisabled: isLoading || isError || data.status !== null, + }); + useSocketMessage({ + channel, + event: 'collated', + handler: handleStatusUpdateMessage, + }); + + return { + ...queryResult, + socketStatus, + }; +} diff --git a/ui/txnBatches/zkEvmL2/ZkEvmL2TxnBatchDetails.tsx b/ui/txnBatches/zkEvmL2/ZkEvmL2TxnBatchDetails.tsx index 1501d6a681..950a2ccec7 100644 --- a/ui/txnBatches/zkEvmL2/ZkEvmL2TxnBatchDetails.tsx +++ b/ui/txnBatches/zkEvmL2/ZkEvmL2TxnBatchDetails.tsx @@ -10,6 +10,7 @@ import { route } from 'nextjs-routes'; import type { ResourceError } from 'lib/api/resources'; import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; +import isCustomAppError from 'ui/shared/AppError/isCustomAppError'; import CopyToClipboard from 'ui/shared/CopyToClipboard'; import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; @@ -42,7 +43,7 @@ const ZkEvmL2TxnBatchDetails = ({ query }: Props) => { }, [ data, router ]); if (isError) { - if (error?.status === 404 || error?.status === 422) { + if (isCustomAppError(error)) { throwOnResourceLoadError({ isError, error }); } @@ -86,7 +87,7 @@ const ZkEvmL2TxnBatchDetails = ({ query }: Props) => { title="Timestamp" isLoading={ isPlaceholderData } > - + { data.timestamp ? : 'Undefined' } { hash={ data.verify_tx_hash } maxW="100%" /> - ) : pending } + ) : Pending } { - const timeAgo = dayjs(item.timestamp).fromNow(); + const timeAgo = item.timestamp ? dayjs(item.timestamp).fromNow() : 'Undefined'; if (!rollupFeature.isEnabled || rollupFeature.type !== 'zkEvm') { return null; @@ -61,7 +61,7 @@ const ZkEvmTxnBatchesListItem = ({ item, isLoading }: Props) => { - Verify Tx Has + Verify tx hash { item.verify_tx_hash ? ( { Status Age Txn count - Verify Tx Has + Verify tx hash Sequence hash diff --git a/ui/txnBatches/zkEvmL2/ZkEvmTxnBatchesTableItem.tsx b/ui/txnBatches/zkEvmL2/ZkEvmTxnBatchesTableItem.tsx index 9491a531c5..a22c3e3fcd 100644 --- a/ui/txnBatches/zkEvmL2/ZkEvmTxnBatchesTableItem.tsx +++ b/ui/txnBatches/zkEvmL2/ZkEvmTxnBatchesTableItem.tsx @@ -17,7 +17,7 @@ const rollupFeature = config.features.rollup; type Props = { item: ZkEvmL2TxnBatchesItem; isLoading?: boolean }; const TxnBatchesTableItem = ({ item, isLoading }: Props) => { - const timeAgo = dayjs(item.timestamp).fromNow(); + const timeAgo = item.timestamp ? dayjs(item.timestamp).fromNow() : 'Undefined'; if (!rollupFeature.isEnabled || rollupFeature.type !== 'zkEvm') { return null; diff --git a/ui/txnBatches/zkSyncL2/ZkSyncL2TxnBatchDetails.tsx b/ui/txnBatches/zkSyncL2/ZkSyncL2TxnBatchDetails.tsx new file mode 100644 index 0000000000..8505177d25 --- /dev/null +++ b/ui/txnBatches/zkSyncL2/ZkSyncL2TxnBatchDetails.tsx @@ -0,0 +1,180 @@ +import { Grid, GridItem, Link, Skeleton, Text } from '@chakra-ui/react'; +import type { UseQueryResult } from '@tanstack/react-query'; +import BigNumber from 'bignumber.js'; +import { useRouter } from 'next/router'; +import React from 'react'; +import { scroller, Element } from 'react-scroll'; + +import { ZKSYNC_L2_TX_BATCH_STATUSES, type ZkSyncBatch } from 'types/api/zkSyncL2'; + +import { route } from 'nextjs-routes'; + +import type { ResourceError } from 'lib/api/resources'; +import { WEI, WEI_IN_GWEI } from 'lib/consts'; +import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; +import { currencyUnits } from 'lib/units'; +import isCustomAppError from 'ui/shared/AppError/isCustomAppError'; +import CopyToClipboard from 'ui/shared/CopyToClipboard'; +import DataFetchAlert from 'ui/shared/DataFetchAlert'; +import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; +import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider'; +import DetailsTimestamp from 'ui/shared/DetailsTimestamp'; +import LinkInternal from 'ui/shared/LinkInternal'; +import PrevNext from 'ui/shared/PrevNext'; +import TruncatedValue from 'ui/shared/TruncatedValue'; +import VerificationSteps from 'ui/shared/verificationSteps/VerificationSteps'; + +import ZkSyncL2TxnBatchHashesInfo from './ZkSyncL2TxnBatchHashesInfo'; + +interface Props { + query: UseQueryResult; +} + +const ZkSyncL2TxnBatchDetails = ({ query }: Props) => { + const router = useRouter(); + const [ isExpanded, setIsExpanded ] = React.useState(false); + + const { data, isPlaceholderData, isError, error } = query; + + const handlePrevNextClick = React.useCallback((direction: 'prev' | 'next') => { + if (!data) { + return; + } + + const increment = direction === 'next' ? +1 : -1; + const nextId = String(data.number + increment); + + router.push({ pathname: '/batches/[number]', query: { number: nextId } }, undefined); + }, [ data, router ]); + + const handleCutClick = React.useCallback(() => { + setIsExpanded((flag) => !flag); + scroller.scrollTo('ZkSyncL2TxnBatchDetails__cutLink', { + duration: 500, + smooth: true, + }); + }, []); + + if (isError) { + if (isCustomAppError(error)) { + throwOnResourceLoadError({ isError, error }); + } + + return ; + } + + if (!data) { + return null; + } + + const txNum = data.l2_tx_count + data.l1_tx_count; + + return ( + + + + { data.number } + + + + + + + + + + { data.timestamp ? : 'Undefined' } + + + + + + { txNum } transaction{ txNum === 1 ? '' : 's' } + + + + + + + + + + + + + { isExpanded ? 'Hide details' : 'View details' } + + + + + + { isExpanded && ( + <> + + + + + + + + + { BigNumber(data.l1_gas_price).dividedBy(WEI).toFixed() } { currencyUnits.ether } + ({ BigNumber(data.l1_gas_price).dividedBy(WEI_IN_GWEI).toFixed() } { currencyUnits.gwei }) + + + + { BigNumber(data.l2_fair_gas_price).dividedBy(WEI).toFixed() } { currencyUnits.ether } + ({ BigNumber(data.l2_fair_gas_price).dividedBy(WEI_IN_GWEI).toFixed() } { currencyUnits.gwei }) + + + ) } + + ); +}; + +export default ZkSyncL2TxnBatchDetails; diff --git a/ui/txnBatches/zkSyncL2/ZkSyncL2TxnBatchHashesInfo.tsx b/ui/txnBatches/zkSyncL2/ZkSyncL2TxnBatchHashesInfo.tsx new file mode 100644 index 0000000000..bb0bfcb80d --- /dev/null +++ b/ui/txnBatches/zkSyncL2/ZkSyncL2TxnBatchHashesInfo.tsx @@ -0,0 +1,101 @@ +import { Flex, Skeleton } from '@chakra-ui/react'; +import React from 'react'; + +import type { ZkSyncBatch } from 'types/api/zkSyncL2'; + +import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; +import DetailsTimestamp from 'ui/shared/DetailsTimestamp'; +import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1'; + +interface Props { + isLoading: boolean; + data: Pick< + ZkSyncBatch, + 'commit_transaction_hash' | + 'commit_transaction_timestamp' | + 'prove_transaction_hash' | + 'prove_transaction_timestamp' | + 'execute_transaction_hash' | + 'execute_transaction_timestamp' + >; +} + +const ZkSyncL2TxnBatchHashesInfo = ({ isLoading, data }: Props) => { + return ( + <> + + { data.commit_transaction_hash ? ( + <> + + { data.commit_transaction_timestamp && ( + + + + ) } + + ) : Pending } + + + + { data.prove_transaction_hash ? ( + <> + + { data.prove_transaction_timestamp && ( + + + + ) } + + ) : Pending } + + + + { data.execute_transaction_hash ? ( + <> + + { data.execute_transaction_timestamp && ( + + + + ) } + + ) : Pending } + + + ); +}; + +export default React.memo(ZkSyncL2TxnBatchHashesInfo); diff --git a/ui/txnBatches/zkSyncL2/ZkSyncTxnBatchesListItem.tsx b/ui/txnBatches/zkSyncL2/ZkSyncTxnBatchesListItem.tsx new file mode 100644 index 0000000000..d930604763 --- /dev/null +++ b/ui/txnBatches/zkSyncL2/ZkSyncTxnBatchesListItem.tsx @@ -0,0 +1,94 @@ +import { Skeleton, Text } from '@chakra-ui/react'; +import React from 'react'; + +import type { ZkSyncBatchesItem } from 'types/api/zkSyncL2'; + +import { route } from 'nextjs-routes'; + +import config from 'configs/app'; +import dayjs from 'lib/date/dayjs'; +import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2'; +import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1'; +import LinkInternal from 'ui/shared/LinkInternal'; +import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; +import ZkSyncL2TxnBatchStatus from 'ui/shared/statusTag/ZkSyncL2TxnBatchStatus'; + +const rollupFeature = config.features.rollup; + +type Props = { item: ZkSyncBatchesItem; isLoading?: boolean }; + +const ZkSyncTxnBatchesListItem = ({ item, isLoading }: Props) => { + const timeAgo = item.timestamp ? dayjs(item.timestamp).fromNow() : 'Undefined'; + + if (!rollupFeature.isEnabled || rollupFeature.type !== 'zkSync') { + return null; + } + + return ( + + + Batch # + + + + + Status + + + + + Age + + { timeAgo } + + + Txn count + + + + { item.tx_count } + + + + + Commit tx + + { item.commit_transaction_hash ? ( + + ) : Pending } + + + Prove tx + + { item.prove_transaction_hash ? ( + + ) : Pending } + + + + ); +}; + +export default ZkSyncTxnBatchesListItem; diff --git a/ui/txnBatches/zkSyncL2/ZkSyncTxnBatchesTable.tsx b/ui/txnBatches/zkSyncL2/ZkSyncTxnBatchesTable.tsx new file mode 100644 index 0000000000..d96754466f --- /dev/null +++ b/ui/txnBatches/zkSyncL2/ZkSyncTxnBatchesTable.tsx @@ -0,0 +1,42 @@ +import { Table, Tbody, Th, Tr } from '@chakra-ui/react'; +import React from 'react'; + +import type { ZkSyncBatchesItem } from 'types/api/zkSyncL2'; + +import { default as Thead } from 'ui/shared/TheadSticky'; + +import ZkSyncTxnBatchesTableItem from './ZkSyncTxnBatchesTableItem'; + +type Props = { + items: Array; + top: number; + isLoading?: boolean; +} + +const ZkSyncTxnBatchesTable = ({ items, top, isLoading }: Props) => { + return ( + + + + + + + + + + + + + { items.map((item, index) => ( + + )) } + +
Batch #StatusAgeTxn countCommit txProve tx
+ ); +}; + +export default ZkSyncTxnBatchesTable; diff --git a/ui/txnBatches/zkSyncL2/ZkSyncTxnBatchesTableItem.tsx b/ui/txnBatches/zkSyncL2/ZkSyncTxnBatchesTableItem.tsx new file mode 100644 index 0000000000..82a2f93fdf --- /dev/null +++ b/ui/txnBatches/zkSyncL2/ZkSyncTxnBatchesTableItem.tsx @@ -0,0 +1,84 @@ +import { Td, Tr, Text, Skeleton } from '@chakra-ui/react'; +import React from 'react'; + +import type { ZkSyncBatchesItem } from 'types/api/zkSyncL2'; + +import { route } from 'nextjs-routes'; + +import config from 'configs/app'; +import dayjs from 'lib/date/dayjs'; +import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2'; +import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1'; +import LinkInternal from 'ui/shared/LinkInternal'; +import ZkSyncL2TxnBatchStatus from 'ui/shared/statusTag/ZkSyncL2TxnBatchStatus'; + +const rollupFeature = config.features.rollup; + +type Props = { item: ZkSyncBatchesItem; isLoading?: boolean }; + +const ZkSyncTxnBatchesTableItem = ({ item, isLoading }: Props) => { + const timeAgo = item.timestamp ? dayjs(item.timestamp).fromNow() : 'Undefined'; + + if (!rollupFeature.isEnabled || rollupFeature.type !== 'zkSync') { + return null; + } + + return ( + + + + + + + + + + { timeAgo } + + + + + + { item.tx_count } + + + + + { item.commit_transaction_hash ? ( + + ) : Pending } + + + { item.prove_transaction_hash ? ( + + ) : Pending } + + + ); +}; + +export default ZkSyncTxnBatchesTableItem; diff --git a/ui/txs/TxAdditionalInfo.pw.tsx b/ui/txs/TxAdditionalInfo.pw.tsx new file mode 100644 index 0000000000..3b12be42a5 --- /dev/null +++ b/ui/txs/TxAdditionalInfo.pw.tsx @@ -0,0 +1,43 @@ +import { test, expect } from '@playwright/experimental-ct-react'; +import React from 'react'; + +import * as txMock from 'mocks/txs/tx'; +import TestApp from 'playwright/TestApp'; + +import TxAdditionalInfo from './TxAdditionalInfo'; + +test('regular transaction +@dark-mode', async({ mount, page }) => { + const component = await mount( + + + , + ); + + await component.getByLabel('Transaction info').click(); + + await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 400, height: 600 } }); +}); + +test('regular transaction +@mobile -@default', async({ mount, page }) => { + const component = await mount( + + + , + ); + + await component.getByLabel('Transaction info').click(); + + await expect(page).toHaveScreenshot(); +}); + +test('blob transaction', async({ mount, page }) => { + const component = await mount( + + + , + ); + + await component.getByLabel('Transaction info').click(); + + await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 400, height: 600 } }); +}); diff --git a/ui/txs/TxAdditionalInfo.tsx b/ui/txs/TxAdditionalInfo.tsx index 356bdc8867..3acba5141f 100644 --- a/ui/txs/TxAdditionalInfo.tsx +++ b/ui/txs/TxAdditionalInfo.tsx @@ -58,7 +58,7 @@ const TxAdditionalInfo = ({ hash, tx, isMobile, isLoading, className }: Props) = - + { content } diff --git a/ui/txs/TxAdditionalInfoContent.tsx b/ui/txs/TxAdditionalInfoContent.tsx index 9e582f1b82..88588ea06a 100644 --- a/ui/txs/TxAdditionalInfoContent.tsx +++ b/ui/txs/TxAdditionalInfoContent.tsx @@ -10,6 +10,7 @@ import config from 'configs/app'; import getValueWithUnit from 'lib/getValueWithUnit'; import { currencyUnits } from 'lib/units'; import CurrencyValue from 'ui/shared/CurrencyValue'; +import BlobEntity from 'ui/shared/entities/blob/BlobEntity'; import LinkInternal from 'ui/shared/LinkInternal'; import TextSeparator from 'ui/shared/TextSeparator'; import TxFeeStability from 'ui/shared/tx/TxFeeStability'; @@ -26,12 +27,34 @@ const TxAdditionalInfoContent = ({ tx }: { tx: Transaction }) => { color: 'gray.500', fontWeight: 600, marginBottom: 3, - fontSize: 'sm', }; return ( <> Additional info + { tx.blob_versioned_hashes && tx.blob_versioned_hashes.length > 0 && ( + + + Blobs: { tx.blob_versioned_hashes.length } + { tx.blob_versioned_hashes.length > 3 && ( + + view all + + ) } + + + { tx.blob_versioned_hashes.slice(0, 3).map((hash, index) => ( + + { index + 1 } + + + )) } + + + ) } { !config.UI.views.tx.hiddenFields?.tx_fee && ( { (tx.stability_fee !== undefined || tx.fee.value !== null) && ( @@ -73,40 +96,42 @@ const TxAdditionalInfoContent = ({ tx }: { tx: Transaction }) => { { tx.base_fee_per_gas !== null && ( Base: - { getValueWithUnit(tx.base_fee_per_gas, 'gwei').toFormat() } + { getValueWithUnit(tx.base_fee_per_gas, 'gwei').toFormat() } ) } { tx.max_fee_per_gas !== null && ( Max: - { getValueWithUnit(tx.max_fee_per_gas, 'gwei').toFormat() } + { getValueWithUnit(tx.max_fee_per_gas, 'gwei').toFormat() } ) } { tx.max_priority_fee_per_gas !== null && ( Max priority: - { getValueWithUnit(tx.max_priority_fee_per_gas, 'gwei').toFormat() } + { getValueWithUnit(tx.max_priority_fee_per_gas, 'gwei').toFormat() } ) } ) } - - Others - - Txn type: - { tx.type } - { tx.type === 2 && (EIP-1559) } - - - Nonce: - { tx.nonce } - - - Position: - { tx.position } + { !(tx.blob_versioned_hashes && tx.blob_versioned_hashes.length > 0) && ( + + Others + + Txn type: + { tx.type } + { tx.type === 2 && (EIP-1559) } + + + Nonce: + { tx.nonce } + + + Position: + { tx.position } + - - More details + ) } + More details ); }; diff --git a/ui/txs/TxTranslationType.tsx b/ui/txs/TxTranslationType.tsx new file mode 100644 index 0000000000..4086efb80f --- /dev/null +++ b/ui/txs/TxTranslationType.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +import type { TransactionType } from 'types/api/transaction'; + +import Tag from 'ui/shared/chakra/Tag'; + +import { camelCaseToSentence } from './noves/utils'; +import TxType from './TxType'; + +export interface Props { + types: Array; + isLoading?: boolean; + translatationType: string | undefined; +} + +const TxTranslationType = ({ types, isLoading, translatationType }: Props) => { + + const filteredTypes = [ 'unclassified' ]; + + if (!translatationType || filteredTypes.includes(translatationType)) { + return ; + } + + return ( + + { camelCaseToSentence(translatationType) } + + ); + +}; + +export default TxTranslationType; diff --git a/ui/txs/TxType.tsx b/ui/txs/TxType.tsx index fe856096dc..fb772a9584 100644 --- a/ui/txs/TxType.tsx +++ b/ui/txs/TxType.tsx @@ -9,7 +9,16 @@ export interface Props { isLoading?: boolean; } -const TYPES_ORDER = [ 'rootstock_remasc', 'rootstock_bridge', 'token_creation', 'contract_creation', 'token_transfer', 'contract_call', 'coin_transfer' ]; +const TYPES_ORDER: Array = [ + 'blob_transaction', + 'rootstock_remasc', + 'rootstock_bridge', + 'token_creation', + 'contract_creation', + 'token_transfer', + 'contract_call', + 'coin_transfer', +]; const TxType = ({ types, isLoading }: Props) => { const typeToShow = types.sort((t1, t2) => TYPES_ORDER.indexOf(t1) - TYPES_ORDER.indexOf(t2))[0]; @@ -22,6 +31,10 @@ const TxType = ({ types, isLoading }: Props) => { label = 'Contract call'; colorScheme = 'blue'; break; + case 'blob_transaction': + label = 'Blob txn'; + colorScheme = 'yellow'; + break; case 'contract_creation': label = 'Contract creation'; colorScheme = 'blue'; diff --git a/ui/txs/TxsContent.tsx b/ui/txs/TxsContent.tsx index a7011e033c..44f687afc5 100644 --- a/ui/txs/TxsContent.tsx +++ b/ui/txs/TxsContent.tsx @@ -10,6 +10,7 @@ import DataListDisplay from 'ui/shared/DataListDisplay'; import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages'; import getNextSortValue from 'ui/shared/sort/getNextSortValue'; +import useDescribeTxs from './noves/useDescribeTxs'; import TxsHeaderMobile from './TxsHeaderMobile'; import TxsList from './TxsList'; import TxsTable from './TxsTable'; @@ -62,7 +63,9 @@ const TxsContent = ({ setSorting(value); }, [ sort, setSorting ]); - const content = items ? ( + const itemsWithTranslation = useDescribeTxs(items, currentAddress, query.isPlaceholderData); + + const content = itemsWithTranslation ? ( <> - + { tx.translation ? + : + + } @@ -51,6 +56,7 @@ const TxsListItem = ({ tx, isLoading, showBlockInfo, currentAddress, enableTimeI hash={ tx.hash } truncation="constant_long" fontWeight="700" + iconName={ tx.tx_types.includes('blob_transaction') ? 'blob' : undefined } /> { tx.timestamp && ( diff --git a/ui/txs/TxsStats.pw.tsx b/ui/txs/TxsStats.pw.tsx new file mode 100644 index 0000000000..704e084249 --- /dev/null +++ b/ui/txs/TxsStats.pw.tsx @@ -0,0 +1,31 @@ +import { test, expect } from '@playwright/experimental-ct-react'; +import React from 'react'; + +import * as statsMock from 'mocks/stats'; +import * as txsStatsMock from 'mocks/txs/stats'; +import TestApp from 'playwright/TestApp'; +import buildApiUrl from 'playwright/utils/buildApiUrl'; + +import TxsStats from './TxsStats'; + +const TXS_STATS_API_URL = buildApiUrl('txs_stats'); +const STATS_API_URL = buildApiUrl('stats'); + +test('base view +@mobile', async({ mount, page }) => { + await page.route(STATS_API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(statsMock.base), + })); + await page.route(TXS_STATS_API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(txsStatsMock.base), + })); + + const component = await mount( + + + , + ); + + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/txs/TxsStats.tsx b/ui/txs/TxsStats.tsx new file mode 100644 index 0000000000..256cdab6a2 --- /dev/null +++ b/ui/txs/TxsStats.tsx @@ -0,0 +1,81 @@ +import { Box } from '@chakra-ui/react'; +import React from 'react'; + +import config from 'configs/app'; +import useApiQuery from 'lib/api/useApiQuery'; +import getCurrencyValue from 'lib/getCurrencyValue'; +import { thinsp } from 'lib/html-entities'; +import { HOMEPAGE_STATS } from 'stubs/stats'; +import { TXS_STATS } from 'stubs/tx'; +import StatsWidget from 'ui/shared/stats/StatsWidget'; + +const TxsStats = () => { + const txsStatsQuery = useApiQuery('txs_stats', { + queryOptions: { + placeholderData: TXS_STATS, + }, + }); + + const statsQuery = useApiQuery('stats', { + queryOptions: { + placeholderData: HOMEPAGE_STATS, + }, + }); + + if (!txsStatsQuery.data) { + return null; + } + + const txFeeAvg = getCurrencyValue({ + value: txsStatsQuery.data.transaction_fees_avg_24h, + exchangeRate: statsQuery.data?.coin_price, + decimals: String(config.chain.currency.decimals), + accuracyUsd: 2, + }); + + return ( + + + + + + + ); +}; + +export default React.memo(TxsStats); diff --git a/ui/txs/TxsTableItem.tsx b/ui/txs/TxsTableItem.tsx index b1a4121cf5..216003bfeb 100644 --- a/ui/txs/TxsTableItem.tsx +++ b/ui/txs/TxsTableItem.tsx @@ -21,6 +21,7 @@ import TxFeeStability from 'ui/shared/tx/TxFeeStability'; import TxWatchListTags from 'ui/shared/tx/TxWatchListTags'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; +import TxTranslationType from './TxTranslationType'; import TxType from './TxType'; type Props = { @@ -62,7 +63,10 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement, - + { tx.translation ? + : + + } diff --git a/ui/txs/__screenshots__/TxAdditionalInfo.pw.tsx_dark-color-mode_regular-transaction-dark-mode-1.png b/ui/txs/__screenshots__/TxAdditionalInfo.pw.tsx_dark-color-mode_regular-transaction-dark-mode-1.png new file mode 100644 index 0000000000..da0dcb7bc2 Binary files /dev/null and b/ui/txs/__screenshots__/TxAdditionalInfo.pw.tsx_dark-color-mode_regular-transaction-dark-mode-1.png differ diff --git a/ui/txs/__screenshots__/TxAdditionalInfo.pw.tsx_default_blob-transaction-1.png b/ui/txs/__screenshots__/TxAdditionalInfo.pw.tsx_default_blob-transaction-1.png new file mode 100644 index 0000000000..878f198c9b Binary files /dev/null and b/ui/txs/__screenshots__/TxAdditionalInfo.pw.tsx_default_blob-transaction-1.png differ diff --git a/ui/txs/__screenshots__/TxAdditionalInfo.pw.tsx_default_regular-transaction-dark-mode-1.png b/ui/txs/__screenshots__/TxAdditionalInfo.pw.tsx_default_regular-transaction-dark-mode-1.png new file mode 100644 index 0000000000..e1a2be103b Binary files /dev/null and b/ui/txs/__screenshots__/TxAdditionalInfo.pw.tsx_default_regular-transaction-dark-mode-1.png differ diff --git a/ui/txs/__screenshots__/TxAdditionalInfo.pw.tsx_mobile_regular-transaction-mobile---default-1.png b/ui/txs/__screenshots__/TxAdditionalInfo.pw.tsx_mobile_regular-transaction-mobile---default-1.png new file mode 100644 index 0000000000..c632730f0f Binary files /dev/null and b/ui/txs/__screenshots__/TxAdditionalInfo.pw.tsx_mobile_regular-transaction-mobile---default-1.png differ diff --git a/ui/txs/__screenshots__/TxsListItem.pw.tsx_dark-color-mode_base-view-dark-mode-1.png b/ui/txs/__screenshots__/TxsListItem.pw.tsx_dark-color-mode_base-view-dark-mode-1.png index 3e12af58d1..152eb5ef85 100644 Binary files a/ui/txs/__screenshots__/TxsListItem.pw.tsx_dark-color-mode_base-view-dark-mode-1.png and b/ui/txs/__screenshots__/TxsListItem.pw.tsx_dark-color-mode_base-view-dark-mode-1.png differ diff --git a/ui/txs/__screenshots__/TxsListItem.pw.tsx_default_base-view-dark-mode-1.png b/ui/txs/__screenshots__/TxsListItem.pw.tsx_default_base-view-dark-mode-1.png index 0cb72cb8d0..b81038e105 100644 Binary files a/ui/txs/__screenshots__/TxsListItem.pw.tsx_default_base-view-dark-mode-1.png and b/ui/txs/__screenshots__/TxsListItem.pw.tsx_default_base-view-dark-mode-1.png differ diff --git a/ui/txs/__screenshots__/TxsListItem.pw.tsx_default_with-base-address-1.png b/ui/txs/__screenshots__/TxsListItem.pw.tsx_default_with-base-address-1.png index 175908210a..21e4bd1616 100644 Binary files a/ui/txs/__screenshots__/TxsListItem.pw.tsx_default_with-base-address-1.png and b/ui/txs/__screenshots__/TxsListItem.pw.tsx_default_with-base-address-1.png differ diff --git a/ui/txs/__screenshots__/TxsStats.pw.tsx_default_base-view-mobile-1.png b/ui/txs/__screenshots__/TxsStats.pw.tsx_default_base-view-mobile-1.png new file mode 100644 index 0000000000..0c97211555 Binary files /dev/null and b/ui/txs/__screenshots__/TxsStats.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/txs/__screenshots__/TxsStats.pw.tsx_mobile_base-view-mobile-1.png b/ui/txs/__screenshots__/TxsStats.pw.tsx_mobile_base-view-mobile-1.png new file mode 100644 index 0000000000..4e6819c7ac Binary files /dev/null and b/ui/txs/__screenshots__/TxsStats.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/txs/__screenshots__/TxsTable.pw.tsx_dark-color-mode_base-view-dark-mode-1.png b/ui/txs/__screenshots__/TxsTable.pw.tsx_dark-color-mode_base-view-dark-mode-1.png index f2715618aa..0418f67fdf 100644 Binary files a/ui/txs/__screenshots__/TxsTable.pw.tsx_dark-color-mode_base-view-dark-mode-1.png and b/ui/txs/__screenshots__/TxsTable.pw.tsx_dark-color-mode_base-view-dark-mode-1.png differ diff --git a/ui/txs/__screenshots__/TxsTable.pw.tsx_default_base-view-dark-mode-1.png b/ui/txs/__screenshots__/TxsTable.pw.tsx_default_base-view-dark-mode-1.png index 1f20725451..45373138cc 100644 Binary files a/ui/txs/__screenshots__/TxsTable.pw.tsx_default_base-view-dark-mode-1.png and b/ui/txs/__screenshots__/TxsTable.pw.tsx_default_base-view-dark-mode-1.png differ diff --git a/ui/txs/__screenshots__/TxsTable.pw.tsx_default_base-view-screen-xl-1.png b/ui/txs/__screenshots__/TxsTable.pw.tsx_default_base-view-screen-xl-1.png index 79573c1691..3df5a6aac3 100644 Binary files a/ui/txs/__screenshots__/TxsTable.pw.tsx_default_base-view-screen-xl-1.png and b/ui/txs/__screenshots__/TxsTable.pw.tsx_default_base-view-screen-xl-1.png differ diff --git a/ui/txs/noves/useDescribeTxs.tsx b/ui/txs/noves/useDescribeTxs.tsx new file mode 100644 index 0000000000..d9081492cd --- /dev/null +++ b/ui/txs/noves/useDescribeTxs.tsx @@ -0,0 +1,89 @@ +import { useQuery } from '@tanstack/react-query'; +import _ from 'lodash'; +import React from 'react'; + +import type { NovesDescribeTxsResponse } from 'types/api/noves'; +import type { Transaction } from 'types/api/transaction'; + +import config from 'configs/app'; +import useApiFetch from 'lib/api/useApiFetch'; + +const feature = config.features.txInterpretation; + +const translateEnabled = feature.isEnabled && feature.provider === 'noves'; + +export default function useDescribeTxs(items: Array | undefined, viewAsAccountAddress: string | undefined, isPlaceholderData: boolean) { + const apiFetch = useApiFetch(); + + const txsHash = _.uniq(items?.map(i => i.hash)); + const txChunks = _.chunk(txsHash, 10); + + const queryKey = { + viewAsAccountAddress, + firstHash: txsHash[0] || '', + lastHash: txsHash[txsHash.length - 1] || '', + }; + + const describeQuery = useQuery({ + queryKey: [ 'noves_describe_txs', queryKey ], + queryFn: async() => { + const queries = txChunks.map((hashes) => { + if (hashes.length === 0) { + return Promise.resolve([]); + } + + return apiFetch('noves_describe_txs', { + queryParams: { + viewAsAccountAddress, + hashes, + }, + }) as Promise; + }); + + return Promise.all(queries); + }, + select: (data) => { + return data.flat(); + }, + enabled: translateEnabled && !isPlaceholderData, + }); + + const itemsWithTranslation = React.useMemo(() => items?.map(tx => { + const queryData = describeQuery.data; + const isLoading = describeQuery.isLoading; + + if (isLoading) { + return { + ...tx, + translation: { + isLoading, + }, + }; + } + + if (!queryData || !translateEnabled) { + return tx; + } + + const query = queryData.find(data => data.txHash.toLowerCase() === tx.hash.toLowerCase()); + + if (query) { + return { + ...tx, + translation: { + data: query, + isLoading: false, + }, + }; + } + + return tx; + }), [ items, describeQuery ]); + + if (!translateEnabled || isPlaceholderData) { + return items; + } + + // return same "items" array of Transaction with a new "translation" field. + return itemsWithTranslation; +} diff --git a/ui/txs/noves/utils.ts b/ui/txs/noves/utils.ts new file mode 100644 index 0000000000..c5a986b66a --- /dev/null +++ b/ui/txs/noves/utils.ts @@ -0,0 +1,11 @@ +export function camelCaseToSentence(camelCaseString: string | undefined) { + if (!camelCaseString) { + return ''; + } + + let sentence = camelCaseString.replace(/([a-z])([A-Z])/g, '$1 $2'); + sentence = sentence.replace(/([A-Z])([A-Z][a-z])/g, '$1 $2'); + sentence = sentence.charAt(0).toUpperCase() + sentence.slice(1); + + return sentence; +} diff --git a/ui/txs/sortTxs.test.ts b/ui/txs/sortTxs.test.ts new file mode 100644 index 0000000000..d4d33e5b92 --- /dev/null +++ b/ui/txs/sortTxs.test.ts @@ -0,0 +1,79 @@ +import type { Transaction } from 'types/api/transaction'; + +import sortTxs, { sortTxsFromSocket } from './sortTxs'; + +describe('sortTxs', () => { + it('should sort transactions by value in descending order', () => { + const txs = [ + { value: '42' }, + { value: '11' }, + { value: '24' }, + ] as Array; + const result = txs.sort(sortTxs('value-desc')); + expect(result).toEqual([ + { value: '42' }, + { value: '24' }, + { value: '11' }, + ]); + }); + + it('should sort transactions by value in ascending order', () => { + const txs = [ + { value: '42' }, + { value: '11' }, + { value: '24' }, + ] as Array; + const result = txs.sort(sortTxs('value-asc')); + expect(result).toEqual([ + { value: '11' }, + { value: '24' }, + { value: '42' }, + ]); + }); + + it('should sort transactions by fee in descending order', () => { + const txs = [ + { fee: { value: '42' } }, + { fee: { value: '11' } }, + { fee: { value: '24' } }, + ] as Array; + const result = txs.sort(sortTxs('fee-desc')); + expect(result).toEqual([ + { fee: { value: '42' } }, + { fee: { value: '24' } }, + { fee: { value: '11' } }, + ]); + }); + + it('should sort transactions by fee in ascending order', () => { + const txs = [ + { fee: { value: '42' } }, + { fee: { value: '11' } }, + { fee: { value: '24' } }, + ] as Array; + const result = txs.sort(sortTxs('fee-asc')); + expect(result).toEqual([ + { fee: { value: '11' } }, + { fee: { value: '24' } }, + { fee: { value: '42' } }, + ]); + }); +}); + +describe('sortTxsFromSocket', () => { + it('should sort transaction by age in ascending order if sorting is not provided', () => { + const txs = [ + { timestamp: '2022-11-01T12:33:00Z' }, + { timestamp: '2022-11-01T12:00:00Z' }, + { timestamp: null }, + { timestamp: '2022-11-03T03:03:00Z' }, + ] as Array; + const result = txs.sort(sortTxsFromSocket(undefined)); + expect(result).toEqual([ + { timestamp: null }, + { timestamp: '2022-11-03T03:03:00Z' }, + { timestamp: '2022-11-01T12:33:00Z' }, + { timestamp: '2022-11-01T12:00:00Z' }, + ]); + }); +}); diff --git a/ui/txs/sortTxs.ts b/ui/txs/sortTxs.ts new file mode 100644 index 0000000000..3993612e4c --- /dev/null +++ b/ui/txs/sortTxs.ts @@ -0,0 +1,38 @@ +import type { Transaction, TransactionsSortingValue } from 'types/api/transaction'; + +import compareBns from 'lib/bigint/compareBns'; + +export default function sortTxs(sorting: TransactionsSortingValue | undefined) { + return function sortingFn(tx1: Transaction, tx2: Transaction) { + switch (sorting) { + case 'value-desc': + return compareBns(tx2.value, tx1.value); + case 'value-asc': + return compareBns(tx1.value, tx2.value); + case 'fee-desc': + return compareBns(tx2.fee.value || 0, tx1.fee.value || 0); + case 'fee-asc': + return compareBns(tx1.fee.value || 0, tx2.fee.value || 0); + default: + return 0; + } + }; +} + +export function sortTxsFromSocket(sorting: TransactionsSortingValue | undefined) { + if (sorting) { + return sortTxs(sorting); + } + + return function sortingFn(tx1: Transaction, tx2: Transaction) { + if (!tx1.timestamp) { + return -1; + } + + if (!tx2.timestamp) { + return 1; + } + + return tx2.timestamp.localeCompare(tx1.timestamp); + }; +} diff --git a/ui/txs/useTxsSort.tsx b/ui/txs/useTxsSort.tsx index bee6eefddd..c2ec9d8377 100644 --- a/ui/txs/useTxsSort.tsx +++ b/ui/txs/useTxsSort.tsx @@ -1,13 +1,14 @@ import type { UseQueryResult } from '@tanstack/react-query'; import React from 'react'; -import type { Transaction, TransactionsSortingValue, TxsResponse } from 'types/api/transaction'; +import type { TransactionsSortingValue, TxsResponse } from 'types/api/transaction'; import type { ResourceError } from 'lib/api/resources'; -import compareBns from 'lib/bigint/compareBns'; import * as cookies from 'lib/cookies'; import type { Option } from 'ui/shared/sort/Sort'; +import sortTxs from './sortTxs'; + export const SORT_OPTIONS: Array> = [ { title: 'Default', id: undefined }, { title: 'Value ascending', id: 'value-asc' }, @@ -23,21 +24,6 @@ type HookResult = UseQueryResult> & { setSortByValue: (value: SortingValue) => void; } -const sortTxs = (sorting: SortingValue) => (tx1: Transaction, tx2: Transaction) => { - switch (sorting) { - case 'value-desc': - return compareBns(tx1.value, tx2.value); - case 'value-asc': - return compareBns(tx2.value, tx1.value); - case 'fee-desc': - return compareBns(tx1.fee.value || 0, tx2.fee.value || 0); - case 'fee-asc': - return compareBns(tx2.fee.value || 0, tx1.fee.value || 0); - default: - return 0; - } -}; - export default function useTxsSort( queryResult: UseQueryResult>, ): HookResult { diff --git a/ui/userOp/UserOpDetails.tsx b/ui/userOp/UserOpDetails.tsx index fed9c6a3cd..9d06807485 100644 --- a/ui/userOp/UserOpDetails.tsx +++ b/ui/userOp/UserOpDetails.tsx @@ -12,6 +12,7 @@ import { WEI, WEI_IN_GWEI } from 'lib/consts'; import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; import { space } from 'lib/html-entities'; import { currencyUnits } from 'lib/units'; +import isCustomAppError from 'ui/shared/AppError/isCustomAppError'; import CurrencyValue from 'ui/shared/CurrencyValue'; import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; @@ -26,6 +27,8 @@ import UserOpSponsorType from 'ui/shared/userOps/UserOpSponsorType'; import UserOpStatus from 'ui/shared/userOps/UserOpStatus'; import Utilization from 'ui/shared/Utilization/Utilization'; +import UserOpDetailsActions from './UserOpDetailsActions'; + interface Props { query: UseQueryResult; } @@ -46,7 +49,7 @@ const UserOpDetails = ({ query }: Props) => { }, []); if (isError) { - if (error?.status === 400 || error?.status === 404 || error?.status === 422) { + if (error?.status === 400 || isCustomAppError(error)) { throwOnResourceLoadError({ isError, error }); } @@ -168,6 +171,8 @@ const UserOpDetails = ({ query }: Props) => {
+ { config.features.txInterpretation.isEnabled && } + { /* CUT */ } diff --git a/ui/userOp/UserOpDetailsActions.tsx b/ui/userOp/UserOpDetailsActions.tsx new file mode 100644 index 0000000000..8a08e8f1c7 --- /dev/null +++ b/ui/userOp/UserOpDetailsActions.tsx @@ -0,0 +1,47 @@ +import React from 'react'; + +import useApiQuery from 'lib/api/useApiQuery'; +import { TX_INTERPRETATION } from 'stubs/txInterpretation'; +import DetailsActionsWrapper from 'ui/shared/DetailsActionsWrapper'; +import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider'; +import TxInterpretation from 'ui/shared/tx/interpretation/TxInterpretation'; + +interface Props { + hash?: string; + isUserOpDataLoading: boolean; +} + +const TxDetailsActionsInterpretation = ({ hash, isUserOpDataLoading }: Props) => { + const interpretationQuery = useApiQuery('user_op_interpretation', { + pathParams: { hash }, + queryOptions: { + enabled: Boolean(hash) && !isUserOpDataLoading, + placeholderData: TX_INTERPRETATION, + refetchOnMount: false, + }, + }); + + const actions = interpretationQuery.data?.data.summaries; + + if (!actions || actions.length < 2) { + return null; + } + + return ( + <> + + { actions.map((action, index: number) => ( + + ), + ) } + + + + ); +}; + +export default TxDetailsActionsInterpretation; diff --git a/ui/userOp/UserOpRaw.tsx b/ui/userOp/UserOpRaw.tsx index 66febf055d..b47b6c05f7 100644 --- a/ui/userOp/UserOpRaw.tsx +++ b/ui/userOp/UserOpRaw.tsx @@ -7,7 +7,7 @@ import RawDataSnippet from 'ui/shared/RawDataSnippet'; // order is taken from the ERC-4337 standard // eslint-disable-next-line max-len -const KEYS_ORDER: Array = [ 'sender', 'nonce', 'init_code', 'call_data', 'call_gas_limit', 'verification_gas_limit', 'pre_verification_gas', 'max_fee_per_gas', 'max_priority_fee_per_gas', 'paymaster_and_data', 'signature' ]; +const KEYS_ORDER: Array = [ 'sender', 'nonce', 'init_code', 'call_data', 'account_gas_limits', 'call_gas_limit', 'verification_gas_limit', 'pre_verification_gas', 'gas_fees', 'max_fee_per_gas', 'max_priority_fee_per_gas', 'paymaster_and_data', 'signature' ]; interface Props { rawData?: UserOp['raw']; @@ -20,7 +20,10 @@ const UserOpRaw = ({ rawData, isLoading }: Props) => { } const text = JSON.stringify(KEYS_ORDER.reduce((res: UserOp['raw'], key: keyof UserOp['raw']) => { - res[key] = rawData[key]; + const value = rawData[key]; + if (value !== undefined) { + res[key] = value; + } return res; }, {} as UserOp['raw']), undefined, 4); diff --git a/ui/userOp/UserOpSubHeading.tsx b/ui/userOp/UserOpSubHeading.tsx new file mode 100644 index 0000000000..230f946bb3 --- /dev/null +++ b/ui/userOp/UserOpSubHeading.tsx @@ -0,0 +1,81 @@ +import { Flex, Link } from '@chakra-ui/react'; +// import type { UseQueryResult } from '@tanstack/react-query'; +import React from 'react'; + +// import type { UserOp } from 'types/api/userOps'; + +import config from 'configs/app'; +// import type { ResourceError } from 'lib/api/resources'; +import useApiQuery from 'lib/api/useApiQuery'; +import { TX_INTERPRETATION } from 'stubs/txInterpretation'; +import { TX_ACTIONS_BLOCK_ID } from 'ui/shared/DetailsActionsWrapper'; +import UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity'; +import TxInterpretation from 'ui/shared/tx/interpretation/TxInterpretation'; + +type Props = { + hash?: string; + // userOpQuery: UseQueryResult>; +} + +const UserOpSubHeading = ({ hash }: Props) => { + const hasInterpretationFeature = config.features.txInterpretation.isEnabled; + + const txInterpretationQuery = useApiQuery('user_op_interpretation', { + pathParams: { hash }, + queryOptions: { + enabled: Boolean(hash) && hasInterpretationFeature, + placeholderData: TX_INTERPRETATION, + }, + }); + + const hasInterpretation = hasInterpretationFeature && + (txInterpretationQuery.isPlaceholderData || Boolean(txInterpretationQuery.data?.data.summaries.length)); + + const hasViewAllInterpretationsLink = + !txInterpretationQuery.isPlaceholderData && txInterpretationQuery.data?.data.summaries && txInterpretationQuery.data?.data.summaries.length > 1; + + if (hasInterpretation) { + return ( + + + { hasViewAllInterpretationsLink && + View all } + + ); + // fallback will be added later + + // } else if (hasInterpretationFeature && userOpQuery.data?.decoded_call_data.method_call && userOpQuery.data?.sender && userOpQuery.data?.to) { + // return ( + // + // ); + } else { + return ; + } +}; + +export default UserOpSubHeading; diff --git a/ui/validators/ValidatorsFilter.tsx b/ui/validators/ValidatorsFilter.tsx index e2287f3867..08ca7dce94 100644 --- a/ui/validators/ValidatorsFilter.tsx +++ b/ui/validators/ValidatorsFilter.tsx @@ -36,7 +36,7 @@ const ValidatorsFilter = ({ onChange, defaultValue, isActive }: Props) => { All Active Probation - Failed + Inactive diff --git a/ui/verifiedContracts/VerifiedContractsCounters.tsx b/ui/verifiedContracts/VerifiedContractsCounters.tsx index 4d7dcd562f..71e10fbe79 100644 --- a/ui/verifiedContracts/VerifiedContractsCounters.tsx +++ b/ui/verifiedContracts/VerifiedContractsCounters.tsx @@ -24,6 +24,7 @@ const VerifiedContractsCounters = () => { diff={ countersQuery.data.new_smart_contracts_24h } diffFormatted={ Number(countersQuery.data.new_smart_contracts_24h).toLocaleString() } isLoading={ countersQuery.isPlaceholderData } + href={{ pathname: '/stats', query: { chartId: 'contractsGrowth' } }} /> { diff={ countersQuery.data.new_verified_smart_contracts_24h } diffFormatted={ Number(countersQuery.data.new_verified_smart_contracts_24h).toLocaleString() } isLoading={ countersQuery.isPlaceholderData } + href={{ pathname: '/stats', query: { chartId: 'verifiedContractsGrowth' } }} />
); diff --git a/ui/verifiedContracts/VerifiedContractsCountersItem.tsx b/ui/verifiedContracts/VerifiedContractsCountersItem.tsx new file mode 100644 index 0000000000..5f64f511da --- /dev/null +++ b/ui/verifiedContracts/VerifiedContractsCountersItem.tsx @@ -0,0 +1,33 @@ +import { Box, Flex, Text, useColorModeValue } from '@chakra-ui/react'; +import React from 'react'; + +type Props = { + name: string; + total: string; + new24: string; +} + +const VerifiedContractsCountersItem = ({ name, total, new24 }: Props) => { + const itemBgColor = useColorModeValue('blue.50', 'blue.800'); + return ( + + { name } + + { Number(total).toLocaleString() } + { Number(new24) > 0 && ( + <> + +{ Number(new24).toLocaleString() } + (24h) + + ) } + + + ); +}; + +export default VerifiedContractsCountersItem; diff --git a/ui/verifiedContracts/VerifiedContractsListItem.tsx b/ui/verifiedContracts/VerifiedContractsListItem.tsx index 8b12f86fd7..04765654a5 100644 --- a/ui/verifiedContracts/VerifiedContractsListItem.tsx +++ b/ui/verifiedContracts/VerifiedContractsListItem.tsx @@ -5,6 +5,7 @@ import React from 'react'; import type { VerifiedContract } from 'types/api/contracts'; import config from 'configs/app'; +import { CONTRACT_LICENSES } from 'lib/contracts/licenses'; import dayjs from 'lib/date/dayjs'; import { currencyUnits } from 'lib/units'; import CopyToClipboard from 'ui/shared/CopyToClipboard'; @@ -23,6 +24,15 @@ const VerifiedContractsListItem = ({ data, isLoading }: Props) => { BigNumber(data.coin_balance).div(10 ** config.chain.currency.decimals).dp(6).toFormat() : '0'; + const license = (() => { + const license = CONTRACT_LICENSES.find((license) => license.type === data.license_type); + if (!license || license.type === 'none') { + return '-'; + } + + return license.label; + })(); + return ( @@ -77,12 +87,12 @@ const VerifiedContractsListItem = ({ data, isLoading }: Props) => { - { /* - Market cap - - N/A - - */ } + + License + + { license } + + ); }; diff --git a/ui/verifiedContracts/VerifiedContractsTable.tsx b/ui/verifiedContracts/VerifiedContractsTable.tsx index d05da11b37..5032b78603 100644 --- a/ui/verifiedContracts/VerifiedContractsTable.tsx +++ b/ui/verifiedContracts/VerifiedContractsTable.tsx @@ -47,7 +47,7 @@ const VerifiedContractsTable = ({ data, sort, setSorting, isLoading }: Props) => Compiler / version Settings Verified - { /* Market cap */ } + License diff --git a/ui/verifiedContracts/VerifiedContractsTableItem.tsx b/ui/verifiedContracts/VerifiedContractsTableItem.tsx index 3a528dd6b9..3bafd4446e 100644 --- a/ui/verifiedContracts/VerifiedContractsTableItem.tsx +++ b/ui/verifiedContracts/VerifiedContractsTableItem.tsx @@ -5,6 +5,7 @@ import React from 'react'; import type { VerifiedContract } from 'types/api/contracts'; import config from 'configs/app'; +import { CONTRACT_LICENSES } from 'lib/contracts/licenses'; import dayjs from 'lib/date/dayjs'; import CopyToClipboard from 'ui/shared/CopyToClipboard'; import AddressEntity from 'ui/shared/entities/address/AddressEntity'; @@ -21,6 +22,15 @@ const VerifiedContractsTableItem = ({ data, isLoading }: Props) => { BigNumber(data.coin_balance).div(10 ** config.chain.currency.decimals).dp(6).toFormat() : '0'; + const license = (() => { + const license = CONTRACT_LICENSES.find((license) => license.type === data.license_type); + if (!license || license.type === 'none') { + return '-'; + } + + return license.label; + })(); + return ( @@ -51,8 +61,10 @@ const VerifiedContractsTableItem = ({ data, isLoading }: Props) => { { data.language } - - { data.compiler_version } + + + { data.compiler_version.split('+')[0] } + @@ -80,9 +92,11 @@ const VerifiedContractsTableItem = ({ data, isLoading }: Props) => { - { /* - N/A - */ } + + + { license } + + ); }; diff --git a/ui/watchlist/AddressModal/AddressForm.tsx b/ui/watchlist/AddressModal/AddressForm.tsx index 613d4029ae..f5db08aef4 100644 --- a/ui/watchlist/AddressModal/AddressForm.tsx +++ b/ui/watchlist/AddressModal/AddressForm.tsx @@ -22,7 +22,7 @@ import TagInput from 'ui/shared/TagInput'; import AddressFormNotifications from './AddressFormNotifications'; // does it depend on the network? -const NOTIFICATIONS = [ 'native', 'ERC-20', 'ERC-721' ] as const; +const NOTIFICATIONS = [ 'native', 'ERC-20', 'ERC-721', 'ERC-404' ] as const; const TAG_MAX_LENGTH = 35; @@ -50,6 +50,10 @@ type Inputs = { outcoming: boolean; incoming: boolean; }; + 'ERC-404': { + outcoming: boolean; + incoming: boolean; + }; }; } @@ -59,7 +63,9 @@ type Checkboxes = 'notification' | 'notification_settings.ERC-20.outcoming' | 'notification_settings.ERC-20.incoming' | 'notification_settings.ERC-721.outcoming' | -'notification_settings.ERC-721.incoming'; +'notification_settings.ERC-721.incoming' | +'notification_settings.ERC-404.outcoming' | +'notification_settings.ERC-404.incoming'; const AddressForm: React.FC = ({ data, onSuccess, setAlertVisible, isAdd }) => { const [ pending, setPending ] = useState(false); diff --git a/ui/watchlist/AddressModal/AddressFormNotifications.tsx b/ui/watchlist/AddressModal/AddressFormNotifications.tsx index 6b3b94d329..23ca3d9ff9 100644 --- a/ui/watchlist/AddressModal/AddressFormNotifications.tsx +++ b/ui/watchlist/AddressModal/AddressFormNotifications.tsx @@ -7,8 +7,8 @@ import config from 'configs/app'; import CheckboxInput from 'ui/shared/CheckboxInput'; // does it depend on the network? -const NOTIFICATIONS = [ 'native', 'ERC-20', 'ERC-721' ] as const; -const NOTIFICATIONS_NAMES = [ config.chain.currency.symbol, 'ERC-20', 'ERC-721, ERC-1155 (NFT)' ]; +const NOTIFICATIONS = [ 'native', 'ERC-20', 'ERC-721', 'ERC-404' ] as const; +const NOTIFICATIONS_NAMES = [ config.chain.currency.symbol, 'ERC-20', 'ERC-721, ERC-1155 (NFT)', 'ERC-404' ]; type Props = { control: Control; diff --git a/ui/withdrawals/WithdrawalsListItem.tsx b/ui/withdrawals/WithdrawalsListItem.tsx new file mode 100644 index 0000000000..cf22035ac5 --- /dev/null +++ b/ui/withdrawals/WithdrawalsListItem.tsx @@ -0,0 +1,89 @@ +import { Skeleton } from '@chakra-ui/react'; +import React from 'react'; + +import type { AddressWithdrawalsItem } from 'types/api/address'; +import type { BlockWithdrawalsItem } from 'types/api/block'; +import type { WithdrawalsItem } from 'types/api/withdrawals'; + +import config from 'configs/app'; +import dayjs from 'lib/date/dayjs'; +import CurrencyValue from 'ui/shared/CurrencyValue'; +import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import BlockEntity from 'ui/shared/entities/block/BlockEntity'; +import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; + +const feature = config.features.beaconChain; + +type Props = ({ + item: WithdrawalsItem; + view: 'list'; +} | { + item: AddressWithdrawalsItem; + view: 'address'; +} | { + item: BlockWithdrawalsItem; + view: 'block'; +}) & { isLoading?: boolean }; + +const WithdrawalsListItem = ({ item, isLoading, view }: Props) => { + if (!feature.isEnabled) { + return null; + } + + return ( + + + Index + + { item.index } + + + Validator index + + { item.validator_index } + + + { view !== 'block' && ( + <> + Block + + + + + ) } + + { view !== 'address' && ( + <> + To + + + + ) } + + { view !== 'block' && ( + <> + Age + + { dayjs(item.timestamp).fromNow() } + + + Value + + + + + ) } + + + ); +}; + +export default WithdrawalsListItem; diff --git a/ui/withdrawals/WithdrawalsTable.tsx b/ui/withdrawals/WithdrawalsTable.tsx new file mode 100644 index 0000000000..a6b91793e6 --- /dev/null +++ b/ui/withdrawals/WithdrawalsTable.tsx @@ -0,0 +1,61 @@ +import { Table, Tbody, Th, Tr } from '@chakra-ui/react'; +import React from 'react'; + +import type { AddressWithdrawalsItem } from 'types/api/address'; +import type { BlockWithdrawalsItem } from 'types/api/block'; +import type { WithdrawalsItem } from 'types/api/withdrawals'; + +import config from 'configs/app'; +import { default as Thead } from 'ui/shared/TheadSticky'; + +import WithdrawalsTableItem from './WithdrawalsTableItem'; + +const feature = config.features.beaconChain; + + type Props = { + top: number; + isLoading?: boolean; + } & ({ + items: Array; + view: 'list'; + } | { + items: Array; + view: 'address'; + } | { + items: Array; + view: 'block'; + }); + +const WithdrawalsTable = ({ items, isLoading, top, view = 'list' }: Props) => { + if (!feature.isEnabled) { + return null; + } + + return ( + + + + + + { view !== 'block' && } + { view !== 'address' && } + { view !== 'block' && } + + + + + { view === 'list' && (items as Array).map((item, index) => ( + + )) } + { view === 'address' && (items as Array).map((item, index) => ( + + )) } + { view === 'block' && (items as Array).map((item, index) => ( + + )) } + +
IndexValidator indexBlockToAge{ `Value ${ feature.currency.symbol }` }
+ ); +}; + +export default WithdrawalsTable; diff --git a/ui/withdrawals/WithdrawalsTableItem.tsx b/ui/withdrawals/WithdrawalsTableItem.tsx new file mode 100644 index 0000000000..b479d0a1d0 --- /dev/null +++ b/ui/withdrawals/WithdrawalsTableItem.tsx @@ -0,0 +1,66 @@ +import { Td, Tr, Skeleton } from '@chakra-ui/react'; +import React from 'react'; + +import type { AddressWithdrawalsItem } from 'types/api/address'; +import type { BlockWithdrawalsItem } from 'types/api/block'; +import type { WithdrawalsItem } from 'types/api/withdrawals'; + +import dayjs from 'lib/date/dayjs'; +import CurrencyValue from 'ui/shared/CurrencyValue'; +import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import BlockEntity from 'ui/shared/entities/block/BlockEntity'; + + type Props = ({ + item: WithdrawalsItem; + view: 'list'; + } | { + item: AddressWithdrawalsItem; + view: 'address'; + } | { + item: BlockWithdrawalsItem; + view: 'block'; + }) & { isLoading?: boolean }; + +const WithdrawalsTableItem = ({ item, view, isLoading }: Props) => { + return ( + + + { item.index } + + + { item.validator_index } + + { view !== 'block' && ( + + + + ) } + { view !== 'address' && ( + + + + ) } + { view !== 'block' && ( + + + { dayjs(item.timestamp).fromNow() } + + + ) } + + + + + ); +}; + +export default WithdrawalsTableItem; diff --git a/ui/withdrawals/optimisticL2/OptimisticL2WithdrawalsTable.tsx b/ui/withdrawals/optimisticL2/OptimisticL2WithdrawalsTable.tsx index b46eb35c69..0198858bb1 100644 --- a/ui/withdrawals/optimisticL2/OptimisticL2WithdrawalsTable.tsx +++ b/ui/withdrawals/optimisticL2/OptimisticL2WithdrawalsTable.tsx @@ -29,7 +29,11 @@ const OptimisticL2WithdrawalsTable = ({ items, top, isLoading }: Props) => { { items.map((item, index) => ( - + )) } diff --git a/ui/withdrawals/zkEvmL2/ZkEvmL2WithdrawalsListItem.tsx b/ui/withdrawals/zkEvmL2/ZkEvmL2WithdrawalsListItem.tsx new file mode 100644 index 0000000000..80a3fa14f1 --- /dev/null +++ b/ui/withdrawals/zkEvmL2/ZkEvmL2WithdrawalsListItem.tsx @@ -0,0 +1,97 @@ +import { Skeleton, chakra } from '@chakra-ui/react'; +import BigNumber from 'bignumber.js'; +import React from 'react'; + +import type { ZkEvmL2WithdrawalsItem } from 'types/api/zkEvmL2'; + +import config from 'configs/app'; +import dayjs from 'lib/date/dayjs'; +import BlockEntity from 'ui/shared/entities/block/BlockEntity'; +import TxEntity from 'ui/shared/entities/tx/TxEntity'; +import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1'; +import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; + +const rollupFeature = config.features.rollup; + +type Props = { item: ZkEvmL2WithdrawalsItem; isLoading?: boolean }; + +const ZkEvmL2WithdrawalsListItem = ({ item, isLoading }: Props) => { + if (!rollupFeature.isEnabled || rollupFeature.type !== 'zkEvm') { + return null; + } + + const timeAgo = dayjs(item.timestamp).fromNow(); + + return ( + + + Block + + + + + Index + + + { item.index } + + + + L2 txn hash + + + + + Age + + { timeAgo } + + + L1 txn hash + + { item.l1_transaction_hash ? ( + + ) : ( + + Pending Claim + + ) } + + + Value + + + { BigNumber(item.value).toFormat() } + + + + Token + + + { item.symbol } + + + + + ); +}; + +export default ZkEvmL2WithdrawalsListItem; diff --git a/ui/withdrawals/zkEvmL2/ZkEvmL2WithdrawalsTable.tsx b/ui/withdrawals/zkEvmL2/ZkEvmL2WithdrawalsTable.tsx new file mode 100644 index 0000000000..d50579e41e --- /dev/null +++ b/ui/withdrawals/zkEvmL2/ZkEvmL2WithdrawalsTable.tsx @@ -0,0 +1,39 @@ +import { Table, Tbody, Th, Tr } from '@chakra-ui/react'; +import React from 'react'; + +import type { ZkEvmL2WithdrawalsItem } from 'types/api/zkEvmL2'; + +import { default as Thead } from 'ui/shared/TheadSticky'; + +import ZkEvmL2WithdrawalsTableItem from './ZkEvmL2WithdrawalsTableItem'; + + type Props = { + items: Array; + top: number; + isLoading?: boolean; + } + +const ZkEvmL2DepositsTable = ({ items, top, isLoading }: Props) => { + return ( + + + + + + + + + + + + + + { items.map((item, index) => ( + + )) } + +
BlockIndexL2 txn hashAgeL1 txn hashValueToken
+ ); +}; + +export default ZkEvmL2DepositsTable; diff --git a/ui/withdrawals/zkEvmL2/ZkEvmL2WithdrawalsTableItem.tsx b/ui/withdrawals/zkEvmL2/ZkEvmL2WithdrawalsTableItem.tsx new file mode 100644 index 0000000000..97e4405432 --- /dev/null +++ b/ui/withdrawals/zkEvmL2/ZkEvmL2WithdrawalsTableItem.tsx @@ -0,0 +1,86 @@ +import { Td, Tr, Skeleton, chakra } from '@chakra-ui/react'; +import BigNumber from 'bignumber.js'; +import React from 'react'; + +import type { ZkEvmL2WithdrawalsItem } from 'types/api/zkEvmL2'; + +import config from 'configs/app'; +import dayjs from 'lib/date/dayjs'; +import BlockEntity from 'ui/shared/entities/block/BlockEntity'; +import TxEntity from 'ui/shared/entities/tx/TxEntity'; +import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1'; + +const rollupFeature = config.features.rollup; + + type Props = { item: ZkEvmL2WithdrawalsItem; isLoading?: boolean }; + +const ZkEvmL2WithdrawalsTableItem = ({ item, isLoading }: Props) => { + if (!rollupFeature.isEnabled || rollupFeature.type !== 'zkEvm') { + return null; + } + + const timeAgo = dayjs(item.timestamp).fromNow(); + + return ( + + + + + + + { item.index } + + + + + + + + { timeAgo } + + + + { item.l1_transaction_hash ? ( + + ) : ( + + Pending Claim + + ) } + + + + { BigNumber(item.value).toFormat() } + + + + + { item.symbol } + + + + ); +}; + +export default ZkEvmL2WithdrawalsTableItem; diff --git a/yarn.lock b/yarn.lock index 7e073743f1..a71da7a3a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1171,14 +1171,6 @@ "@babel/helper-validator-option" "^7.18.6" "@babel/plugin-transform-typescript" "^7.18.6" -"@babel/runtime-corejs3@^7.10.2": - version "7.19.4" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.19.4.tgz#870dbfd9685b3dad5aeb2d00841bb8b6192e3095" - integrity sha512-HzjQ8+dzdx7dmZy4DQ8KV8aHi/74AjEbBGTFutBmg/pd3dY5/q1sfuOGPTFGEytlQhWoeVXqcK5BwMgIkRkNDQ== - dependencies: - core-js-pure "^3.25.1" - regenerator-runtime "^0.13.4" - "@babel/runtime-corejs3@^7.20.7": version "7.21.0" resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.21.0.tgz#6e4939d9d9789ff63e2dc58e88f13a3913a24eba" @@ -1195,7 +1187,7 @@ core-js-pure "^3.30.2" regenerator-runtime "^0.14.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.9": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3": version "7.19.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.4.tgz#a42f814502ee467d55b38dd1c256f53a7b885c78" integrity sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA== @@ -1216,12 +1208,12 @@ dependencies: regenerator-runtime "^0.13.11" -"@babel/runtime@^7.17.2", "@babel/runtime@^7.5.5": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd" - integrity sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ== +"@babel/runtime@^7.20.6", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.0.tgz#584c450063ffda59697021430cb47101b085951e" + integrity sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw== dependencies: - regenerator-runtime "^0.13.11" + regenerator-runtime "^0.14.0" "@babel/runtime@^7.23.5": version "7.23.6" @@ -1230,6 +1222,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.5.5": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd" + integrity sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ== + dependencies: + regenerator-runtime "^0.13.11" + "@babel/runtime@^7.8.4": version "7.20.1" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.1.tgz#1148bb33ab252b165a06698fde7576092a78b4a9" @@ -2149,28 +2148,20 @@ picocolors "^1.0.0" sisteransi "^1.0.5" -"@coinbase/wallet-sdk@^3.6.6": - version "3.6.6" - resolved "https://registry.yarnpkg.com/@coinbase/wallet-sdk/-/wallet-sdk-3.6.6.tgz#4a0758fe0fe0ba3ed7e33b5bb6eb094ff8bd6c98" - integrity sha512-vX+epj/Ttjo7XRwlr3TFUUfW5GTRMvORpERPwiu7z2jl3DSVL4rXLmHt5y6LDPlUVreas2gumdcFbu0fLRG9Jg== +"@coinbase/wallet-sdk@3.9.1": + version "3.9.1" + resolved "https://registry.yarnpkg.com/@coinbase/wallet-sdk/-/wallet-sdk-3.9.1.tgz#503a14671bb392d653623ef2340934e213ac971f" + integrity sha512-cGUE8wm1/cMI8irRMVOqbFWYcnNugqCtuy2lnnHfgloBg+GRLs9RsrkOUDMdv/StfUeeKhCDyYudsXXvcL1xIA== dependencies: - "@metamask/safe-event-emitter" "2.0.0" - "@solana/web3.js" "^1.70.1" - bind-decorator "^1.0.11" - bn.js "^5.1.1" + bn.js "^5.2.1" buffer "^6.0.3" - clsx "^1.1.0" - eth-block-tracker "6.1.0" - eth-json-rpc-filters "5.1.0" - eth-rpc-errors "4.0.2" - json-rpc-engine "6.1.0" - keccak "^3.0.1" - preact "^10.5.9" - qs "^6.10.3" - rxjs "^6.6.3" + clsx "^1.2.1" + eth-block-tracker "^7.1.0" + eth-json-rpc-filters "^6.0.0" + eventemitter3 "^5.0.1" + keccak "^3.0.3" + preact "^10.16.0" sha.js "^2.4.11" - stream-browserify "^3.0.0" - util "^0.12.4" "@cspotcode/source-map-support@^0.8.0": version "0.8.1" @@ -2184,6 +2175,11 @@ resolved "https://registry.yarnpkg.com/@cush/relative/-/relative-1.0.0.tgz#8cd1769bf9bde3bb27dac356b1bc94af40f6cc16" integrity sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA== +"@discoveryjs/json-ext@0.5.7": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + "@emotion/babel-plugin@^11.10.5": version "11.10.5" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz#65fa6e1790ddc9e23cc22658a4c5dea423c55c3c" @@ -2202,6 +2198,23 @@ source-map "^0.5.7" stylis "4.1.3" +"@emotion/babel-plugin@^11.11.0": + version "11.11.0" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c" + integrity sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/runtime" "^7.18.3" + "@emotion/hash" "^0.9.1" + "@emotion/memoize" "^0.8.1" + "@emotion/serialize" "^1.1.2" + babel-plugin-macros "^3.1.0" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "4.2.0" + "@emotion/cache@^11.10.5", "@emotion/cache@^11.4.0": version "11.10.5" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.10.5.tgz#c142da9351f94e47527ed458f7bbbbe40bb13c12" @@ -2213,11 +2226,27 @@ "@emotion/weak-memoize" "^0.3.0" stylis "4.1.3" +"@emotion/cache@^11.11.0": + version "11.11.0" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.11.0.tgz#809b33ee6b1cb1a625fef7a45bc568ccd9b8f3ff" + integrity sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ== + dependencies: + "@emotion/memoize" "^0.8.1" + "@emotion/sheet" "^1.2.2" + "@emotion/utils" "^1.2.1" + "@emotion/weak-memoize" "^0.3.1" + stylis "4.2.0" + "@emotion/hash@^0.9.0": version "0.9.0" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.0.tgz#c5153d50401ee3c027a57a177bc269b16d889cb7" integrity sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ== +"@emotion/hash@^0.9.1": + version "0.9.1" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.1.tgz#4ffb0055f7ef676ebc3a5a91fb621393294e2f43" + integrity sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ== + "@emotion/is-prop-valid@^0.8.2": version "0.8.8" resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a" @@ -2232,6 +2261,13 @@ dependencies: "@emotion/memoize" "^0.8.0" +"@emotion/is-prop-valid@^1.2.1": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz#d4175076679c6a26faa92b03bb786f9e52612337" + integrity sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw== + dependencies: + "@emotion/memoize" "^0.8.1" + "@emotion/memoize@0.7.4": version "0.7.4" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" @@ -2242,6 +2278,11 @@ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.0.tgz#f580f9beb67176fa57aae70b08ed510e1b18980f" integrity sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA== +"@emotion/memoize@^0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17" + integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA== + "@emotion/react@^11.10.4", "@emotion/react@^11.8.1": version "11.10.5" resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.10.5.tgz#95fff612a5de1efa9c0d535384d3cfa115fe175d" @@ -2256,6 +2297,20 @@ "@emotion/weak-memoize" "^0.3.0" hoist-non-react-statics "^3.3.1" +"@emotion/react@^11.10.6": + version "11.11.4" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.11.4.tgz#3a829cac25c1f00e126408fab7f891f00ecc3c1d" + integrity sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.11.0" + "@emotion/cache" "^11.11.0" + "@emotion/serialize" "^1.1.3" + "@emotion/use-insertion-effect-with-fallbacks" "^1.0.1" + "@emotion/utils" "^1.2.1" + "@emotion/weak-memoize" "^0.3.1" + hoist-non-react-statics "^3.3.1" + "@emotion/serialize@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.1.tgz#0595701b1902feded8a96d293b26be3f5c1a5cf0" @@ -2267,11 +2322,27 @@ "@emotion/utils" "^1.2.0" csstype "^3.0.2" +"@emotion/serialize@^1.1.2", "@emotion/serialize@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.3.tgz#84b77bfcfe3b7bb47d326602f640ccfcacd5ffb0" + integrity sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA== + dependencies: + "@emotion/hash" "^0.9.1" + "@emotion/memoize" "^0.8.1" + "@emotion/unitless" "^0.8.1" + "@emotion/utils" "^1.2.1" + csstype "^3.0.2" + "@emotion/sheet@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.1.tgz#0767e0305230e894897cadb6c8df2c51e61a6c2c" integrity sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA== +"@emotion/sheet@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.2.tgz#d58e788ee27267a14342303e1abb3d508b6d0fec" + integrity sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA== + "@emotion/styled@^11.10.4": version "11.10.5" resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.10.5.tgz#1fe7bf941b0909802cb826457e362444e7e96a79" @@ -2284,26 +2355,58 @@ "@emotion/use-insertion-effect-with-fallbacks" "^1.0.0" "@emotion/utils" "^1.2.0" +"@emotion/styled@^11.10.6": + version "11.11.0" + resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.11.0.tgz#26b75e1b5a1b7a629d7c0a8b708fbf5a9cdce346" + integrity sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.11.0" + "@emotion/is-prop-valid" "^1.2.1" + "@emotion/serialize" "^1.1.2" + "@emotion/use-insertion-effect-with-fallbacks" "^1.0.1" + "@emotion/utils" "^1.2.1" + "@emotion/unitless@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.0.tgz#a4a36e9cbdc6903737cd20d38033241e1b8833db" integrity sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw== +"@emotion/unitless@^0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3" + integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ== + "@emotion/use-insertion-effect-with-fallbacks@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz#ffadaec35dbb7885bd54de3fa267ab2f860294df" integrity sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A== +"@emotion/use-insertion-effect-with-fallbacks@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz#08de79f54eb3406f9daaf77c76e35313da963963" + integrity sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw== + "@emotion/utils@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.0.tgz#9716eaccbc6b5ded2ea5a90d65562609aab0f561" integrity sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw== +"@emotion/utils@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.1.tgz#bbab58465738d31ae4cb3dbb6fc00a5991f755e4" + integrity sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg== + "@emotion/weak-memoize@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz#ea89004119dc42db2e1dba0f97d553f7372f6fcb" integrity sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg== +"@emotion/weak-memoize@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz#d0fce5d07b0620caa282b5131c297bb60f9d87e6" + integrity sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww== + "@esbuild/android-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622" @@ -2421,19 +2524,19 @@ dependencies: eslint-visitor-keys "^3.3.0" -"@eslint-community/regexpp@^4.4.0": - version "4.5.1" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.1.tgz#cdd35dce4fa1a89a4fd42b1599eb35b3af408884" - integrity sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ== +"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" + integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== -"@eslint/eslintrc@^1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.1.tgz#af58772019a2d271b7e2d4c23ff4ddcba3ccfb3e" - integrity sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA== +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.4.0" + espree "^9.6.0" globals "^13.19.0" ignore "^5.2.0" import-fresh "^3.2.1" @@ -2441,6 +2544,11 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@eslint/js@8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" + integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== + "@ethereumjs/common@^3.2.0": version "3.2.0" resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-3.2.0.tgz#b71df25845caf5456449163012074a55f048e0a0" @@ -2454,7 +2562,7 @@ resolved "https://registry.yarnpkg.com/@ethereumjs/rlp/-/rlp-4.0.1.tgz#626fabfd9081baab3d0a3074b0c7ecaf674aaa41" integrity sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw== -"@ethereumjs/tx@^4.1.2": +"@ethereumjs/tx@^4.1.2", "@ethereumjs/tx@^4.2.0": version "4.2.0" resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-4.2.0.tgz#5988ae15daf5a3b3c815493bc6b495e76009e853" integrity sha512-1nc6VO4jtFd172BbSnTnDQVr9IYBFl1y4xPzZdtkrkKIncBCkdbgfdRV+MiTkJYAtTxvV12GRZLqBFT1PNK6Yw== @@ -2473,348 +2581,6 @@ ethereum-cryptography "^2.0.0" micro-ftch "^0.3.1" -"@ethersproject/abi@5.7.0", "@ethersproject/abi@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.7.0.tgz#b3f3e045bbbeed1af3947335c247ad625a44e449" - integrity sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA== - dependencies: - "@ethersproject/address" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/constants" "^5.7.0" - "@ethersproject/hash" "^5.7.0" - "@ethersproject/keccak256" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/strings" "^5.7.0" - -"@ethersproject/abstract-provider@5.7.0", "@ethersproject/abstract-provider@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz#b0a8550f88b6bf9d51f90e4795d48294630cb9ef" - integrity sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw== - dependencies: - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/networks" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/transactions" "^5.7.0" - "@ethersproject/web" "^5.7.0" - -"@ethersproject/abstract-signer@5.7.0", "@ethersproject/abstract-signer@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz#13f4f32117868452191a4649723cb086d2b596b2" - integrity sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ== - dependencies: - "@ethersproject/abstract-provider" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - -"@ethersproject/address@5.7.0", "@ethersproject/address@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.7.0.tgz#19b56c4d74a3b0a46bfdbb6cfcc0a153fc697f37" - integrity sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA== - dependencies: - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/keccak256" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/rlp" "^5.7.0" - -"@ethersproject/base64@5.7.0", "@ethersproject/base64@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.7.0.tgz#ac4ee92aa36c1628173e221d0d01f53692059e1c" - integrity sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ== - dependencies: - "@ethersproject/bytes" "^5.7.0" - -"@ethersproject/basex@5.7.0", "@ethersproject/basex@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.7.0.tgz#97034dc7e8938a8ca943ab20f8a5e492ece4020b" - integrity sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw== - dependencies: - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - -"@ethersproject/bignumber@5.7.0", "@ethersproject/bignumber@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.7.0.tgz#e2f03837f268ba655ffba03a57853e18a18dc9c2" - integrity sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw== - dependencies: - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - bn.js "^5.2.1" - -"@ethersproject/bytes@5.7.0", "@ethersproject/bytes@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.7.0.tgz#a00f6ea8d7e7534d6d87f47188af1148d71f155d" - integrity sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A== - dependencies: - "@ethersproject/logger" "^5.7.0" - -"@ethersproject/constants@5.7.0", "@ethersproject/constants@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.7.0.tgz#df80a9705a7e08984161f09014ea012d1c75295e" - integrity sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA== - dependencies: - "@ethersproject/bignumber" "^5.7.0" - -"@ethersproject/contracts@5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.7.0.tgz#c305e775abd07e48aa590e1a877ed5c316f8bd1e" - integrity sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg== - dependencies: - "@ethersproject/abi" "^5.7.0" - "@ethersproject/abstract-provider" "^5.7.0" - "@ethersproject/abstract-signer" "^5.7.0" - "@ethersproject/address" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/constants" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/transactions" "^5.7.0" - -"@ethersproject/hash@5.7.0", "@ethersproject/hash@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.7.0.tgz#eb7aca84a588508369562e16e514b539ba5240a7" - integrity sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g== - dependencies: - "@ethersproject/abstract-signer" "^5.7.0" - "@ethersproject/address" "^5.7.0" - "@ethersproject/base64" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/keccak256" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/strings" "^5.7.0" - -"@ethersproject/hdnode@5.7.0", "@ethersproject/hdnode@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.7.0.tgz#e627ddc6b466bc77aebf1a6b9e47405ca5aef9cf" - integrity sha512-OmyYo9EENBPPf4ERhR7oj6uAtUAhYGqOnIS+jE5pTXvdKBS99ikzq1E7Iv0ZQZ5V36Lqx1qZLeak0Ra16qpeOg== - dependencies: - "@ethersproject/abstract-signer" "^5.7.0" - "@ethersproject/basex" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/pbkdf2" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/sha2" "^5.7.0" - "@ethersproject/signing-key" "^5.7.0" - "@ethersproject/strings" "^5.7.0" - "@ethersproject/transactions" "^5.7.0" - "@ethersproject/wordlists" "^5.7.0" - -"@ethersproject/json-wallets@5.7.0", "@ethersproject/json-wallets@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.7.0.tgz#5e3355287b548c32b368d91014919ebebddd5360" - integrity sha512-8oee5Xgu6+RKgJTkvEMl2wDgSPSAQ9MB/3JYjFV9jlKvcYHUXZC+cQp0njgmxdHkYWn8s6/IqIZYm0YWCjO/0g== - dependencies: - "@ethersproject/abstract-signer" "^5.7.0" - "@ethersproject/address" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/hdnode" "^5.7.0" - "@ethersproject/keccak256" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/pbkdf2" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/random" "^5.7.0" - "@ethersproject/strings" "^5.7.0" - "@ethersproject/transactions" "^5.7.0" - aes-js "3.0.0" - scrypt-js "3.0.1" - -"@ethersproject/keccak256@5.7.0", "@ethersproject/keccak256@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.7.0.tgz#3186350c6e1cd6aba7940384ec7d6d9db01f335a" - integrity sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg== - dependencies: - "@ethersproject/bytes" "^5.7.0" - js-sha3 "0.8.0" - -"@ethersproject/logger@5.7.0", "@ethersproject/logger@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.7.0.tgz#6ce9ae168e74fecf287be17062b590852c311892" - integrity sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig== - -"@ethersproject/networks@5.7.1", "@ethersproject/networks@^5.7.0": - version "5.7.1" - resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.7.1.tgz#118e1a981d757d45ccea6bb58d9fd3d9db14ead6" - integrity sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ== - dependencies: - "@ethersproject/logger" "^5.7.0" - -"@ethersproject/pbkdf2@5.7.0", "@ethersproject/pbkdf2@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.7.0.tgz#d2267d0a1f6e123f3771007338c47cccd83d3102" - integrity sha512-oR/dBRZR6GTyaofd86DehG72hY6NpAjhabkhxgr3X2FpJtJuodEl2auADWBZfhDHgVCbu3/H/Ocq2uC6dpNjjw== - dependencies: - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/sha2" "^5.7.0" - -"@ethersproject/properties@5.7.0", "@ethersproject/properties@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.7.0.tgz#a6e12cb0439b878aaf470f1902a176033067ed30" - integrity sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw== - dependencies: - "@ethersproject/logger" "^5.7.0" - -"@ethersproject/providers@5.7.2": - version "5.7.2" - resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.7.2.tgz#f8b1a4f275d7ce58cf0a2eec222269a08beb18cb" - integrity sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg== - dependencies: - "@ethersproject/abstract-provider" "^5.7.0" - "@ethersproject/abstract-signer" "^5.7.0" - "@ethersproject/address" "^5.7.0" - "@ethersproject/base64" "^5.7.0" - "@ethersproject/basex" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/constants" "^5.7.0" - "@ethersproject/hash" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/networks" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/random" "^5.7.0" - "@ethersproject/rlp" "^5.7.0" - "@ethersproject/sha2" "^5.7.0" - "@ethersproject/strings" "^5.7.0" - "@ethersproject/transactions" "^5.7.0" - "@ethersproject/web" "^5.7.0" - bech32 "1.1.4" - ws "7.4.6" - -"@ethersproject/random@5.7.0", "@ethersproject/random@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.7.0.tgz#af19dcbc2484aae078bb03656ec05df66253280c" - integrity sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ== - dependencies: - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - -"@ethersproject/rlp@5.7.0", "@ethersproject/rlp@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.7.0.tgz#de39e4d5918b9d74d46de93af80b7685a9c21304" - integrity sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w== - dependencies: - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - -"@ethersproject/sha2@5.7.0", "@ethersproject/sha2@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.7.0.tgz#9a5f7a7824ef784f7f7680984e593a800480c9fb" - integrity sha512-gKlH42riwb3KYp0reLsFTokByAKoJdgFCwI+CCiX/k+Jm2mbNs6oOaCjYQSlI1+XBVejwH2KrmCbMAT/GnRDQw== - dependencies: - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - hash.js "1.1.7" - -"@ethersproject/signing-key@5.7.0", "@ethersproject/signing-key@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.7.0.tgz#06b2df39411b00bc57c7c09b01d1e41cf1b16ab3" - integrity sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q== - dependencies: - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - bn.js "^5.2.1" - elliptic "6.5.4" - hash.js "1.1.7" - -"@ethersproject/solidity@5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.7.0.tgz#5e9c911d8a2acce2a5ebb48a5e2e0af20b631cb8" - integrity sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA== - dependencies: - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/keccak256" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/sha2" "^5.7.0" - "@ethersproject/strings" "^5.7.0" - -"@ethersproject/strings@5.7.0", "@ethersproject/strings@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.7.0.tgz#54c9d2a7c57ae8f1205c88a9d3a56471e14d5ed2" - integrity sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg== - dependencies: - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/constants" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - -"@ethersproject/transactions@5.7.0", "@ethersproject/transactions@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.7.0.tgz#91318fc24063e057885a6af13fdb703e1f993d3b" - integrity sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ== - dependencies: - "@ethersproject/address" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/constants" "^5.7.0" - "@ethersproject/keccak256" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/rlp" "^5.7.0" - "@ethersproject/signing-key" "^5.7.0" - -"@ethersproject/units@5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.7.0.tgz#637b563d7e14f42deeee39245275d477aae1d8b1" - integrity sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg== - dependencies: - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/constants" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - -"@ethersproject/wallet@5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.7.0.tgz#4e5d0790d96fe21d61d38fb40324e6c7ef350b2d" - integrity sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA== - dependencies: - "@ethersproject/abstract-provider" "^5.7.0" - "@ethersproject/abstract-signer" "^5.7.0" - "@ethersproject/address" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/hash" "^5.7.0" - "@ethersproject/hdnode" "^5.7.0" - "@ethersproject/json-wallets" "^5.7.0" - "@ethersproject/keccak256" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/random" "^5.7.0" - "@ethersproject/signing-key" "^5.7.0" - "@ethersproject/transactions" "^5.7.0" - "@ethersproject/wordlists" "^5.7.0" - -"@ethersproject/web@5.7.1", "@ethersproject/web@^5.7.0": - version "5.7.1" - resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.7.1.tgz#de1f285b373149bee5928f4eb7bcb87ee5fbb4ae" - integrity sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w== - dependencies: - "@ethersproject/base64" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/strings" "^5.7.0" - -"@ethersproject/wordlists@5.7.0", "@ethersproject/wordlists@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.7.0.tgz#8fb2c07185d68c3e09eb3bfd6e779ba2774627f5" - integrity sha512-S2TFNJNfHWVHNE6cNDjbVlZ6MgE17MIxMbMg2zv3wn+3XSJGosL1m9ZVv3GXCf/2ymSsQ+hRI5IzoMJTG6aoVA== - dependencies: - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/hash" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/strings" "^5.7.0" - "@fastify/busboy@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.0.0.tgz#f22824caff3ae506b18207bad4126dbc6ccdb6b8" @@ -2962,13 +2728,13 @@ "@hapi/hoek" "^9.0.0" "@hapi/topo" "^5.0.0" -"@humanwhocodes/config-array@^0.11.8": - version "0.11.8" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" - integrity sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g== +"@humanwhocodes/config-array@^0.11.14": + version "0.11.14" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" + integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== dependencies: - "@humanwhocodes/object-schema" "^1.2.1" - debug "^4.1.1" + "@humanwhocodes/object-schema" "^2.0.2" + debug "^4.3.1" minimatch "^3.0.5" "@humanwhocodes/module-importer@^1.0.1": @@ -2976,10 +2742,10 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" - integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@humanwhocodes/object-schema@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== "@hypelab/sdk-react@^1.0.0": version "1.0.0" @@ -3301,6 +3067,24 @@ dependencies: "@lit-labs/ssr-dom-shim" "^1.1.2" +"@metamask/eth-json-rpc-provider@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@metamask/eth-json-rpc-provider/-/eth-json-rpc-provider-1.0.1.tgz#3fd5316c767847f4ca107518b611b15396a5a32c" + integrity sha512-whiUMPlAOrVGmX8aKYVPvlKyG4CpQXiNNyt74vE1xb5sPvmx5oA7B/kOi/JdBvhGQq97U1/AVdXEdk2zkP8qyA== + dependencies: + "@metamask/json-rpc-engine" "^7.0.0" + "@metamask/safe-event-emitter" "^3.0.0" + "@metamask/utils" "^5.0.1" + +"@metamask/json-rpc-engine@^7.0.0": + version "7.3.3" + resolved "https://registry.yarnpkg.com/@metamask/json-rpc-engine/-/json-rpc-engine-7.3.3.tgz#f2b30a2164558014bfcca45db10f5af291d989af" + integrity sha512-dwZPq8wx9yV3IX2caLi9q9xZBw2XeIoYqdyihDDDpuHVCEiqadJLwqM3zy+uwf6F1QYQ65A8aOMQg1Uw7LMLNg== + dependencies: + "@metamask/rpc-errors" "^6.2.1" + "@metamask/safe-event-emitter" "^3.0.0" + "@metamask/utils" "^8.3.0" + "@metamask/object-multiplex@^1.1.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@metamask/object-multiplex/-/object-multiplex-1.2.0.tgz#38fc15c142f61939391e1b9a8eed679696c7e4f4" @@ -3310,6 +3094,21 @@ once "^1.4.0" readable-stream "^2.3.3" +"@metamask/onboarding@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@metamask/onboarding/-/onboarding-1.0.1.tgz#14a36e1e175e2f69f09598e2008ab6dc1b3297e6" + integrity sha512-FqHhAsCI+Vacx2qa5mAFcWNSrTcVGMNjzxVgaX8ECSny/BJ9/vgXP9V7WF/8vb9DltPeQkxr+Fnfmm6GHfmdTQ== + dependencies: + bowser "^2.9.0" + +"@metamask/post-message-stream@^6.1.0": + version "6.2.0" + resolved "https://registry.yarnpkg.com/@metamask/post-message-stream/-/post-message-stream-6.2.0.tgz#3db0a50adc2b2206d1bb95739e7fff49e36e0324" + integrity sha512-WunZ0bruClF862mvbKQGETn5SM0XKGmocPMQR1Ew6sYix9/FDzeoZnoI8RkXk01E+70FCdxhTE/r8kk5SFOuTw== + dependencies: + "@metamask/utils" "^5.0.0" + readable-stream "2.3.3" + "@metamask/post-message-stream@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@metamask/post-message-stream/-/post-message-stream-7.0.0.tgz#b7698909ec54923b6a6990b8d46cc15c2810d840" @@ -3336,22 +3135,83 @@ pump "^3.0.0" webextension-polyfill-ts "^0.25.0" -"@metamask/safe-event-emitter@2.0.0", "@metamask/safe-event-emitter@^2.0.0": +"@metamask/rpc-errors@^6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@metamask/rpc-errors/-/rpc-errors-6.2.1.tgz#f5daf429ededa7cb83069dc621bd5738fe2a1d80" + integrity sha512-VTgWkjWLzb0nupkFl1duQi9Mk8TGT9rsdnQg6DeRrYEFxtFOh0IF8nAwxM/4GWqDl6uIB06lqUBgUrAVWl62Bw== + dependencies: + "@metamask/utils" "^8.3.0" + fast-safe-stringify "^2.0.6" + +"@metamask/safe-event-emitter@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz#af577b477c683fad17c619a78208cede06f9605c" integrity sha512-/kSXhY692qiV1MXu6EeOZvg5nECLclxNXcKCxJ3cXQgYuRymRHpdx/t7JXfsK+JLjwA1e1c1/SBrlQYpusC29Q== -"@metamask/utils@^3.0.1": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@metamask/utils/-/utils-3.6.0.tgz#b218b969a05ca7a8093b5d1670f6625061de707d" - integrity sha512-9cIRrfkWvHblSiNDVXsjivqa9Ak0RYo/1H6tqTqTbAx+oBK2Sva0lWDHxGchOqA7bySGUJKAWSNJvH6gdHZ0gQ== +"@metamask/safe-event-emitter@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-3.0.0.tgz#8c2b9073fe0722d48693143b0dc8448840daa3bd" + integrity sha512-j6Z47VOmVyGMlnKXZmL0fyvWfEYtKWCA9yGZkU3FCsGZUT5lHGmvaV9JA5F2Y+010y7+ROtR3WMXIkvl/nVzqQ== + +"@metamask/sdk-communication-layer@0.14.3": + version "0.14.3" + resolved "https://registry.yarnpkg.com/@metamask/sdk-communication-layer/-/sdk-communication-layer-0.14.3.tgz#0e7ec8e472641273da5802f3b357687ce12369c3" + integrity sha512-yjSbj8y7fFbQXv2HBzUX6D9C8BimkCYP6BDV7hdw53W8b/GlYCtXVxUFajQ9tuO1xPTRjR/xt/dkdr2aCi6WGw== dependencies: - "@types/debug" "^4.1.7" - debug "^4.3.4" - semver "^7.3.8" - superstruct "^1.0.3" + bufferutil "^4.0.8" + cross-fetch "^3.1.5" + date-fns "^2.29.3" + eciesjs "^0.3.16" + eventemitter2 "^6.4.5" + socket.io-client "^4.5.1" + utf-8-validate "^6.0.3" + uuid "^8.3.2" + +"@metamask/sdk-install-modal-web@0.14.1": + version "0.14.1" + resolved "https://registry.yarnpkg.com/@metamask/sdk-install-modal-web/-/sdk-install-modal-web-0.14.1.tgz#c8e64b4f7d2dac262c2ec28025c541b258478c31" + integrity sha512-emT8HKbnfVwGhPxyUfMja6DWzvtJvDEBQxqCVx93H0HsyrrOzOC43iGCAosslw6o5h7gOfRKLqWmK8V7jQAS2Q== + dependencies: + "@emotion/react" "^11.10.6" + "@emotion/styled" "^11.10.6" + i18next "22.5.1" + qr-code-styling "^1.6.0-rc.1" + react "^18.2.0" + react-dom "^18.2.0" + react-i18next "^13.2.2" + +"@metamask/sdk@0.14.3": + version "0.14.3" + resolved "https://registry.yarnpkg.com/@metamask/sdk/-/sdk-0.14.3.tgz#ec1ecf00edef981fd17e2c5cf4ec40ce0a43a55c" + integrity sha512-BYLs//nY2wioVSih78gOQI6sLIYY3vWkwVqXGYUgkBV+bi49bv+9S0m+hZ2cwiRaxfMYtKs0KvhAQ8weiYwDrg== + dependencies: + "@metamask/onboarding" "^1.0.1" + "@metamask/post-message-stream" "^6.1.0" + "@metamask/providers" "^10.2.1" + "@metamask/sdk-communication-layer" "0.14.3" + "@metamask/sdk-install-modal-web" "0.14.1" + "@react-native-async-storage/async-storage" "^1.17.11" + "@types/dom-screen-wake-lock" "^1.0.0" + bowser "^2.9.0" + cross-fetch "^4.0.0" + eciesjs "^0.3.15" + eth-rpc-errors "^4.0.3" + eventemitter2 "^6.4.7" + extension-port-stream "^2.0.1" + i18next "22.5.1" + i18next-browser-languagedetector "^7.1.0" + obj-multiplex "^1.0.0" + pump "^3.0.0" + qrcode-terminal-nooctal "^0.12.1" + react-i18next "^13.2.2" + react-native-webview "^11.26.0" + readable-stream "^2.3.7" + rollup-plugin-visualizer "^5.9.2" + socket.io-client "^4.5.1" + util "^0.12.4" + uuid "^8.3.2" -"@metamask/utils@^5.0.0": +"@metamask/utils@^5.0.0", "@metamask/utils@^5.0.1": version "5.0.2" resolved "https://registry.yarnpkg.com/@metamask/utils/-/utils-5.0.2.tgz#140ba5061d90d9dac0280c19cab101bc18c8857c" integrity sha512-yfmE79bRQtnMzarnKfX7AEJBwFTxvTyw3nBQlu/5rmGXrjAeAMltoGxO62TFurxrQAFMNa/fEjIHNvungZp0+g== @@ -3362,6 +3222,20 @@ semver "^7.3.8" superstruct "^1.0.3" +"@metamask/utils@^8.3.0": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@metamask/utils/-/utils-8.3.0.tgz#a20de447aeb9ffb75924d822a186a597033984b6" + integrity sha512-WFVcMPEkKKRCJ8DDkZUTVbLlpwgRn98F4VM/WzN89HM8PmHMnCyk/oG0AmK/seOxtik7uC7Bbi2YBC5Z5XB2zw== + dependencies: + "@ethereumjs/tx" "^4.2.0" + "@noble/hashes" "^1.3.1" + "@scure/base" "^1.1.3" + "@types/debug" "^4.1.7" + debug "^4.3.4" + pony-cause "^2.1.10" + semver "^7.5.4" + superstruct "^1.0.3" + "@monaco-editor/loader@^1.3.2": version "1.3.2" resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.3.2.tgz#04effbb87052d19cd7d3c9d81c0635490f9bb6d8" @@ -3504,17 +3378,17 @@ resolved "https://registry.yarnpkg.com/@n1ru4l/push-pull-async-iterable-iterator/-/push-pull-async-iterable-iterator-3.2.0.tgz#c15791112db68dd9315d329d652b7e797f737655" integrity sha512-3fkKj25kEjsfObL6IlKPAlHYPq/oYwUkkQ03zsTTiDjD7vg/RxjdiLeCydqtxHZP0JgsXL3D/X5oAkMGzuUp/Q== -"@next/bundle-analyzer@^14.0.1": - version "14.0.1" - resolved "https://registry.yarnpkg.com/@next/bundle-analyzer/-/bundle-analyzer-14.0.1.tgz#0b371a00a4c252596669319e2fdda2d3cff4b52b" - integrity sha512-AbZZnj4gZ1ZQFppZxAC9e8+skj0rFiSvY6E6Ut+ydS1r6oizR7PMu/7o02psIm4ekAsmp2O1Eq8IowHQgPWPCQ== +"@next/bundle-analyzer@14.2.3": + version "14.2.3" + resolved "https://registry.yarnpkg.com/@next/bundle-analyzer/-/bundle-analyzer-14.2.3.tgz#dfa43586983d3fffdeb5f3c50f2c65ab1c51f184" + integrity sha512-Z88hbbngMs7njZKI8kTJIlpdLKYfMSLwnsqYe54AP4aLmgL70/Ynx/J201DQ+q2Lr6FxFw1uCeLGImDrHOl2ZA== dependencies: - webpack-bundle-analyzer "4.7.0" + webpack-bundle-analyzer "4.10.1" -"@next/env@13.5.4": - version "13.5.4" - resolved "https://registry.yarnpkg.com/@next/env/-/env-13.5.4.tgz#777c3af16de2cf2f611b6c8126910062d13d222c" - integrity sha512-LGegJkMvRNw90WWphGJ3RMHMVplYcOfRWf2Be3td3sUa+1AaxmsYyANsA+znrGCBjXJNi4XAQlSoEfUxs/4kIQ== +"@next/env@14.2.3": + version "14.2.3" + resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.3.tgz#d6def29d1c763c0afb397343a15a82e7d92353a0" + integrity sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA== "@next/eslint-plugin-next@13.3.0": version "13.3.0" @@ -3523,50 +3397,50 @@ dependencies: glob "7.1.7" -"@next/swc-darwin-arm64@13.5.4": - version "13.5.4" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.4.tgz#241957774fef3f876dc714cfc0ca6f00f561737e" - integrity sha512-Df8SHuXgF1p+aonBMcDPEsaahNo2TCwuie7VXED4FVyECvdXfRT9unapm54NssV9tF3OQFKBFOdlje4T43VO0w== - -"@next/swc-darwin-x64@13.5.4": - version "13.5.4" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.4.tgz#fa11bb97bf06cd45cbd554354b46bf93e22c025b" - integrity sha512-siPuUwO45PnNRMeZnSa8n/Lye5ZX93IJom9wQRB5DEOdFrw0JjOMu1GINB8jAEdwa7Vdyn1oJ2xGNaQpdQQ9Pw== - -"@next/swc-linux-arm64-gnu@13.5.4": - version "13.5.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.4.tgz#dd3a482cd6871ed23b049066a0f3c4c2f955dc88" - integrity sha512-l/k/fvRP/zmB2jkFMfefmFkyZbDkYW0mRM/LB+tH5u9pB98WsHXC0WvDHlGCYp3CH/jlkJPL7gN8nkTQVrQ/2w== - -"@next/swc-linux-arm64-musl@13.5.4": - version "13.5.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.4.tgz#ed6d7abaf5712cff2752ce5300d6bacc6aff1b18" - integrity sha512-YYGb7SlLkI+XqfQa8VPErljb7k9nUnhhRrVaOdfJNCaQnHBcvbT7cx/UjDQLdleJcfyg1Hkn5YSSIeVfjgmkTg== - -"@next/swc-linux-x64-gnu@13.5.4": - version "13.5.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.4.tgz#977a040388e8a685a3a85e0dbdff90a4ee2a7189" - integrity sha512-uE61vyUSClnCH18YHjA8tE1prr/PBFlBFhxBZis4XBRJoR+txAky5d7gGNUIbQ8sZZ7LVkSVgm/5Fc7mwXmRAg== - -"@next/swc-linux-x64-musl@13.5.4": - version "13.5.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.4.tgz#3e29a0ad8efc016196c3a120da04397eea328b2a" - integrity sha512-qVEKFYML/GvJSy9CfYqAdUexA6M5AklYcQCW+8JECmkQHGoPxCf04iMh7CPR7wkHyWWK+XLt4Ja7hhsPJtSnhg== - -"@next/swc-win32-arm64-msvc@13.5.4": - version "13.5.4" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.4.tgz#18a236c3fe5a48d24b56d939e6a05488bb682b7e" - integrity sha512-mDSQfqxAlfpeZOLPxLymZkX0hYF3juN57W6vFHTvwKlnHfmh12Pt7hPIRLYIShk8uYRsKPtMTth/EzpwRI+u8w== - -"@next/swc-win32-ia32-msvc@13.5.4": - version "13.5.4" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.4.tgz#255132243ab6fb20d3c7c92a585e2c4fa50368fe" - integrity sha512-aoqAT2XIekIWoriwzOmGFAvTtVY5O7JjV21giozBTP5c6uZhpvTWRbmHXbmsjZqY4HnEZQRXWkSAppsIBweKqw== - -"@next/swc-win32-x64-msvc@13.5.4": - version "13.5.4" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.4.tgz#cc542907b55247c5634d9a8298e1c143a1847e25" - integrity sha512-cyRvlAxwlddlqeB9xtPSfNSCRy8BOa4wtMo0IuI9P7Y0XT2qpDrpFKRyZ7kUngZis59mPVla5k8X1oOJ8RxDYg== +"@next/swc-darwin-arm64@14.2.3": + version "14.2.3" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.3.tgz#db1a05eb88c0224089b815ad10ac128ec79c2cdb" + integrity sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A== + +"@next/swc-darwin-x64@14.2.3": + version "14.2.3" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.3.tgz#a3f8af05b5f9a52ac3082e66ac29e125ab1d7b9c" + integrity sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA== + +"@next/swc-linux-arm64-gnu@14.2.3": + version "14.2.3" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.3.tgz#4e63f43879285b52554bfd39e6e0cc78a9b27bbf" + integrity sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA== + +"@next/swc-linux-arm64-musl@14.2.3": + version "14.2.3" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.3.tgz#ebdaed26214448b1e6f2c3e8b3cd29bfba387990" + integrity sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw== + +"@next/swc-linux-x64-gnu@14.2.3": + version "14.2.3" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.3.tgz#19e3bcc137c3b582a1ab867106817e5c90a20593" + integrity sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w== + +"@next/swc-linux-x64-musl@14.2.3": + version "14.2.3" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz#794a539b98e064169cf0ff7741b2a4fb16adec7d" + integrity sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ== + +"@next/swc-win32-arm64-msvc@14.2.3": + version "14.2.3" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz#eda9fa0fbf1ff9113e87ac2668ee67ce9e5add5a" + integrity sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A== + +"@next/swc-win32-ia32-msvc@14.2.3": + version "14.2.3" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz#7c1190e3f640ab16580c6bdbd7d0e766b9920457" + integrity sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw== + +"@next/swc-win32-x64-msvc@14.2.3": + version "14.2.3" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz#2be4e39ee25bfbd85be78eea17c0e7751dc4323c" + integrity sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA== "@noble/curves@1.0.0", "@noble/curves@~1.0.0": version "1.0.0" @@ -3589,11 +3463,6 @@ dependencies: "@noble/hashes" "1.3.2" -"@noble/ed25519@^1.7.0": - version "1.7.1" - resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.7.1.tgz#6899660f6fbb97798a6fbd227227c4589a454724" - integrity sha512-Rk4SkJFaXZiznFyC/t77Q0NKS4FL7TLJJsVG2V2oiEq3kJVeTdxysEe/yRWSpnWMe808XRDJ+VFh5pt/FN5plw== - "@noble/hashes@1.3.0", "@noble/hashes@~1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.0.tgz#085fd70f6d7d9d109671090ccae1d3bec62554a1" @@ -3609,21 +3478,11 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== -"@noble/hashes@^1.1.2": - version "1.1.5" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.5.tgz#1a0377f3b9020efe2fae03290bd2a12140c95c11" - integrity sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ== - -"@noble/hashes@~1.3.2": +"@noble/hashes@^1.3.1", "@noble/hashes@~1.3.2": version "1.3.3" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699" integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== -"@noble/secp256k1@^1.6.3": - version "1.7.1" - resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" - integrity sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw== - "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -3671,6 +3530,11 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.7.0.tgz#b139c81999c23e3c8d3c0a7234480e945920fc40" integrity sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw== +"@opentelemetry/api@^1.4.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.8.0.tgz#5aa7abb48f23f693068ed2999ae627d2f7d902ec" + integrity sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w== + "@opentelemetry/auto-instrumentations-node@^0.39.4": version "0.39.4" resolved "https://registry.yarnpkg.com/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.39.4.tgz#c772201ba224e6ebffaaf17c9f2234e6e8343328" @@ -4728,10 +4592,10 @@ dependencies: playwright "1.41.1" -"@polka/url@^1.0.0-next.20": - version "1.0.0-next.23" - resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.23.tgz#498e41218ab3b6a1419c735e5c6ae2c5ed609b6c" - integrity sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg== +"@polka/url@^1.0.0-next.24": + version "1.0.0-next.25" + resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.25.tgz#f077fdc0b5d0078d30893396ff4827a13f99e817" + integrity sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ== "@popperjs/core@^2.9.3": version "2.11.6" @@ -4943,6 +4807,13 @@ prop-types "^15.7.2" tslib "^2.3.0" +"@react-native-async-storage/async-storage@^1.17.11": + version "1.22.3" + resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.22.3.tgz#ad490236a9eda8ac68cffc12c738f20b1815464e" + integrity sha512-Ov3wjuqxHd62tLYfgTjxj77YRYWra3A4Fi8uICIPcePgNO2WkS5B0ADXt9e/JLzSCNqVlXCq4Fir/gHmZTU9ww== + dependencies: + merge-options "^3.0.4" + "@rollup/pluginutils@^5.0.0": version "5.0.2" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.0.2.tgz#012b8f53c71e4f6f9cb317e311df1404f56e7a33" @@ -4953,11 +4824,11 @@ picomatch "^2.3.1" "@rushstack/eslint-patch@^1.1.3": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728" - integrity sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg== + version "1.10.2" + resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.2.tgz#053f1540703faa81dea2966b768ee5581c66aeda" + integrity sha512-hw437iINopmQuxWPSUEvqE56NCPsiU8N4AYtfHmJFckclktzK9YQJieD3XkDCDH4OjL+C7zgPUh73R/nrcHrqw== -"@safe-global/safe-apps-provider@^0.18.1": +"@safe-global/safe-apps-provider@0.18.1": version "0.18.1" resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-provider/-/safe-apps-provider-0.18.1.tgz#287b5a1e2ef3be630dacde54279409df3ced8202" integrity sha512-V4a05A3EgJcriqtDoJklDz1BOinWhC6P0hjUSxshA4KOZM7rGPCTto/usXs09zr1vvL28evl/NldSTv97j2bmg== @@ -4965,7 +4836,7 @@ "@safe-global/safe-apps-sdk" "^8.1.0" events "^3.3.0" -"@safe-global/safe-apps-sdk@^8.1.0": +"@safe-global/safe-apps-sdk@8.1.0", "@safe-global/safe-apps-sdk@^8.1.0": version "8.1.0" resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-sdk/-/safe-apps-sdk-8.1.0.tgz#d1d0c69cd2bf4eef8a79c5d677d16971926aa64a" integrity sha512-XJbEPuaVc7b9n23MqlF6c+ToYIS3f7P2Sel8f3cSBQ9WORE4xrSuvhMpK9fDSFqJ7by/brc+rmJR/5HViRr0/w== @@ -4980,16 +4851,16 @@ dependencies: cross-fetch "^3.1.5" +"@scure/base@^1.1.3", "@scure/base@~1.1.2": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.5.tgz#1d85d17269fe97694b9c592552dd9e5e33552157" + integrity sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ== + "@scure/base@~1.1.0": version "1.1.1" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== -"@scure/base@~1.1.2": - version "1.1.5" - resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.5.tgz#1d85d17269fe97694b9c592552dd9e5e33552157" - integrity sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ== - "@scure/bip32@1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.0.tgz#6c8d980ef3f290987736acd0ee2e0f0d50068d87" @@ -5140,34 +5011,10 @@ dependencies: react-script-hook "^1.7.2" -"@solana/buffer-layout@^4.0.0": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz#b996235eaec15b1e0b5092a8ed6028df77fa6c15" - integrity sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA== - dependencies: - buffer "~6.0.3" - -"@solana/web3.js@^1.70.1": - version "1.73.0" - resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.73.0.tgz#c65f9f954ac80fca6952765c931dd72e57e1b572" - integrity sha512-YrgX3Py7ylh8NYkbanoINUPCj//bWUjYZ5/WPy9nQ9SK3Cl7QWCR+NmbDjmC/fTspZGR+VO9LTQslM++jr5PRw== - dependencies: - "@babel/runtime" "^7.12.5" - "@noble/ed25519" "^1.7.0" - "@noble/hashes" "^1.1.2" - "@noble/secp256k1" "^1.6.3" - "@solana/buffer-layout" "^4.0.0" - agentkeepalive "^4.2.1" - bigint-buffer "^1.1.5" - bn.js "^5.0.0" - borsh "^0.7.0" - bs58 "^4.0.1" - buffer "6.0.1" - fast-stable-stringify "^1.0.0" - jayson "^3.4.4" - node-fetch "2" - rpc-websockets "^7.5.0" - superstruct "^0.14.2" +"@socket.io/component-emitter@~3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" + integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== "@stablelib/aead@^1.0.1": version "1.0.1" @@ -5723,11 +5570,17 @@ "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1" "^0.77.0" "@swagger-api/apidom-parser-adapter-yaml-1-2" "^0.77.0" -"@swc/helpers@0.5.2": - version "0.5.2" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d" - integrity sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw== +"@swc/counter@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9" + integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== + +"@swc/helpers@0.5.5": + version "0.5.5" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.5.tgz#12689df71bfc9b21c4f4ca00ae55f2f16c8b77c0" + integrity sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A== dependencies: + "@swc/counter" "^0.1.3" tslib "^2.4.0" "@tanstack/eslint-plugin-query@^5.0.5": @@ -5737,35 +5590,16 @@ dependencies: "@typescript-eslint/utils" "^6.20.0" -"@tanstack/query-core@4.29.11": - version "4.29.11" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.29.11.tgz#fa338f7d6897c6be5de6d8dabd603d9b78ee48c7" - integrity sha512-8C+hF6SFAb/TlFZyS9FItgNwrw4PMa7YeX+KQYe2ZAiEz6uzg6yIr+QBzPkUwZ/L0bXvGd1sufTm3wotoz+GwQ== - -"@tanstack/query-core@5.4.3": - version "5.4.3" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.4.3.tgz#fbdd36ccf1acf70579980f2e7cf16d2c2aa2a5e9" - integrity sha512-fnI9ORjcuLGm1sNrKatKIosRQUpuqcD4SV7RqRSVmj8JSicX2aoMyKryHEBpVQvf6N4PaBVgBxQomjsbsGPssQ== +"@tanstack/query-core@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.25.0.tgz#e08ed0a9fad34c8005d1a282e57280031ac50cdc" + integrity sha512-vlobHP64HTuSE68lWF1mEhwSRC5Q7gaT+a/m9S+ItuN+ruSOxe1rFnR9j0ACWQ314BPhBEVKfBQ6mHL0OWfdbQ== "@tanstack/query-devtools@5.4.2": version "5.4.2" resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.4.2.tgz#1687645ba1b9ffc76c55ac7759461d331d0776bf" integrity sha512-EXdaMXi8CxZuMp97J5mq6wy1RduOfoWFv5vtA1U+hyqb8Wst6M8kkkjDSdFvGZIRpYY4K8mKLlEFHNUZDG5dtw== -"@tanstack/query-persist-client-core@4.29.11": - version "4.29.11" - resolved "https://registry.yarnpkg.com/@tanstack/query-persist-client-core/-/query-persist-client-core-4.29.11.tgz#96b4b83bead480eb37e024a59fd59bfd84b0545e" - integrity sha512-CSmMZchr+446r79NJ/pjD2yfjqNqFV7k8BnqOq4yTZvXsaQLEIn3tsaU45IsPgs4N7g9OBfPUPDdapSQvck2WQ== - dependencies: - "@tanstack/query-core" "4.29.11" - -"@tanstack/query-sync-storage-persister@^4.27.1": - version "4.29.11" - resolved "https://registry.yarnpkg.com/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-4.29.11.tgz#577ead9d08588a420a24ff0a8b77de427a2e6ca8" - integrity sha512-JP9U3m9YPkUelcE9+7D6fAEsNEp2ysKsH6qrd0WJtqsuhe5Zwr407NxaQ5uB2ow/MmhPOm8bxzMZw00Y7RagoQ== - dependencies: - "@tanstack/query-persist-client-core" "4.29.11" - "@tanstack/react-query-devtools@^5.4.3": version "5.4.3" resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.4.3.tgz#7df56de0454104c229f25393cf57a957f6245186" @@ -5773,27 +5607,12 @@ dependencies: "@tanstack/query-devtools" "5.4.2" -"@tanstack/react-query-persist-client@^4.28.0": - version "4.29.12" - resolved "https://registry.yarnpkg.com/@tanstack/react-query-persist-client/-/react-query-persist-client-4.29.12.tgz#a3d3dc8dc4318689b275f7329db3619b710401e1" - integrity sha512-rh6zZJB+3j8lr+YsEkVadnqmUELmqNFZQzGGsHS5col/YOjYsMe9ppqaUjIMJ2aXnFXye50sbe4KxHhSGoaNVw== - dependencies: - "@tanstack/query-persist-client-core" "4.29.11" - -"@tanstack/react-query@^4.28.0": - version "4.29.12" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.29.12.tgz#de111cf1d6c389b86acacfaf972302914cfa1208" - integrity sha512-zhcN6+zF6cxprxhTHQajHGlvxgK8npnp9uLe9yaWhGc6sYcPWXzyO4raL4HomUzQOPzu3jLvkriJQ7BOrDM8vA== - dependencies: - "@tanstack/query-core" "4.29.11" - use-sync-external-store "^1.2.0" - "@tanstack/react-query@^5.4.3": - version "5.4.3" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.4.3.tgz#cf59120690032e44b8c1c4c463cfb43aaad2fc5f" - integrity sha512-4aSOrRNa6yEmf7mws5QPTVMn8Lp7L38tFoTZ0c1ZmhIvbr8GIA0WT7X5N3yz/nuK8hUtjw9cAzBr4BPDZZ+tzA== + version "5.25.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.25.0.tgz#f4dac794cf10dd956aa56dbbdf67049a5ba2669d" + integrity sha512-u+n5R7mLO7RmeiIonpaCRVXNRWtZEef/aVZ/XGWRPa7trBIvGtzlfo0Ah7ZtnTYfrKEVwnZ/tzRCBcoiqJ/tFw== dependencies: - "@tanstack/query-core" "5.4.3" + "@tanstack/query-core" "5.25.0" "@testing-library/dom@^9.0.0": version "9.3.0" @@ -5940,13 +5759,6 @@ dependencies: "@types/node" "*" -"@types/connect@^3.4.33": - version "3.4.35" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" - integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== - dependencies: - "@types/node" "*" - "@types/content-disposition@*": version "0.5.8" resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.8.tgz#6742a5971f490dc41e59d277eee71361fea0b537" @@ -6189,6 +6001,11 @@ dependencies: "@types/ms" "*" +"@types/dom-screen-wake-lock@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/dom-screen-wake-lock/-/dom-screen-wake-lock-1.0.3.tgz#c3588a5f6f40fae957f9ce5be9bc4927a61bb9a0" + integrity sha512-3Iten7X3Zgwvk6kh6/NRdwN7WbZ760YgFCsF5AxDifltUQzW1RaW+WRmcVtgwFzLjaNu64H+0MPJ13yRa8g3Dw== + "@types/dom-to-image@^2.6.4": version "2.6.4" resolved "https://registry.yarnpkg.com/@types/dom-to-image/-/dom-to-image-2.6.4.tgz#008411e23903cb0ee9e51a42ab8358c609541ee8" @@ -6496,11 +6313,6 @@ dependencies: undici-types "~5.26.4" -"@types/node@^12.12.54": - version "12.20.55" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" - integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== - "@types/papaparse@^5.3.5": version "5.3.5" resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-5.3.5.tgz#e5ad94b1fe98e2a8ea0b03284b83d2cb252bbf39" @@ -6635,6 +6447,13 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5" integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ== +"@types/secp256k1@^4.0.4": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@types/secp256k1/-/secp256k1-4.0.6.tgz#d60ba2349a51c2cbc5e816dcd831a42029d376bf" + integrity sha512-hHxJU6PAEUn0TP4S/ZOzuTUvJWuZ6eIKeNKb5RBpODvSl6hp1Wrw4s7ATY50rklRCScUDpHzVA/DQdSjJ3UoYQ== + dependencies: + "@types/node" "*" + "@types/semver@^7.3.12": version "7.3.13" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" @@ -6706,13 +6525,6 @@ resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== -"@types/ws@^7.4.4": - version "7.4.7" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" - integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww== - dependencies: - "@types/node" "*" - "@types/ws@^8.5.3": version "8.5.3" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d" @@ -6733,29 +6545,29 @@ "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^5.60.0": - version "5.60.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.60.1.tgz#81382d6ecb92b8dda70e91f9035611cb2fecd1c3" - integrity sha512-KSWsVvsJsLJv3c4e73y/Bzt7OpqMCADUO846bHcuWYSYM19bldbAeDv7dYyV0jwkbMfJ2XdlzwjhXtuD7OY6bw== + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db" + integrity sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag== dependencies: "@eslint-community/regexpp" "^4.4.0" - "@typescript-eslint/scope-manager" "5.60.1" - "@typescript-eslint/type-utils" "5.60.1" - "@typescript-eslint/utils" "5.60.1" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/type-utils" "5.62.0" + "@typescript-eslint/utils" "5.62.0" debug "^4.3.4" - grapheme-splitter "^1.0.4" + graphemer "^1.4.0" ignore "^5.2.0" natural-compare-lite "^1.4.0" semver "^7.3.7" tsutils "^3.21.0" "@typescript-eslint/parser@^5.42.0": - version "5.59.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.0.tgz#0ad7cd019346cc5d150363f64869eca10ca9977c" - integrity sha512-qK9TZ70eJtjojSUMrrEwA9ZDQ4N0e/AuoOIgXuNBorXYcBDk397D2r5MIe1B3cok/oCtdNC5j+lUUpVB+Dpb+w== + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.62.0.tgz#1b63d082d849a2fcae8a569248fbe2ee1b8a56c7" + integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA== dependencies: - "@typescript-eslint/scope-manager" "5.59.0" - "@typescript-eslint/types" "5.59.0" - "@typescript-eslint/typescript-estree" "5.59.0" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/typescript-estree" "5.62.0" debug "^4.3.4" "@typescript-eslint/scope-manager@5.45.0": @@ -6766,37 +6578,21 @@ "@typescript-eslint/types" "5.45.0" "@typescript-eslint/visitor-keys" "5.45.0" -"@typescript-eslint/scope-manager@5.59.0": - version "5.59.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.0.tgz#86501d7a17885710b6716a23be2e93fc54a4fe8c" - integrity sha512-tsoldKaMh7izN6BvkK6zRMINj4Z2d6gGhO2UsI8zGZY3XhLq1DndP3Ycjhi1JwdwPRwtLMW4EFPgpuKhbCGOvQ== - dependencies: - "@typescript-eslint/types" "5.59.0" - "@typescript-eslint/visitor-keys" "5.59.0" - -"@typescript-eslint/scope-manager@5.60.1": - version "5.60.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.60.1.tgz#35abdb47f500c68c08f2f2b4f22c7c79472854bb" - integrity sha512-Dn/LnN7fEoRD+KspEOV0xDMynEmR3iSHdgNsarlXNLGGtcUok8L4N71dxUgt3YvlO8si7E+BJ5Fe3wb5yUw7DQ== - dependencies: - "@typescript-eslint/types" "5.60.1" - "@typescript-eslint/visitor-keys" "5.60.1" - -"@typescript-eslint/scope-manager@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" - integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== +"@typescript-eslint/scope-manager@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c" + integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w== dependencies: "@typescript-eslint/types" "6.21.0" "@typescript-eslint/visitor-keys" "6.21.0" -"@typescript-eslint/type-utils@5.60.1": - version "5.60.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.60.1.tgz#17770540e98d65ab4730c7aac618003f702893f4" - integrity sha512-vN6UztYqIu05nu7JqwQGzQKUJctzs3/Hg7E2Yx8rz9J+4LgtIDFWjjl1gm3pycH0P3mHAcEUBd23LVgfrsTR8A== +"@typescript-eslint/type-utils@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz#286f0389c41681376cdad96b309cedd17d70346a" + integrity sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew== dependencies: - "@typescript-eslint/typescript-estree" "5.60.1" - "@typescript-eslint/utils" "5.60.1" + "@typescript-eslint/typescript-estree" "5.62.0" + "@typescript-eslint/utils" "5.62.0" debug "^4.3.4" tsutils "^3.21.0" @@ -6805,20 +6601,10 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.45.0.tgz#794760b9037ee4154c09549ef5a96599621109c5" integrity sha512-QQij+u/vgskA66azc9dCmx+rev79PzX8uDHpsqSjEFtfF2gBUTRCpvYMh2gw2ghkJabNkPlSUCimsyBEQZd1DA== -"@typescript-eslint/types@5.59.0": - version "5.59.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.0.tgz#3fcdac7dbf923ec5251545acdd9f1d42d7c4fe32" - integrity sha512-yR2h1NotF23xFFYKHZs17QJnB51J/s+ud4PYU4MqdZbzeNxpgUr05+dNeCN/bb6raslHvGdd6BFCkVhpPk/ZeA== - -"@typescript-eslint/types@5.60.1": - version "5.60.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.60.1.tgz#a17473910f6b8d388ea83c9d7051af89c4eb7561" - integrity sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg== - -"@typescript-eslint/types@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" - integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== +"@typescript-eslint/types@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" + integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== "@typescript-eslint/typescript-estree@5.45.0": version "5.45.0" @@ -6833,36 +6619,10 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@5.59.0": - version "5.59.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.0.tgz#8869156ee1dcfc5a95be3ed0e2809969ea28e965" - integrity sha512-sUNnktjmI8DyGzPdZ8dRwW741zopGxltGs/SAPgGL/AAgDpiLsCFLcMNSpbfXfmnNeHmK9h3wGmCkGRGAoUZAg== - dependencies: - "@typescript-eslint/types" "5.59.0" - "@typescript-eslint/visitor-keys" "5.59.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" - -"@typescript-eslint/typescript-estree@5.60.1": - version "5.60.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.1.tgz#8c71824b7165b64d5ebd7aa42968899525959834" - integrity sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw== - dependencies: - "@typescript-eslint/types" "5.60.1" - "@typescript-eslint/visitor-keys" "5.60.1" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" - -"@typescript-eslint/typescript-estree@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" - integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== +"@typescript-eslint/typescript-estree@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" + integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== dependencies: "@typescript-eslint/types" "6.21.0" "@typescript-eslint/visitor-keys" "6.21.0" @@ -6873,17 +6633,17 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/utils@5.60.1": - version "5.60.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.60.1.tgz#6861ebedbefba1ac85482d2bdef6f2ff1eb65b80" - integrity sha512-tiJ7FFdFQOWssFa3gqb94Ilexyw0JVxj6vBzaSpfN/8IhoKkDuSAenUKvsSHw2A/TMpJb26izIszTXaqygkvpQ== +"@typescript-eslint/utils@5.62.0", "@typescript-eslint/utils@^5.54.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86" + integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.60.1" - "@typescript-eslint/types" "5.60.1" - "@typescript-eslint/typescript-estree" "5.60.1" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/typescript-estree" "5.62.0" eslint-scope "^5.1.1" semver "^7.3.7" @@ -6901,19 +6661,6 @@ eslint-utils "^3.0.0" semver "^7.3.7" -"@typescript-eslint/utils@^6.20.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.21.0.tgz#4714e7a6b39e773c1c8e97ec587f520840cd8134" - integrity sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ== - dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@types/json-schema" "^7.0.12" - "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "6.21.0" - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/typescript-estree" "6.21.0" - semver "^7.5.4" - "@typescript-eslint/visitor-keys@5.45.0": version "5.45.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.0.tgz#e0d160e9e7fdb7f8da697a5b78e7a14a22a70528" @@ -6922,29 +6669,18 @@ "@typescript-eslint/types" "5.45.0" eslint-visitor-keys "^3.3.0" -"@typescript-eslint/visitor-keys@5.59.0": - version "5.59.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.0.tgz#a59913f2bf0baeb61b5cfcb6135d3926c3854365" - integrity sha512-qZ3iXxQhanchCeaExlKPV3gDQFxMUmU35xfd5eCXB6+kUw1TUAbIy2n7QIrwz9s98DQLzNWyHp61fY0da4ZcbA== +"@typescript-eslint/visitor-keys@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" + integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== dependencies: - "@typescript-eslint/types" "5.59.0" - eslint-visitor-keys "^3.3.0" + "@typescript-eslint/types" "6.21.0" + eslint-visitor-keys "^3.4.1" -"@typescript-eslint/visitor-keys@5.60.1": - version "5.60.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.60.1.tgz#19a877358bf96318ec35d90bfe6bd1445cce9434" - integrity sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw== - dependencies: - "@typescript-eslint/types" "5.60.1" - eslint-visitor-keys "^3.3.0" - -"@typescript-eslint/visitor-keys@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" - integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== - dependencies: - "@typescript-eslint/types" "6.21.0" - eslint-visitor-keys "^3.4.1" +"@ungap/structured-clone@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== "@vitejs/plugin-react@^4.0.0": version "4.0.0" @@ -7051,35 +6787,31 @@ resolved "https://registry.yarnpkg.com/@wagmi/chains/-/chains-1.2.0.tgz#d59eaa70ec51a5fdcd113975926992acfb17ab12" integrity sha512-dmDRipsE54JfyudOBkuhEexqQWcrZqxn/qiujG8SBzMh/az/AH5xlJSA+j1CPWTx9+QofSMF3B7A4gb6XRmSaQ== -"@wagmi/connectors@3.1.10": - version "3.1.10" - resolved "https://registry.yarnpkg.com/@wagmi/connectors/-/connectors-3.1.10.tgz#830cd788579ef56a2526574914481f2d3aabc9f7" - integrity sha512-ZLJC1QaeiZarkF07Cr9mOlVjPO1Lf5TBx+JKBms2y5fUIXlKrxCfQgO/gDCureboI+Us2X3IRI659+XacSGpbA== +"@wagmi/connectors@4.1.22": + version "4.1.22" + resolved "https://registry.yarnpkg.com/@wagmi/connectors/-/connectors-4.1.22.tgz#6e2b253a5e08ff8a145cfc2beb943a375a073414" + integrity sha512-9t14G28awyeWTjGS/OfEd/DhdeX5t4Pio3SJHw4M4aTexIfN5DHGmmtfUtSu6DP1r+LD2vSI4ShT7vH0dgtZwA== dependencies: - "@coinbase/wallet-sdk" "^3.6.6" - "@safe-global/safe-apps-provider" "^0.18.1" - "@safe-global/safe-apps-sdk" "^8.1.0" - "@walletconnect/ethereum-provider" "2.10.6" - "@walletconnect/legacy-provider" "^2.0.0" + "@coinbase/wallet-sdk" "3.9.1" + "@metamask/sdk" "0.14.3" + "@safe-global/safe-apps-provider" "0.18.1" + "@safe-global/safe-apps-sdk" "8.1.0" + "@walletconnect/ethereum-provider" "2.11.2" "@walletconnect/modal" "2.6.2" - "@walletconnect/utils" "2.10.2" - abitype "0.8.7" - eventemitter3 "^4.0.7" -"@wagmi/core@1.4.12": - version "1.4.12" - resolved "https://registry.yarnpkg.com/@wagmi/core/-/core-1.4.12.tgz#84558d1af746b2bcef02337dc3c46108242b1dd7" - integrity sha512-bLcYmmGgjtl3jAGo8X3Sm6oUwsdjbVxFMu9SWnwHdE4S9JdYeWM57dEhQgq8SYul2yQ7yY2/gimBf1Or0Ky3dQ== +"@wagmi/core@2.6.13": + version "2.6.13" + resolved "https://registry.yarnpkg.com/@wagmi/core/-/core-2.6.13.tgz#eaca669b6b4283ec69412a206e66182a11d926c6" + integrity sha512-RZQ0mBXMH5oe7uvFkNh0e/gPWWLsbkDv/6/SeTY2H7fsH4I/T62ID6eNYUtyOoz7FgQTdAyPdSEt9lst1ZXxkQ== dependencies: - "@wagmi/connectors" "3.1.10" - abitype "0.8.7" - eventemitter3 "^4.0.7" - zustand "^4.3.1" + eventemitter3 "5.0.1" + mipd "0.0.5" + zustand "4.4.1" -"@walletconnect/core@2.10.6": - version "2.10.6" - resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.10.6.tgz#786b0d2e2045c210c917e29bfa0498bbc210be20" - integrity sha512-Z4vh4ZdfcoQjgPEOxeuF9HUZCVLtV3MgRbS/awLIj/omDrFnOwlBhxi5Syr4Y8muVGC0ocRetQYHae0/gX5crQ== +"@walletconnect/core@2.11.2": + version "2.11.2" + resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.11.2.tgz#35286be92c645fa461fecc0dfe25de9f076fca8f" + integrity sha512-bB4SiXX8hX3/hyBfVPC5gwZCXCl+OPj+/EDVM71iAO3TDsh78KPbrVAbDnnsbHzZVHlsMohtXX3j5XVsheN3+g== dependencies: "@walletconnect/heartbeat" "1.2.1" "@walletconnect/jsonrpc-provider" "1.0.13" @@ -7092,33 +6824,13 @@ "@walletconnect/relay-auth" "^1.0.4" "@walletconnect/safe-json" "^1.0.2" "@walletconnect/time" "^1.0.2" - "@walletconnect/types" "2.10.6" - "@walletconnect/utils" "2.10.6" + "@walletconnect/types" "2.11.2" + "@walletconnect/utils" "2.11.2" events "^3.3.0" + isomorphic-unfetch "3.1.0" lodash.isequal "4.5.0" uint8arrays "^3.1.0" -"@walletconnect/crypto@^1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@walletconnect/crypto/-/crypto-1.0.3.tgz#7b8dd4d7e2884fe3543c7c07aea425eef5ef9dd4" - integrity sha512-+2jdORD7XQs76I2Odgr3wwrtyuLUXD/kprNVsjWRhhhdO9Mt6WqVzOPu0/t7OHSmgal8k7SoBQzUc5hu/8zL/g== - dependencies: - "@walletconnect/encoding" "^1.0.2" - "@walletconnect/environment" "^1.0.1" - "@walletconnect/randombytes" "^1.0.3" - aes-js "^3.1.2" - hash.js "^1.1.7" - tslib "1.14.1" - -"@walletconnect/encoding@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@walletconnect/encoding/-/encoding-1.0.2.tgz#cb3942ad038d6a6bf01158f66773062dd25724da" - integrity sha512-CrwSBrjqJ7rpGQcTL3kU+Ief+Bcuu9PH6JLOb+wM6NITX1GTxR/MfNwnQfhLKK6xpRAyj2/nM04OOH6wS8Imag== - dependencies: - is-typedarray "1.0.0" - tslib "1.14.1" - typedarray-to-buffer "3.1.5" - "@walletconnect/environment@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/environment/-/environment-1.0.1.tgz#1d7f82f0009ab821a2ba5ad5e5a7b8ae3b214cd7" @@ -7126,20 +6838,20 @@ dependencies: tslib "1.14.1" -"@walletconnect/ethereum-provider@2.10.6": - version "2.10.6" - resolved "https://registry.yarnpkg.com/@walletconnect/ethereum-provider/-/ethereum-provider-2.10.6.tgz#53720771cc2d6accd452916a853ac927f26acbaa" - integrity sha512-bBQ+yUfxLv8VxNttgNKY7nED35gSVayO/BnLHbNKvyV1gpvSCla5mWB9MsXuQs70MK0g+/qtgRVSrOtdSubaNQ== +"@walletconnect/ethereum-provider@2.11.2": + version "2.11.2" + resolved "https://registry.yarnpkg.com/@walletconnect/ethereum-provider/-/ethereum-provider-2.11.2.tgz#914f773e37a879bc00cf367437c4e98a826247b1" + integrity sha512-BUDqee0Uy2rCZVkW5Ao3q6Ado/3fePYnFdryVF+YL6bPhj+xQZ5OfKodl+uvs7Rwq++O5wTX2RqOTzpW7+v+Mg== dependencies: "@walletconnect/jsonrpc-http-connection" "^1.0.7" "@walletconnect/jsonrpc-provider" "^1.0.13" "@walletconnect/jsonrpc-types" "^1.0.3" "@walletconnect/jsonrpc-utils" "^1.0.8" - "@walletconnect/modal" "^2.4.3" - "@walletconnect/sign-client" "2.10.6" - "@walletconnect/types" "2.10.6" - "@walletconnect/universal-provider" "2.10.6" - "@walletconnect/utils" "2.10.6" + "@walletconnect/modal" "^2.6.2" + "@walletconnect/sign-client" "2.11.2" + "@walletconnect/types" "2.11.2" + "@walletconnect/universal-provider" "2.11.2" + "@walletconnect/utils" "2.11.2" events "^3.3.0" "@walletconnect/events@^1.0.1": @@ -7159,16 +6871,6 @@ "@walletconnect/time" "^1.0.2" tslib "1.14.1" -"@walletconnect/jsonrpc-http-connection@^1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-http-connection/-/jsonrpc-http-connection-1.0.4.tgz#aeb0f7eae6565dd031f01d650ee73d358d760ee2" - integrity sha512-ji79pspdBhmIbTwve383tMaDu5Le9plW+oj5GE2aqzxIl3ib8JvRBZRn5lGEBGqVCvqB3MBJL7gBlEwpyRtoxQ== - dependencies: - "@walletconnect/jsonrpc-utils" "^1.0.4" - "@walletconnect/safe-json" "^1.0.1" - cross-fetch "^3.1.4" - tslib "1.14.1" - "@walletconnect/jsonrpc-http-connection@^1.0.7": version "1.0.7" resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-http-connection/-/jsonrpc-http-connection-1.0.7.tgz#a6973569b8854c22da707a759d241e4f5c2d5a98" @@ -7188,15 +6890,6 @@ "@walletconnect/safe-json" "^1.0.2" tslib "1.14.1" -"@walletconnect/jsonrpc-provider@^1.0.6": - version "1.0.6" - resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-provider/-/jsonrpc-provider-1.0.6.tgz#e91321ef523f1904e6634e7866a0f3c6f056d2cd" - integrity sha512-f5vQxr53vUVQ51/9mRLb1OiNciT/546XZ68Byn9OYnDBGeGJXK2kQWDHp8sPWZbN5x0p7B6asdCWMVFJ6danlw== - dependencies: - "@walletconnect/jsonrpc-utils" "^1.0.4" - "@walletconnect/safe-json" "^1.0.1" - tslib "1.14.1" - "@walletconnect/jsonrpc-types@1.0.3", "@walletconnect/jsonrpc-types@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-types/-/jsonrpc-types-1.0.3.tgz#65e3b77046f1a7fa8347ae02bc1b841abe6f290c" @@ -7222,15 +6915,6 @@ "@walletconnect/jsonrpc-types" "^1.0.3" tslib "1.14.1" -"@walletconnect/jsonrpc-utils@^1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-utils/-/jsonrpc-utils-1.0.4.tgz#2009ba3907b02516f2caacd2fb871ff0d472b2cb" - integrity sha512-y0+tDxcTZ9BHBBKBJbjZxLUXb+zQZCylf7y/jTvDPNx76J0hYYc+F9zHzyqBLeorSKepLTk6yI8hw3NXbAQB3g== - dependencies: - "@walletconnect/environment" "^1.0.1" - "@walletconnect/jsonrpc-types" "^1.0.2" - tslib "1.14.1" - "@walletconnect/jsonrpc-ws-connection@1.0.14": version "1.0.14" resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-ws-connection/-/jsonrpc-ws-connection-1.0.14.tgz#eec700e74766c7887de2bd76c91a0206628732aa" @@ -7241,14 +6925,6 @@ events "^3.3.0" ws "^7.5.1" -"@walletconnect/keyvaluestorage@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.0.2.tgz#92f5ca0f54c1a88a093778842ce0c874d86369c8" - integrity sha512-U/nNG+VLWoPFdwwKx0oliT4ziKQCEoQ27L5Hhw8YOFGA2Po9A9pULUYNWhDgHkrb0gYDNt//X7wABcEWWBd3FQ== - dependencies: - safe-json-utils "^1.1.1" - tslib "1.14.1" - "@walletconnect/keyvaluestorage@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz#dd2caddabfbaf80f6b8993a0704d8b83115a1842" @@ -7258,66 +6934,6 @@ idb-keyval "^6.2.1" unstorage "^1.9.0" -"@walletconnect/legacy-client@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@walletconnect/legacy-client/-/legacy-client-2.0.0.tgz#9f2c09694789fd4b6c5d68d6423b44bac55aed30" - integrity sha512-v5L7rYk9loVnfvUf0mF+76bUPFaU5/Vh7mzL6/950CD/yoGdzYZ3Kj+L7mkC6HPMEGeQsBP1+sqBuiVGZ/aODA== - dependencies: - "@walletconnect/crypto" "^1.0.3" - "@walletconnect/encoding" "^1.0.2" - "@walletconnect/jsonrpc-utils" "^1.0.4" - "@walletconnect/legacy-types" "^2.0.0" - "@walletconnect/legacy-utils" "^2.0.0" - "@walletconnect/safe-json" "^1.0.1" - "@walletconnect/window-getters" "^1.0.1" - "@walletconnect/window-metadata" "^1.0.1" - detect-browser "^5.3.0" - query-string "^6.13.5" - -"@walletconnect/legacy-modal@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@walletconnect/legacy-modal/-/legacy-modal-2.0.0.tgz#d0fab01a1337a8f5d88cdb1430cbef2d46072bbf" - integrity sha512-jckNd8lMhm4X7dX9TDdxM3bXKJnaqkRs6K2Mo5j6GmbIF9Eyx40jZ5+q457RVxvM6ciZEDT5s1wBHWdWoOo+9Q== - dependencies: - "@walletconnect/legacy-types" "^2.0.0" - "@walletconnect/legacy-utils" "^2.0.0" - copy-to-clipboard "^3.3.3" - preact "^10.12.0" - qrcode "^1.5.1" - -"@walletconnect/legacy-provider@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@walletconnect/legacy-provider/-/legacy-provider-2.0.0.tgz#08e2db1e4c234743b2f30422bc8100bc42e8fc44" - integrity sha512-A8xPebMI1A+50HbWwTpFCbwP7G+1NGKdTKyg8BUUg3h3Y9JucpC1W6w/x0v1Xw7qFEqQnz74LoIN/A3ytH9xrQ== - dependencies: - "@walletconnect/jsonrpc-http-connection" "^1.0.4" - "@walletconnect/jsonrpc-provider" "^1.0.6" - "@walletconnect/legacy-client" "^2.0.0" - "@walletconnect/legacy-modal" "^2.0.0" - "@walletconnect/legacy-types" "^2.0.0" - "@walletconnect/legacy-utils" "^2.0.0" - -"@walletconnect/legacy-types@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@walletconnect/legacy-types/-/legacy-types-2.0.0.tgz#224278ae2874c6a2ca805c2d1d062a511dcf7227" - integrity sha512-sOVrA7HUdbI1OwKyPOQU0/DdvTSVFlsXWpAk2K2WvP2erTkBWPMTJq6cv2BmKdoJ3p6gLApT7sd+jHi3OF71uw== - dependencies: - "@walletconnect/jsonrpc-types" "^1.0.2" - -"@walletconnect/legacy-utils@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@walletconnect/legacy-utils/-/legacy-utils-2.0.0.tgz#e3a637c00783f9cd2ae139b640f82223ab78ed9d" - integrity sha512-CPWxSVVXw0kgNCxvU126g4GiV3mzXmC8IPJ15twE46aJ1FX+RHEIfAzFMFz2F2+fEhBxL63A7dwNQKDXorRPcQ== - dependencies: - "@walletconnect/encoding" "^1.0.2" - "@walletconnect/jsonrpc-utils" "^1.0.4" - "@walletconnect/legacy-types" "^2.0.0" - "@walletconnect/safe-json" "^1.0.1" - "@walletconnect/window-getters" "^1.0.1" - "@walletconnect/window-metadata" "^1.0.1" - detect-browser "^5.3.0" - query-string "^6.13.5" - "@walletconnect/logger@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/logger/-/logger-2.0.1.tgz#7f489b96e9a1ff6bf3e58f0fbd6d69718bf844a8" @@ -7343,7 +6959,7 @@ motion "10.16.2" qrcode "1.5.3" -"@walletconnect/modal@2.6.2", "@walletconnect/modal@^2.4.3": +"@walletconnect/modal@2.6.2", "@walletconnect/modal@^2.6.2": version "2.6.2" resolved "https://registry.yarnpkg.com/@walletconnect/modal/-/modal-2.6.2.tgz#4b534a836f5039eeb3268b80be7217a94dd12651" integrity sha512-eFopgKi8AjKf/0U4SemvcYw9zlLpx9njVN8sf6DAkowC2Md0gPU/UNEbH1Wwj407pEKnEds98pKWib1NN1ACoA== @@ -7351,16 +6967,6 @@ "@walletconnect/modal-core" "2.6.2" "@walletconnect/modal-ui" "2.6.2" -"@walletconnect/randombytes@^1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@walletconnect/randombytes/-/randombytes-1.0.3.tgz#e795e4918367fd1e6a2215e075e64ab93e23985b" - integrity sha512-35lpzxcHFbTN3ABefC9W+uBpNZl1GC4Wpx0ed30gibfO/y9oLdy1NznbV96HARQKSBV9J9M/rrtIvf6a23jfYw== - dependencies: - "@walletconnect/encoding" "^1.0.2" - "@walletconnect/environment" "^1.0.1" - randombytes "^2.1.0" - tslib "1.14.1" - "@walletconnect/relay-api@^1.0.9": version "1.0.9" resolved "https://registry.yarnpkg.com/@walletconnect/relay-api/-/relay-api-1.0.9.tgz#f8c2c3993dddaa9f33ed42197fc9bfebd790ecaf" @@ -7395,19 +7001,19 @@ dependencies: tslib "1.14.1" -"@walletconnect/sign-client@2.10.6": - version "2.10.6" - resolved "https://registry.yarnpkg.com/@walletconnect/sign-client/-/sign-client-2.10.6.tgz#722d2c2844565e2826dce6a6d3a36c9b3ca1ea91" - integrity sha512-EvUWjaZBQu2yKnH5/5F2qzbuiIuUN9ZgrNKgvXkw5z1Dq5RJCks0S9/MFlKH/ZSGqXnLl7uAzBXtoX4sMgbCMA== +"@walletconnect/sign-client@2.11.2": + version "2.11.2" + resolved "https://registry.yarnpkg.com/@walletconnect/sign-client/-/sign-client-2.11.2.tgz#855609653855f0d23b0502cdbdcf43402e34c459" + integrity sha512-MfBcuSz2GmMH+P7MrCP46mVE5qhP0ZyWA0FyIH6/WuxQ6G+MgKsGfaITqakpRPsykWOJq8tXMs3XvUPDU413OQ== dependencies: - "@walletconnect/core" "2.10.6" + "@walletconnect/core" "2.11.2" "@walletconnect/events" "^1.0.1" "@walletconnect/heartbeat" "1.2.1" "@walletconnect/jsonrpc-utils" "1.0.8" "@walletconnect/logger" "^2.0.1" "@walletconnect/time" "^1.0.2" - "@walletconnect/types" "2.10.6" - "@walletconnect/utils" "2.10.6" + "@walletconnect/types" "2.11.2" + "@walletconnect/utils" "2.11.2" events "^3.3.0" "@walletconnect/time@^1.0.2": @@ -7417,22 +7023,10 @@ dependencies: tslib "1.14.1" -"@walletconnect/types@2.10.2": - version "2.10.2" - resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.10.2.tgz#68e433a29ec2cf42d79d8b50c77bd5c1d91db721" - integrity sha512-luNV+07Wdla4STi9AejseCQY31tzWKQ5a7C3zZZaRK/di+rFaAAb7YW04OP4klE7tw/mJRGPTlekZElmHxO8kQ== - dependencies: - "@walletconnect/events" "^1.0.1" - "@walletconnect/heartbeat" "1.2.1" - "@walletconnect/jsonrpc-types" "1.0.3" - "@walletconnect/keyvaluestorage" "^1.0.2" - "@walletconnect/logger" "^2.0.1" - events "^3.3.0" - -"@walletconnect/types@2.10.6": - version "2.10.6" - resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.10.6.tgz#d9920ed4fd0113e0addbda8e7e73a5176a3163fd" - integrity sha512-WgHfiTG1yakmxheaBRiXhUdEmgxwrvsAdOIWaMf/spvrzVKYh6sHI3oyEEky5qj5jjiMiyQBeB57QamzCotbcQ== +"@walletconnect/types@2.11.2": + version "2.11.2" + resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.11.2.tgz#d0359dd4106fcaa1634241a00428d3ea08d0d3c7" + integrity sha512-p632MFB+lJbip2cvtXPBQslpUdiw1sDtQ5y855bOlAGquay+6fZ4h1DcDePeKQDQM3P77ax2a9aNPZxV6y/h1Q== dependencies: "@walletconnect/events" "^1.0.1" "@walletconnect/heartbeat" "1.2.1" @@ -7441,25 +7035,25 @@ "@walletconnect/logger" "^2.0.1" events "^3.3.0" -"@walletconnect/universal-provider@2.10.6": - version "2.10.6" - resolved "https://registry.yarnpkg.com/@walletconnect/universal-provider/-/universal-provider-2.10.6.tgz#1a6c42517581f11ce275474bc70d0eb4f1044525" - integrity sha512-CEivusqqoD31BhCTKp08DnrccfGjwD9MFjZs5BNRorDteRFE8zVm9LmP6DSiNJCw82ZajGlZThggLQ/BAATfwA== +"@walletconnect/universal-provider@2.11.2": + version "2.11.2" + resolved "https://registry.yarnpkg.com/@walletconnect/universal-provider/-/universal-provider-2.11.2.tgz#bec3038f51445d707bbec75f0cb8af0a1f1e04db" + integrity sha512-cNtIn5AVoDxKAJ4PmB8m5adnf5mYQMUamEUPKMVvOPscfGtIMQEh9peKsh2AN5xcRVDbgluC01Id545evFyymw== dependencies: "@walletconnect/jsonrpc-http-connection" "^1.0.7" "@walletconnect/jsonrpc-provider" "1.0.13" "@walletconnect/jsonrpc-types" "^1.0.2" "@walletconnect/jsonrpc-utils" "^1.0.7" "@walletconnect/logger" "^2.0.1" - "@walletconnect/sign-client" "2.10.6" - "@walletconnect/types" "2.10.6" - "@walletconnect/utils" "2.10.6" + "@walletconnect/sign-client" "2.11.2" + "@walletconnect/types" "2.11.2" + "@walletconnect/utils" "2.11.2" events "^3.3.0" -"@walletconnect/utils@2.10.2": - version "2.10.2" - resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.10.2.tgz#1f2c6a2f1bb95bcc4517b1e94aa7164c9286eb46" - integrity sha512-syxXRpc2yhSknMu3IfiBGobxOY7fLfLTJuw+ppKaeO6WUdZpIit3wfuGOcc0Ms3ZPFCrGfyGOoZsCvgdXtptRg== +"@walletconnect/utils@2.11.2": + version "2.11.2" + resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.11.2.tgz#dee0f19adf5e38543612cbe9fa4de7ed28eb7e85" + integrity sha512-LyfdmrnZY6dWqlF4eDrx5jpUwsB2bEPjoqR5Z6rXPiHJKUOdJt7az+mNOn5KTSOlRpd1DmozrBrWr+G9fFLYVw== dependencies: "@stablelib/chacha20poly1305" "1.0.1" "@stablelib/hkdf" "1.0.1" @@ -7469,27 +7063,7 @@ "@walletconnect/relay-api" "^1.0.9" "@walletconnect/safe-json" "^1.0.2" "@walletconnect/time" "^1.0.2" - "@walletconnect/types" "2.10.2" - "@walletconnect/window-getters" "^1.0.1" - "@walletconnect/window-metadata" "^1.0.1" - detect-browser "5.3.0" - query-string "7.1.3" - uint8arrays "^3.1.0" - -"@walletconnect/utils@2.10.6": - version "2.10.6" - resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.10.6.tgz#749b37d14e291e346862e7027ec7548463350226" - integrity sha512-oRsWWhN2+hi3aiDXrQEOfysz6FHQJGXLsNQPVt+WIBJplO6Szmdau9dbleD88u1iiT4GKPqE0R9FOYvvPm1H/w== - dependencies: - "@stablelib/chacha20poly1305" "1.0.1" - "@stablelib/hkdf" "1.0.1" - "@stablelib/random" "^1.0.2" - "@stablelib/sha256" "1.0.1" - "@stablelib/x25519" "^1.0.3" - "@walletconnect/relay-api" "^1.0.9" - "@walletconnect/safe-json" "^1.0.2" - "@walletconnect/time" "^1.0.2" - "@walletconnect/types" "2.10.6" + "@walletconnect/types" "2.11.2" "@walletconnect/window-getters" "^1.0.1" "@walletconnect/window-metadata" "^1.0.1" detect-browser "5.3.0" @@ -7511,100 +7085,102 @@ "@walletconnect/window-getters" "^1.0.1" tslib "1.14.1" -"@web3modal/common@3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@web3modal/common/-/common-3.5.0.tgz#010f24c7170c70fbad15fae9500ede8a18b43e0d" - integrity sha512-cpNFf6TiPYXEozYtIzLH9PcWSlFeoeDM6hTiXEVNutJeFRppFICLOAsyFoa7MszexFJaZ21cAxj/PzI2GIKGjA== +"@web3modal/common@4.1.3": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@web3modal/common/-/common-4.1.3.tgz#b31a60dbfb2b02708ddef7b896bd2992534ede09" + integrity sha512-xxNS0+J88TQHNKxHk57uEIb1J03OI9n9CulaOexHv7oPp2x6D+vb5FYdJj/Oxskq6osYKg+LVupCSEaJEr6bQw== dependencies: dayjs "1.11.10" -"@web3modal/core@3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@web3modal/core/-/core-3.5.0.tgz#089c47c8dc9a41f1f3c50374d682860b7c34e81c" - integrity sha512-yZFk4YZDcmOhRyKkQYP0xdxK3JfdVwayive6UyBRY1bOxl4V4PAs+vRNLNY/3LulMMpqaHZGuD1zty1GXJ7INA== +"@web3modal/core@4.1.3": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@web3modal/core/-/core-4.1.3.tgz#92a1a2a7c00b4f9d8221c20adce3a1ce908fb662" + integrity sha512-T8J9vfxiDrnRQxuqRQtfE1siBSf7WQxgyfvgPfUmAtnzAYVpQ5eAL4JEUTp6KrOyB070ujT5fmfBjz8di8agQg== dependencies: - "@web3modal/common" "3.5.0" - valtio "1.12.1" + "@web3modal/common" "4.1.3" + "@web3modal/wallet" "4.1.3" + valtio "1.11.2" -"@web3modal/polyfills@3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@web3modal/polyfills/-/polyfills-3.5.0.tgz#e3ee329a14de924ba796dccaa58dfafe42ac5819" - integrity sha512-QxVgUd+GKENGLo84bNTXjHk78l61Yr65LLvq5GPQWwrIPWFDqP2Nf7ixtaI36tTx29VN2SVyzBXGMhtrL73ZTg== +"@web3modal/polyfills@4.1.3": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@web3modal/polyfills/-/polyfills-4.1.3.tgz#444fd1d7a942ba91592533d34cee6dcf16f19ef9" + integrity sha512-CTT1A0B43Ae4uSm8GW4F6Qamhx3tcIC0/yyOSW40BaRvd10K2Imm50K3I8jLFS8HG1ZzqZe+pOV2P/OyOwPU4g== dependencies: buffer "6.0.3" -"@web3modal/scaffold-react@3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@web3modal/scaffold-react/-/scaffold-react-3.5.0.tgz#3054b1a9d95a69dbf32d91c539b9bb475bf7e8db" - integrity sha512-4F4QkUMa3VFcmbP2gYOQ+h4x23EFa13BcWEZepQ47q9HbW/sL3zLmS7k44EpyhLMSEX4HrPHXiUlGTr9rK038g== +"@web3modal/scaffold-react@4.1.3": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@web3modal/scaffold-react/-/scaffold-react-4.1.3.tgz#72b82927f1c6756bf83daa456187da090b549d65" + integrity sha512-zx/jVqY6MFjOZWj/X9RUpSaTZ5Kz+dmH6wuOGSdlyUSYvUV5hJgUGEKGjCr5AVBk67n67lHKFUy1nxS9GXvVdg== dependencies: - "@web3modal/scaffold" "3.5.0" + "@web3modal/scaffold" "4.1.3" -"@web3modal/scaffold-utils@3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@web3modal/scaffold-utils/-/scaffold-utils-3.5.0.tgz#c05d136538277899d4c6deb76993a5e4ade94767" - integrity sha512-RpNp5N+c8oaPyy48u9pOj65uuZDmMAluj2ZUWRoh3FqoN+gshpQsCbbPwtuTug4MsGc84i8RJorTEargzrKthg== +"@web3modal/scaffold-utils@4.1.3": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@web3modal/scaffold-utils/-/scaffold-utils-4.1.3.tgz#c751ce47babfef98d53d1c46aaae5b50a0d28537" + integrity sha512-1rNZkTjhpvVRPXm8RKz51E/T9Ujhjh8DPxT3oQFnTNlws98yU+RLDJYrR8rQoqJfmDK3E5dViLmxjIbrLKh/dw== dependencies: - "@web3modal/polyfills" "3.5.0" - "@web3modal/scaffold" "3.5.0" + "@web3modal/core" "4.1.3" + "@web3modal/polyfills" "4.1.3" + valtio "1.11.2" -"@web3modal/scaffold-vue@3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@web3modal/scaffold-vue/-/scaffold-vue-3.5.0.tgz#04222fd36a4421a860c9662f73accb3bd51b6f94" - integrity sha512-babm6SpuOeYxyTfdg9v603lYCrOFbFg1Y8KHTCAISu3Tk4GEnJ99wQVUE1ujX2CHoLU8GvAeyVKszJniKyPaJQ== +"@web3modal/scaffold-vue@4.1.3": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@web3modal/scaffold-vue/-/scaffold-vue-4.1.3.tgz#d8e827c8431526a86cc6fbb7430e3fb7d560b9b0" + integrity sha512-xHySyt2A01VYGtQ3qHKEyv5n+JGOnZ41/ZM8glK6yfe4nDNS0c9X/psPoeVwWvLhOYPKWCikkgGvB1TmoyjNiQ== dependencies: - "@web3modal/scaffold" "3.5.0" + "@web3modal/scaffold" "4.1.3" -"@web3modal/scaffold@3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@web3modal/scaffold/-/scaffold-3.5.0.tgz#808def7fe15c729e85a7f7ff59689eebd4ba5b52" - integrity sha512-wBiOeFdpANOiXqf2o0bi1cd+P427oBf5p3hb5nRJ4abBOPtjr6g0rEQgdLwtalBRt4K4qLAElOwem6toRVViig== +"@web3modal/scaffold@4.1.3": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@web3modal/scaffold/-/scaffold-4.1.3.tgz#2a51ad18d6522233af71b3c4e8994ac3d9905a14" + integrity sha512-daOCaa8h4AT9BGqbsBAAU0CQL/hFWt9Bs6CK9zjesvhwxK2qQZ2O/t8tU2c68Gsj6VRDim7CcWCKjNYAqtGNyQ== dependencies: - "@web3modal/core" "3.5.0" - "@web3modal/ui" "3.5.0" + "@web3modal/common" "4.1.3" + "@web3modal/core" "4.1.3" + "@web3modal/ui" "4.1.3" lit "3.1.0" + optionalDependencies: + "@web3modal/siwe" "4.1.3" -"@web3modal/siwe@*": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@web3modal/siwe/-/siwe-3.5.0.tgz#5f7fa6a7209c6ddddcd008ad47bb4dc3815d4a37" - integrity sha512-i9AvSH3uA93lu7z79d90aMaHx3Kn/EhblRJa2NOykADuAZX3UeXkX9KX7pkLVEYD1TkQN4mOHTFRQ6LeXq9EcA== +"@web3modal/siwe@4.1.3": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@web3modal/siwe/-/siwe-4.1.3.tgz#e00438a688617cf4a65f6ec62437b4e7178b7ec8" + integrity sha512-YjCFvoUyjjkTmRLyaDuhCaWpz9/gqQNZRIXwkDg/0AZY92WeVq0jJQJIWmFv6Ag16nakqo73vtHjUGfPC4bbFQ== dependencies: - "@web3modal/core" "3.5.0" - "@web3modal/scaffold-utils" "3.5.0" - optionalDependencies: - react ">=17" - react-dom ">=17" - vue ">=3" + "@web3modal/core" "4.1.3" + "@web3modal/scaffold-utils" "4.1.3" + lit "3.1.0" + valtio "1.11.2" -"@web3modal/ui@3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@web3modal/ui/-/ui-3.5.0.tgz#e925cf829d5b0904c5693094db1c3a95c4ee5c1a" - integrity sha512-1H8pHCxcMDPxx1jKYMycSqilVGKDnKEGqVUa+NBhu9YeebIqB9WB5wWhyROLADsX/XzKwXUqhQndHDpcLyMyFw== +"@web3modal/ui@4.1.3": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@web3modal/ui/-/ui-4.1.3.tgz#b7ca4a28afc8e8b2910e47d2501eafe92a5a1173" + integrity sha512-gZBkpq186iPC05jYBi58MXYH4b3qxo+LIYBdXz++qvaZRiSV894hvHpVLTjJgJivUiP0Lzt/I+u63/G4dkwSgA== dependencies: lit "3.1.0" qrcode "1.5.3" -"@web3modal/wagmi@3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@web3modal/wagmi/-/wagmi-3.5.0.tgz#c0f6e92ff4c5e16febac74eddc180333bb039a98" - integrity sha512-bQoytNkcVbjWfZsePKAv+XF9KDtDx1iqitoC4RPold9oB5RukEgGNHFQ7AcDKTwjkmiE3JqwyubQyvupZ2Z/Yg== - dependencies: - "@web3modal/polyfills" "3.5.0" - "@web3modal/scaffold" "3.5.0" - "@web3modal/scaffold-react" "3.5.0" - "@web3modal/scaffold-utils" "3.5.0" - "@web3modal/scaffold-vue" "3.5.0" - "@web3modal/wallet" "3.5.0" +"@web3modal/wagmi@4.1.3": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@web3modal/wagmi/-/wagmi-4.1.3.tgz#fe3f7dacdf2057ef33ee0272452c533edbcf2567" + integrity sha512-YFYYFmp2HoXIo7mfL/eWc//F9cqIyHdKgUZmSYArDWxNze/MOuCrShnZWarXa1fVl7kY3Cq+bZpExM8TcttQCw== + dependencies: + "@web3modal/polyfills" "4.1.3" + "@web3modal/scaffold" "4.1.3" + "@web3modal/scaffold-react" "4.1.3" + "@web3modal/scaffold-utils" "4.1.3" + "@web3modal/scaffold-vue" "4.1.3" optionalDependencies: - "@web3modal/siwe" "*" + "@web3modal/siwe" "4.1.3" react ">=17" react-dom ">=17" vue ">=3" -"@web3modal/wallet@3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@web3modal/wallet/-/wallet-3.5.0.tgz#1123e84d6dad20957de2223eeab5bb70a9d27fc4" - integrity sha512-uNAOZS94Dfzp1ha3X1MztFS4R6XuOZ/Nfe0oiSvLAbeXb2pbV/JDJR1g0LjYiNU/XsDWYEW9WYeRUyg9ZLdUdg== +"@web3modal/wallet@4.1.3": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@web3modal/wallet/-/wallet-4.1.3.tgz#b38a1e96ff3f87b555075a2581154152de7ac41f" + integrity sha512-wcfljBc2c+0VF5d1hfVWRDrnRdlNGGTPz0iky6tjQW57sam9Yg5IKAbrEqyDZZs0zrAz8K03yHgKnP1BbQHc5A== dependencies: zod "3.22.4" @@ -7628,14 +7204,6 @@ resolved "https://registry.yarnpkg.com/@zag-js/focus-visible/-/focus-visible-0.2.2.tgz#56233480ca1275d3218fb2e10696a33d1a6b9e64" integrity sha512-0j2gZq8HiZ51z4zNnSkF1iSkqlwRDvdH+son3wHdoz+7IUdMN/5Exd4TxMJ+gq2Of1DiXReYLL9qqh2PdQ4wgA== -JSONStream@^1.3.5: - version "1.3.5" - resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" - integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== - dependencies: - jsonparse "^1.2.0" - through ">=2.2.7 <3" - abab@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" @@ -7646,16 +7214,16 @@ abitype@0.8.11: resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.8.11.tgz#66e1cf2cbf46f48d0e57132d7c1c392447536cc1" integrity sha512-bM4v2dKvX08sZ9IU38IN5BKmN+ZkOSd2oI4a9f0ejHYZQYV6cDr7j+d95ga0z2XHG36Y4jzoG5Z7qDqxp7fi/A== -abitype@0.8.7: - version "0.8.7" - resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.8.7.tgz#e4b3f051febd08111f486c0cc6a98fa72d033622" - integrity sha512-wQ7hV8Yg/yKmGyFpqrNZufCxbszDe5es4AZGYPBitocfSqXtjrTG9JMWFcc4N30ukl2ve48aBTwt7NJxVQdU3w== - abitype@0.9.8: version "0.9.8" resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.9.8.tgz#1f120b6b717459deafd213dfbf3a3dd1bf10ae8c" integrity sha512-puLifILdm+8sjyss4S+fsUN09obiT1g2YW6CtcQF+QDzxR0euzgEB29MZujC6zMk2a6SVmtttq1fc6+YFA7WYQ== +abitype@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.0.tgz#237176dace81d90d018bebf3a45cb42f2a2d9e97" + integrity sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ== + abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -7706,15 +7274,10 @@ acorn@^8.8.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== -aes-js@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" - integrity sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw== - -aes-js@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.1.2.tgz#db9aabde85d5caabbfc0d4f2a4446960f627146a" - integrity sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ== +acorn@^8.9.0: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== agent-base@6: version "6.0.2" @@ -7723,15 +7286,6 @@ agent-base@6: dependencies: debug "4" -agentkeepalive@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.1.tgz#a7975cbb9f83b367f06c90cc51ff28fe7d499717" - integrity sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA== - dependencies: - debug "^4.1.0" - depd "^1.1.2" - humanize-ms "^1.2.1" - aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -7740,7 +7294,7 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" -ajv@^6.10.0, ajv@^6.12.4: +ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -7851,14 +7405,6 @@ aria-hidden@^1.2.2: dependencies: tslib "^2.0.0" -aria-query@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" - integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA== - dependencies: - "@babel/runtime" "^7.10.2" - "@babel/runtime-corejs3" "^7.10.2" - aria-query@^5.0.0: version "5.1.3" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" @@ -7866,7 +7412,22 @@ aria-query@^5.0.0: dependencies: deep-equal "^2.0.5" -array-includes@^3.1.4, array-includes@^3.1.5: +aria-query@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" + integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== + dependencies: + dequal "^2.0.3" + +array-buffer-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" + integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== + dependencies: + call-bind "^1.0.5" + is-array-buffer "^3.0.4" + +array-includes@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.5.tgz#2c320010db8d31031fd2a5f6b3bbd4b1aad31bdb" integrity sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ== @@ -7888,29 +7449,55 @@ array-includes@^3.1.6: get-intrinsic "^1.1.3" is-string "^1.0.7" +array-includes@^3.1.7: + version "3.1.8" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" + integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" + is-string "^1.0.7" + array-union@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -array.prototype.flat@^1.2.5: - version "1.3.0" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz#0b0c1567bf57b38b56b4c97b8aa72ab45e4adc7b" - integrity sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw== +array.prototype.findlast@^1.2.4: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" + integrity sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.2" - es-shim-unscopables "^1.0.0" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-shim-unscopables "^1.0.2" -array.prototype.flatmap@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.0.tgz#a7e8ed4225f4788a70cd910abcf0791e76a5534f" - integrity sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg== +array.prototype.findlastindex@^1.2.3: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz#8c35a755c72908719453f87145ca011e39334d0d" + integrity sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-shim-unscopables "^1.0.2" + +array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" + integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== dependencies: call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" es-shim-unscopables "^1.0.0" array.prototype.flatmap@^1.3.1: @@ -7923,6 +7510,26 @@ array.prototype.flatmap@^1.3.1: es-abstract "^1.20.4" es-shim-unscopables "^1.0.0" +array.prototype.flatmap@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" + integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.toreversed@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz#b989a6bf35c4c5051e1dc0325151bf8088954eba" + integrity sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + array.prototype.tosorted@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz#ccf44738aa2b5ac56578ffda97c03fd3e23dd532" @@ -7934,10 +7541,35 @@ array.prototype.tosorted@^1.1.1: es-shim-unscopables "^1.0.0" get-intrinsic "^1.1.3" -ast-types-flow@^0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" - integrity sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag== +array.prototype.tosorted@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz#c8c89348337e51b8a3c48a9227f9ce93ceedcba8" + integrity sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg== + dependencies: + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.1.0" + es-shim-unscopables "^1.0.2" + +arraybuffer.prototype.slice@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" + integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.2.1" + get-intrinsic "^1.2.3" + is-array-buffer "^3.0.4" + is-shared-array-buffer "^1.0.2" + +ast-types-flow@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" + integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== astral-regex@^2.0.0: version "2.0.0" @@ -7978,10 +7610,17 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== -axe-core@^4.4.3: - version "4.4.3" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.3.tgz#11c74d23d5013c0fa5d183796729bc3482bd2f6f" - integrity sha512-32+ub6kkdhhWick/UjvEwRchgoetXqTK14INLqbGm5U2TzBkBNF3nQtLYm8ovxSkQWArjEQvftCKryjZaATu3w== +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + +axe-core@=4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf" + integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== axios@^1.4.0: version "1.6.2" @@ -7992,10 +7631,21 @@ axios@^1.4.0: form-data "^4.0.0" proxy-from-env "^1.1.0" -axobject-query@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" - integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA== +axios@^1.6.2: + version "1.6.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.7.tgz#7b48c2e27c96f9c68a2f8f31e2ab19f59b06b0a7" + integrity sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA== + dependencies: + follow-redirects "^1.15.4" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +axobject-query@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" + integrity sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg== + dependencies: + dequal "^2.0.3" babel-jest@^29.3.1: version "29.3.1" @@ -8095,30 +7745,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base-x@^3.0.2: - version "3.0.9" - resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.9.tgz#6349aaabb58526332de9f60995e548a53fe21320" - integrity sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ== - dependencies: - safe-buffer "^5.0.1" - base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -bech32@1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" - integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== - -bigint-buffer@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/bigint-buffer/-/bigint-buffer-1.1.5.tgz#d038f31c8e4534c1f8d0015209bf34b4fa6dd442" - integrity sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA== - dependencies: - bindings "^1.3.0" - bignumber.js@^9.0.0: version "9.1.2" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" @@ -8134,17 +7765,10 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -bind-decorator@^1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/bind-decorator/-/bind-decorator-1.0.11.tgz#e41bc06a1f65dd9cec476c91c5daf3978488252f" - integrity sha512-yzkH0uog6Vv/vQ9+rhSKxecnqGUZHYncg7qS7voz3Q76+TAi1SGiOKk2mlOvusQnFz9Dc4BC/NMkeXu11YgjJg== - -bindings@^1.3.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" +bintrees@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.2.tgz#49f896d6e858a4a499df85c38fb399b9aff840f8" + integrity sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw== bl@^4.0.3: version "4.1.0" @@ -8165,7 +7789,7 @@ bn.js@^4.11.9: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== -bn.js@^5.0.0, bn.js@^5.1.1, bn.js@^5.2.0, bn.js@^5.2.1: +bn.js@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== @@ -8175,14 +7799,10 @@ boolbase@^1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== -borsh@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/borsh/-/borsh-0.7.0.tgz#6e9560d719d86d90dc589bca60ffc8a6c51fec2a" - integrity sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA== - dependencies: - bn.js "^5.2.0" - bs58 "^4.0.0" - text-encoding-utf-8 "^1.0.2" +bowser@^2.9.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" + integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA== brace-expansion@^1.1.7: version "1.1.11" @@ -8228,13 +7848,6 @@ bs-logger@0.x: dependencies: fast-json-stable-stringify "2.x" -bs58@^4.0.0, bs58@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" - integrity sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw== - dependencies: - base-x "^3.0.2" - bser@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" @@ -8247,15 +7860,7 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer@6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.1.tgz#3cbea8c1463e5a0779e30b66d4c88c6ffa182ac2" - integrity sha512-rVAXBwEcEoYtxnHSO5iWyhzV/O1WMtkUYWlfdLS7FjU4PnSJJHEfHXi/uHPI5EwltmOA794gN3bm3/pzuctWjQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" - -buffer@6.0.3, buffer@^6.0.3, buffer@~6.0.3: +buffer@6.0.3, buffer@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== @@ -8271,10 +7876,10 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" -bufferutil@^4.0.1: - version "4.0.7" - resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.7.tgz#60c0d19ba2c992dd8273d3f73772ffc894c153ad" - integrity sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw== +bufferutil@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.8.tgz#1de6a71092d65d7766c4d8a522b261a6e787e8ea" + integrity sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw== dependencies: node-gyp-build "^4.3.0" @@ -8303,6 +7908,17 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" +call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -8323,10 +7939,10 @@ caniuse-lite@^1.0.30001400: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz#5f459215192a024c99e3e3a53aac310fc7cf24e6" integrity sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg== -caniuse-lite@^1.0.30001406: - version "1.0.30001480" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001480.tgz#9bbd35ee44c2480a1e3a3b9f4496f5066817164a" - integrity sha512-q7cpoPPvZYgtyC4VaBSN0Bt+PJ4c4EYRf0DrduInOz2SkFpHD5p3LnvEpqBp7UnJn+8x1Ogl1s38saUxe+ihQQ== +caniuse-lite@^1.0.30001579: + version "1.0.30001614" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001614.tgz#f894b4209376a0bf923d67d9c361d96b1dfebe39" + integrity sha512-jmZQ1VpmlRwHgdP1/uiKzgiAuGOfLEJsYFP4+GBou/QQ4U6IOJCB4NP1c+1p9RGLpwObcT94jA5/uO+F1vBbog== chakra-react-select@^4.4.3: version "4.4.3" @@ -8484,7 +8100,7 @@ cliui@^8.0.1: strip-ansi "^6.0.1" wrap-ansi "^7.0.0" -clsx@^1.1.0: +clsx@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== @@ -8592,6 +8208,11 @@ comment-parser@^1.1.2: resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.3.1.tgz#3d7ea3adaf9345594aedee6563f422348f165c1b" integrity sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA== +common-tags@1.8.2: + version "1.8.2" + resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" + integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== + compute-scroll-into-view@1.0.20: version "1.0.20" resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz#1768b5522d1172754f5d0c9b02de3af6be506a43" @@ -8627,7 +8248,7 @@ cookie@~0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== -copy-to-clipboard@3.3.3, copy-to-clipboard@^3.2.0, copy-to-clipboard@^3.3.1, copy-to-clipboard@^3.3.3: +copy-to-clipboard@3.3.3, copy-to-clipboard@^3.2.0, copy-to-clipboard@^3.3.1: version "3.3.3" resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0" integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA== @@ -8691,7 +8312,14 @@ cross-fetch@^3.1.4, cross-fetch@^3.1.5: dependencies: node-fetch "2.6.7" -cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-fetch@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983" + integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g== + dependencies: + node-fetch "^2.6.12" + +cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -9087,14 +8715,14 @@ damerau-levenshtein@^1.0.8: resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== -dappscout-iframe@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/dappscout-iframe/-/dappscout-iframe-0.1.0.tgz#c971d8a8aa4fb3d64ca357700d3ac809dcdecb0d" - integrity sha512-nL65Uuv2+r2ujELyBtw+VL6PYkUtleCvKi2XaeMiroZz6tieeUw338PEOc6cZaIfwXk59rFwNR0FZki2HnOrIA== +dappscout-iframe@0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/dappscout-iframe/-/dappscout-iframe-0.2.1.tgz#b4718515ee4f00022af3912fac6ca1a321c156f9" + integrity sha512-EsiAAEk2I6hN+/E8o45WUn4BFd7aN8UvBwsIcOH79WOly0GOOHkPEO/puPkBCV0EcdxBsZIfssx3X0fSWVz5Bw== dependencies: - ethers "^5.7.2" react "^18.2.0" react-dom "^18.2.0" + viem "^1.20.3" data-uri-to-buffer@^4.0.0: version "4.0.0" @@ -9110,6 +8738,40 @@ data-urls@^3.0.2: whatwg-mimetype "^3.0.0" whatwg-url "^11.0.0" +data-view-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" + integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" + integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" + integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +date-fns@^2.29.3: + version "2.30.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" + integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== + dependencies: + "@babel/runtime" "^7.21.0" + dateformat@^4.6.3: version "4.6.3" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" @@ -9125,20 +8787,18 @@ dayjs@^1.11.5: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.5.tgz#00e8cc627f231f9499c19b38af49f56dc0ac5e93" integrity sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA== -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: +debounce@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" + integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== + +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" -debug@^2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -9156,7 +8816,7 @@ decimal.js@^10.4.1: resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.2.tgz#0341651d1d997d86065a2ce3a441fbd0d8e8b98e" integrity sha512-ic1yEvwT6GuvaYwBLLY6/aFFgjZdySKTE8en/fkU3QICTmRtgtSlFn0u0BXN06InZwtfCelR7j8LRiDI/02iGA== -decode-uri-component@^0.2.0, decode-uri-component@^0.2.2: +decode-uri-component@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== @@ -9214,6 +8874,15 @@ deepmerge@~4.3.0: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + define-lazy-prop@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" @@ -9227,6 +8896,15 @@ define-properties@^1.1.3, define-properties@^1.1.4: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +define-properties@^1.2.0, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + defu@^6.1.2, defu@^6.1.3: version "6.1.3" resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.3.tgz#6d7f56bc61668e844f9f593ace66fd67ef1205fd" @@ -9239,11 +8917,6 @@ delaunator@5: dependencies: robust-predicates "^3.0.0" -delay@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" - integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== - delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -9254,22 +8927,17 @@ denque@^2.1.0: resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== -depd@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== - -derive-valtio@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/derive-valtio/-/derive-valtio-0.1.0.tgz#4b9fb393dfefccfef15fcbbddd745dd22d5d63d7" - integrity sha512-OCg2UsLbXK7GmmpzMXhYkdO64vhJ1ROUUGaTFyHjVwEdMEcTTRj7W1TxLbSBxdY8QLBPCcp66MTyaSy0RpO17A== +dequal@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== destr@^2.0.1, destr@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.2.tgz#8d3c0ee4ec0a76df54bc8b819bca215592a8c218" integrity sha512-65AlobnZMiCET00KaFFjUefxDX0khFA/E4myqZ7a6Sq1yZtR8+FVIvilVX66vF2uobSumxooYZChiRPCKNqhmg== -detect-browser@5.3.0, detect-browser@^5.2.0, detect-browser@^5.3.0: +detect-browser@5.3.0, detect-browser@^5.2.0: version "5.3.0" resolved "https://registry.yarnpkg.com/detect-browser/-/detect-browser-5.3.0.tgz#9705ef2bddf46072d0f7265a1fe300e36fe7ceca" integrity sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w== @@ -9465,15 +9133,24 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +eciesjs@^0.3.15, eciesjs@^0.3.16: + version "0.3.18" + resolved "https://registry.yarnpkg.com/eciesjs/-/eciesjs-0.3.18.tgz#67b5d73a8466e40a45bbc2f2a3177e71e9c0643d" + integrity sha512-RQhegEtLSyIiGJmFTZfvCTHER/fymipXFVx6OwSRYD6hOuy+6Kjpk0dGvIfP9kxn/smBpxQy71uxpGO406ITCw== + dependencies: + "@types/secp256k1" "^4.0.4" + futoin-hkdf "^1.5.3" + secp256k1 "^5.0.0" + electron-to-chromium@^1.4.251: version "1.4.276" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.276.tgz#17837b19dafcc43aba885c4689358b298c19b520" integrity sha512-EpuHPqu8YhonqLBXHoU6hDJCD98FCe6KDoet3/gY1qsQ6usjJoHqBH2YIVs8FXaAtHwVL8Uqa/fsYao/vq9VWQ== -elliptic@6.5.4: - version "6.5.4" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" - integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== +elliptic@^6.5.4: + version "6.5.5" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.5.tgz#c715e09f78b6923977610d4c2346d6ce22e6dded" + integrity sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw== dependencies: bn.js "^4.11.9" brorand "^1.1.0" @@ -9503,13 +9180,29 @@ encode-utf8@^1.0.3: resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda" integrity sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw== -end-of-stream@^1.1.0, end-of-stream@^1.4.1, end-of-stream@^1.4.4: +end-of-stream@^1.1.0, end-of-stream@^1.4.0, end-of-stream@^1.4.1, end-of-stream@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== dependencies: once "^1.4.0" +engine.io-client@~6.5.2: + version "6.5.3" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.5.3.tgz#4cf6fa24845029b238f83c628916d9149c399bc5" + integrity sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.11.0" + xmlhttprequest-ssl "~2.0.0" + +engine.io-parser@~5.2.1: + version "5.2.2" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.2.tgz#37b48e2d23116919a3453738c5720455e64e1c49" + integrity sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw== + enhanced-resolve@^5.12.0: version "5.12.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634" @@ -9560,7 +9253,7 @@ error@^7.0.0: dependencies: string-template "~0.2.1" -es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5: +es-abstract@^1.19.0, es-abstract@^1.19.5: version "1.20.4" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.4.tgz#1d103f9f8d78d4cf0713edcd6d0ed1a46eed5861" integrity sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA== @@ -9629,6 +9322,70 @@ es-abstract@^1.20.4: unbox-primitive "^1.0.2" which-typed-array "^1.1.9" +es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: + version "1.23.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" + integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== + dependencies: + array-buffer-byte-length "^1.0.1" + arraybuffer.prototype.slice "^1.0.3" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + data-view-buffer "^1.0.1" + data-view-byte-length "^1.0.1" + data-view-byte-offset "^1.0.0" + es-define-property "^1.0.0" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-set-tostringtag "^2.0.3" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.4" + get-symbol-description "^1.0.2" + globalthis "^1.0.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + hasown "^2.0.2" + internal-slot "^1.0.7" + is-array-buffer "^3.0.4" + is-callable "^1.2.7" + is-data-view "^1.0.1" + is-negative-zero "^2.0.3" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.3" + is-string "^1.0.7" + is-typed-array "^1.1.13" + is-weakref "^1.0.2" + object-inspect "^1.13.1" + object-keys "^1.1.1" + object.assign "^4.1.5" + regexp.prototype.flags "^1.5.2" + safe-array-concat "^1.1.2" + safe-regex-test "^1.0.3" + string.prototype.trim "^1.2.9" + string.prototype.trimend "^1.0.8" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.2" + typed-array-byte-length "^1.0.1" + typed-array-byte-offset "^1.0.2" + typed-array-length "^1.0.6" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.15" + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.1.0, es-errors@^1.2.1, es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + es-get-iterator@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.2.tgz#9234c54aba713486d7ebde0220864af5e2b283f7" @@ -9643,6 +9400,33 @@ es-get-iterator@^1.1.2: is-string "^1.0.5" isarray "^2.0.5" +es-iterator-helpers@^1.0.15, es-iterator-helpers@^1.0.17: + version "1.0.19" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz#117003d0e5fec237b4b5c08aded722e0c6d50ca8" + integrity sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.3" + es-errors "^1.3.0" + es-set-tostringtag "^2.0.3" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + globalthis "^1.0.3" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + internal-slot "^1.0.7" + iterator.prototype "^1.1.2" + safe-array-concat "^1.1.2" + +es-object-atoms@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" + integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== + dependencies: + es-errors "^1.3.0" + es-set-tostringtag@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" @@ -9652,6 +9436,15 @@ es-set-tostringtag@^2.0.1: has "^1.0.3" has-tostringtag "^1.0.0" +es-set-tostringtag@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" + integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== + dependencies: + get-intrinsic "^1.2.4" + has-tostringtag "^1.0.2" + hasown "^2.0.1" + es-shim-unscopables@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" @@ -9659,6 +9452,13 @@ es-shim-unscopables@^1.0.0: dependencies: has "^1.0.3" +es-shim-unscopables@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" + integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== + dependencies: + hasown "^2.0.0" + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -9668,18 +9468,6 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -es6-promise@^4.0.3: - version "4.2.8" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" - integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== - -es6-promisify@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" - integrity sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ== - dependencies: - es6-promise "^4.0.3" - esbuild@^0.18.10: version "0.18.20" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.18.20.tgz#4709f5a34801b43b799ab7d6d82f7284a9b7a7a6" @@ -9713,16 +9501,16 @@ escalade@^3.1.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escape-string-regexp@2.0.0, escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== -escape-string-regexp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" - integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== - escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" @@ -9763,6 +9551,15 @@ eslint-import-resolver-node@^0.3.6: debug "^3.2.7" resolve "^1.20.0" +eslint-import-resolver-node@^0.3.9: + version "0.3.9" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" + integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== + dependencies: + debug "^3.2.7" + is-core-module "^2.13.0" + resolve "^1.22.4" + eslint-import-resolver-typescript@^3.5.2: version "3.5.5" resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.5.tgz#0a9034ae7ed94b254a360fbea89187b60ea7456d" @@ -9777,13 +9574,6 @@ eslint-import-resolver-typescript@^3.5.2: is-glob "^4.0.3" synckit "^0.8.5" -eslint-module-utils@^2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz#4f3e41116aaf13a20792261e61d3a2e7e0583974" - integrity sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA== - dependencies: - debug "^3.2.7" - eslint-module-utils@^2.7.4: version "2.8.0" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz#e439fee65fc33f6bba630ff621efc38ec0375c49" @@ -9791,6 +9581,13 @@ eslint-module-utils@^2.7.4: dependencies: debug "^3.2.7" +eslint-module-utils@^2.8.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz#52f2404300c3bd33deece9d7372fb337cc1d7c34" + integrity sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q== + dependencies: + debug "^3.2.7" + eslint-plugin-es5@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/eslint-plugin-es5/-/eslint-plugin-es5-1.5.0.tgz#aab19af3d4798f7924bba309bc4f87087280fbba" @@ -9802,23 +9599,27 @@ eslint-plugin-import-helpers@^1.2.1: integrity sha512-BSqLlCnyX4tWGlvPUTpBgUoaFiWxXSztpk9SozZVW4TZU1ygZuF0Lrfn9CO5xx1XT+PVAR9yroP9JPRyB4rAjQ== eslint-plugin-import@^2.26.0: - version "2.26.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz#f812dc47be4f2b72b478a021605a59fc6fe8b88b" - integrity sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA== - dependencies: - array-includes "^3.1.4" - array.prototype.flat "^1.2.5" - debug "^2.6.9" + version "2.29.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz#d45b37b5ef5901d639c15270d74d46d161150643" + integrity sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw== + dependencies: + array-includes "^3.1.7" + array.prototype.findlastindex "^1.2.3" + array.prototype.flat "^1.3.2" + array.prototype.flatmap "^1.3.2" + debug "^3.2.7" doctrine "^2.1.0" - eslint-import-resolver-node "^0.3.6" - eslint-module-utils "^2.7.3" - has "^1.0.3" - is-core-module "^2.8.1" + eslint-import-resolver-node "^0.3.9" + eslint-module-utils "^2.8.0" + hasown "^2.0.0" + is-core-module "^2.13.1" is-glob "^4.0.3" minimatch "^3.1.2" - object.values "^1.1.5" - resolve "^1.22.0" - tsconfig-paths "^3.14.1" + object.fromentries "^2.0.7" + object.groupby "^1.0.1" + object.values "^1.1.7" + semver "^6.3.1" + tsconfig-paths "^3.15.0" eslint-plugin-jest@^27.1.6: version "27.1.6" @@ -9828,23 +9629,26 @@ eslint-plugin-jest@^27.1.6: "@typescript-eslint/utils" "^5.10.0" eslint-plugin-jsx-a11y@^6.5.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.1.tgz#93736fc91b83fdc38cc8d115deedfc3091aef1ff" - integrity sha512-sXgFVNHiWffBq23uiS/JaP6eVR622DqwB4yTzKvGZGcPq6/yZ3WmOZfuBks/vHWo9GaFOqC2ZK4i6+C35knx7Q== - dependencies: - "@babel/runtime" "^7.18.9" - aria-query "^4.2.2" - array-includes "^3.1.5" - ast-types-flow "^0.0.7" - axe-core "^4.4.3" - axobject-query "^2.2.0" + version "6.8.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz#2fa9c701d44fcd722b7c771ec322432857fcbad2" + integrity sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA== + dependencies: + "@babel/runtime" "^7.23.2" + aria-query "^5.3.0" + array-includes "^3.1.7" + array.prototype.flatmap "^1.3.2" + ast-types-flow "^0.0.8" + axe-core "=4.7.0" + axobject-query "^3.2.1" damerau-levenshtein "^1.0.8" emoji-regex "^9.2.2" - has "^1.0.3" - jsx-ast-utils "^3.3.2" - language-tags "^1.0.5" + es-iterator-helpers "^1.0.15" + hasown "^2.0.0" + jsx-ast-utils "^3.3.5" + language-tags "^1.0.9" minimatch "^3.1.2" - semver "^6.3.0" + object.entries "^1.1.7" + object.fromentries "^2.0.7" eslint-plugin-no-cyrillic-string@^1.0.5: version "1.0.5" @@ -9859,9 +9663,9 @@ eslint-plugin-playwright@^0.11.2: integrity sha512-uRLRLk7uTzc8NE6t4wBU8dijQwHvC66R/h7xwdM779jsJjMUtSmeaB8ayRkkpfwi+UU5BEfwvDANwmE+ccMVDw== eslint-plugin-react-hooks@^4.5.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3" - integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== + version "4.6.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596" + integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== eslint-plugin-react@^7.21.0: version "7.32.2" @@ -9885,24 +9689,28 @@ eslint-plugin-react@^7.21.0: string.prototype.matchall "^4.0.8" eslint-plugin-react@^7.31.7: - version "7.31.10" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.31.10.tgz#6782c2c7fe91c09e715d536067644bbb9491419a" - integrity sha512-e4N/nc6AAlg4UKW/mXeYWd3R++qUano5/o+t+wnWxIf+bLsOaH3a4q74kX3nDjYym3VBN4HyO9nEn1GcAqgQOA== - dependencies: - array-includes "^3.1.5" - array.prototype.flatmap "^1.3.0" + version "7.34.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz#6806b70c97796f5bbfb235a5d3379ece5f4da997" + integrity sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw== + dependencies: + array-includes "^3.1.7" + array.prototype.findlast "^1.2.4" + array.prototype.flatmap "^1.3.2" + array.prototype.toreversed "^1.1.2" + array.prototype.tosorted "^1.1.3" doctrine "^2.1.0" + es-iterator-helpers "^1.0.17" estraverse "^5.3.0" jsx-ast-utils "^2.4.1 || ^3.0.0" minimatch "^3.1.2" - object.entries "^1.1.5" - object.fromentries "^2.0.5" - object.hasown "^1.1.1" - object.values "^1.1.5" + object.entries "^1.1.7" + object.fromentries "^2.0.7" + object.hasown "^1.1.3" + object.values "^1.1.7" prop-types "^15.8.1" - resolve "^2.0.0-next.3" - semver "^6.3.0" - string.prototype.matchall "^4.0.7" + resolve "^2.0.0-next.5" + semver "^6.3.1" + string.prototype.matchall "^4.0.10" eslint-plugin-regexp@^1.7.0: version "1.9.0" @@ -9926,10 +9734,10 @@ eslint-scope@^5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-scope@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" - integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" @@ -9951,74 +9759,73 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint-visitor-keys@^3.4.1: +eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== eslint@^8.32.0: - version "8.32.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.32.0.tgz#d9690056bb6f1a302bd991e7090f5b68fbaea861" - integrity sha512-nETVXpnthqKPFyuY2FNjz/bEd6nbosRgKbkgS/y1C7LJop96gYHWpiguLecMHQ2XCPxn77DS0P+68WzG6vkZSQ== + version "8.57.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" + integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== dependencies: - "@eslint/eslintrc" "^1.4.1" - "@humanwhocodes/config-array" "^0.11.8" + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.57.0" + "@humanwhocodes/config-array" "^0.11.14" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" - ajv "^6.10.0" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" chalk "^4.0.0" cross-spawn "^7.0.2" debug "^4.3.2" doctrine "^3.0.0" escape-string-regexp "^4.0.0" - eslint-scope "^7.1.1" - eslint-utils "^3.0.0" - eslint-visitor-keys "^3.3.0" - espree "^9.4.0" - esquery "^1.4.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" file-entry-cache "^6.0.1" find-up "^5.0.0" glob-parent "^6.0.2" globals "^13.19.0" - grapheme-splitter "^1.0.4" + graphemer "^1.4.0" ignore "^5.2.0" - import-fresh "^3.0.0" imurmurhash "^0.1.4" is-glob "^4.0.0" is-path-inside "^3.0.3" - js-sdsl "^4.1.4" js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" lodash.merge "^4.6.2" minimatch "^3.1.2" natural-compare "^1.4.0" - optionator "^0.9.1" - regexpp "^3.2.0" + optionator "^0.9.3" strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" text-table "^0.2.0" -espree@^9.4.0: - version "9.4.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.4.0.tgz#cd4bc3d6e9336c433265fc0aa016fc1aaf182f8a" - integrity sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw== +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== dependencies: - acorn "^8.8.0" + acorn "^8.9.0" acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.3.0" + eslint-visitor-keys "^3.4.1" esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" - integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== +esquery@^1.4.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" + integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== dependencies: estraverse "^5.1.0" @@ -10049,22 +9856,23 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -eth-block-tracker@6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/eth-block-tracker/-/eth-block-tracker-6.1.0.tgz#0481f97bbb88a100b9d45806fe7e37af741cbefc" - integrity sha512-K9SY8+/xMBi4M5HHTDdxnpEqEEGjbNpzHFqvxyjMZej8InV/B+CkFRKM6W+uvrFJ7m8Zd1E0qUkseU3vdIDFYQ== +eth-block-tracker@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/eth-block-tracker/-/eth-block-tracker-7.1.0.tgz#dfc16085c6817cc30caabba381deb8d204c1c766" + integrity sha512-8YdplnuE1IK4xfqpf4iU7oBxnOYAc35934o083G8ao+8WM8QQtt/mVlAY6yIAdY1eMeLqg4Z//PZjJGmWGPMRg== dependencies: - "@metamask/safe-event-emitter" "^2.0.0" - "@metamask/utils" "^3.0.1" + "@metamask/eth-json-rpc-provider" "^1.0.0" + "@metamask/safe-event-emitter" "^3.0.0" + "@metamask/utils" "^5.0.1" json-rpc-random-id "^1.0.1" pify "^3.0.0" -eth-json-rpc-filters@5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/eth-json-rpc-filters/-/eth-json-rpc-filters-5.1.0.tgz#f0c2aeaec2a45e2dc6ca1b9843d8e85447821427" - integrity sha512-fos+9xmoa1A2Ytsc9eYof17r81BjdJOUcGcgZn4K/tKdCCTb+a8ytEtwlu1op5qsXFDlgGmstTELFrDEc89qEQ== +eth-json-rpc-filters@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/eth-json-rpc-filters/-/eth-json-rpc-filters-6.0.1.tgz#0b3e370f017f5c6f58d3e7bd0756d8099ed85c56" + integrity sha512-ITJTvqoCw6OVMLs7pI8f4gG92n/St6x80ACtHodeS+IXmO0w+t1T5OOzfSt7KLSMLRkVUoexV7tztLgDxg+iig== dependencies: - "@metamask/safe-event-emitter" "^2.0.0" + "@metamask/safe-event-emitter" "^3.0.0" async-mutex "^0.2.6" eth-query "^2.1.2" json-rpc-engine "^6.1.0" @@ -10078,14 +9886,7 @@ eth-query@^2.1.2: json-rpc-random-id "^1.0.0" xtend "^4.0.1" -eth-rpc-errors@4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/eth-rpc-errors/-/eth-rpc-errors-4.0.2.tgz#11bc164e25237a679061ac05b7da7537b673d3b7" - integrity sha512-n+Re6Gu8XGyfFy1it0AwbD1x0MUzspQs0D5UiPs1fFPCr6WAwZM+vbIhXheBFrpgosqN9bs5PqlB4Q61U/QytQ== - dependencies: - fast-safe-stringify "^2.0.6" - -eth-rpc-errors@^4.0.2: +eth-rpc-errors@^4.0.2, eth-rpc-errors@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/eth-rpc-errors/-/eth-rpc-errors-4.0.3.tgz#6ddb6190a4bf360afda82790bb7d9d5e724f423a" integrity sha512-Z3ymjopaoft7JDoxZcEb3pwdGh7yiYMhOwm2doUt6ASXlMavpNlK6Cre0+IMl2VSGyEU9rkiperQhp5iRxn5Pg== @@ -10102,51 +9903,20 @@ ethereum-cryptography@^2.0.0: "@scure/bip32" "1.3.1" "@scure/bip39" "1.2.1" -ethers@^5.7.2: - version "5.7.2" - resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" - integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== - dependencies: - "@ethersproject/abi" "5.7.0" - "@ethersproject/abstract-provider" "5.7.0" - "@ethersproject/abstract-signer" "5.7.0" - "@ethersproject/address" "5.7.0" - "@ethersproject/base64" "5.7.0" - "@ethersproject/basex" "5.7.0" - "@ethersproject/bignumber" "5.7.0" - "@ethersproject/bytes" "5.7.0" - "@ethersproject/constants" "5.7.0" - "@ethersproject/contracts" "5.7.0" - "@ethersproject/hash" "5.7.0" - "@ethersproject/hdnode" "5.7.0" - "@ethersproject/json-wallets" "5.7.0" - "@ethersproject/keccak256" "5.7.0" - "@ethersproject/logger" "5.7.0" - "@ethersproject/networks" "5.7.1" - "@ethersproject/pbkdf2" "5.7.0" - "@ethersproject/properties" "5.7.0" - "@ethersproject/providers" "5.7.2" - "@ethersproject/random" "5.7.0" - "@ethersproject/rlp" "5.7.0" - "@ethersproject/sha2" "5.7.0" - "@ethersproject/signing-key" "5.7.0" - "@ethersproject/solidity" "5.7.0" - "@ethersproject/strings" "5.7.0" - "@ethersproject/transactions" "5.7.0" - "@ethersproject/units" "5.7.0" - "@ethersproject/wallet" "5.7.0" - "@ethersproject/web" "5.7.1" - "@ethersproject/wordlists" "5.7.0" - event-target-shim@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== -eventemitter3@^4.0.7: - version "4.0.7" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" - integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== +eventemitter2@^6.4.5, eventemitter2@^6.4.7: + version "6.4.9" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.9.tgz#41f2750781b4230ed58827bc119d293471ecb125" + integrity sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg== + +eventemitter3@5.0.1, eventemitter3@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== events@^3.3.0: version "3.3.0" @@ -10216,11 +9986,6 @@ extension-port-stream@^2.0.1: dependencies: webextension-polyfill-ts "^0.22.0" -eyes@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" - integrity sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ== - fast-copy@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.0.tgz#875ebf33b13948ae012b6e51d33da5e6e7571ab8" @@ -10272,11 +10037,6 @@ fast-safe-stringify@^2.0.6, fast-safe-stringify@^2.1.1: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== -fast-stable-stringify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz#5c5543462b22aeeefd36d05b34e51c78cb86d313" - integrity sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag== - fast-url-parser@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d" @@ -10320,11 +10080,6 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -10366,17 +10121,18 @@ find-yarn-workspace-root@^2.0.0: micromatch "^4.0.2" flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== dependencies: - flatted "^3.1.0" + flatted "^3.2.9" + keyv "^4.5.3" rimraf "^3.0.2" -flatted@^3.1.0: - version "3.2.7" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" - integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== +flatted@^3.2.9: + version "3.3.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" + integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== focus-lock@^0.11.6: version "0.11.6" @@ -10395,6 +10151,11 @@ follow-redirects@^1.15.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== +follow-redirects@^1.15.4: + version "1.15.5" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" + integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== + fontfaceobserver@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fontfaceobserver/-/fontfaceobserver-2.1.0.tgz#e2705d293e2c585a6531c2a722905657317a2991" @@ -10509,11 +10270,26 @@ function.prototype.name@^1.1.5: es-abstract "^1.19.0" functions-have-names "^1.2.2" -functions-have-names@^1.2.2: +function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" + +functions-have-names@^1.2.2, functions-have-names@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== +futoin-hkdf@^1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/futoin-hkdf/-/futoin-hkdf-1.5.3.tgz#6c8024f2e1429da086d4e18289ef2239ad33ee35" + integrity sha512-SewY5KdMpaoCeh7jachEWFsh1nNlaDjNHZXWqL5IGwtpEYHTgkr2+AMCgNwKWkcc0wpSYrZfR7he4WdmHFtDxQ== + gaxios@^5.0.0: version "5.1.3" resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-5.1.3.tgz#f7fa92da0fe197c846441e5ead2573d4979e9013" @@ -10532,6 +10308,13 @@ gcp-metadata@^5.0.0: gaxios "^5.0.0" json-bigint "^1.0.0" +generic-names@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-4.0.0.tgz#0bd8a2fd23fe8ea16cbd0a279acd69c06933d9a3" + integrity sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A== + dependencies: + loader-utils "^3.2.0" + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -10560,6 +10343,17 @@ get-intrinsic@^1.1.0, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: has "^1.0.3" has-symbols "^1.0.3" +get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + get-nonce@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" @@ -10588,11 +10382,29 @@ get-symbol-description@^1.0.0: call-bind "^1.0.2" get-intrinsic "^1.1.1" +get-symbol-description@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" + integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== + dependencies: + call-bind "^1.0.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + get-tsconfig@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.5.0.tgz#6d52d1c7b299bd3ee9cd7638561653399ac77b0f" integrity sha512-MjhiaIWCJ1sAU4pIQ5i5OfOuHHxVo1oYeNsWTON7jxYkod8pHocXeh+SSbmu5OZZZK73B6cbJ2XADzXehLyovQ== +getit-sdk@^1.0.4: + version "1.0.13" + resolved "https://registry.yarnpkg.com/getit-sdk/-/getit-sdk-1.0.13.tgz#702cd2e8dfeb42884deed6d8d7fbac9af413c73a" + integrity sha512-gWDJZNMoy7Uw9S3qOvQ+gVFdmrHuI17ePGhJYbP25DGOzgCwD5n7KDy9JWxGpLqEnf+V3v3aoyTvP8TtZheEaA== + dependencies: + axios "^1.6.2" + jest-transform-css "^6.0.1" + tslib "^2.6.2" + github-from-package@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" @@ -10617,11 +10429,6 @@ glob-regex@^0.3.0: resolved "https://registry.yarnpkg.com/glob-regex/-/glob-regex-0.3.2.tgz#27348f2f60648ec32a4a53137090b9fb934f3425" integrity sha512-m5blUd3/OqDTWwzBBtWBPrGlAzatRywHameHeekAZyZrskYouOGdNB8T/q6JucucvJXtOuyHIn0/Yia7iDasDw== -glob-to-regexp@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" - integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== - glob@7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" @@ -10686,9 +10493,9 @@ globals@^11.1.0: integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^13.19.0: - version "13.19.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.19.0.tgz#7a42de8e6ad4f7242fbcca27ea5b23aca367b5c8" - integrity sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ== + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== dependencies: type-fest "^0.20.2" @@ -10739,7 +10546,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: +graceful-fs@^4.1.11, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -10763,6 +10570,11 @@ grapheme-splitter@^1.0.4: resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + graphiql@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/graphiql/-/graphiql-2.2.0.tgz#406b9b28bca130ae9ece892826c1d60958c1ac13" @@ -10835,11 +10647,23 @@ has-property-descriptors@^1.0.0: dependencies: get-intrinsic "^1.1.1" +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + has-proto@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== +has-proto@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" @@ -10852,6 +10676,13 @@ has-tostringtag@^1.0.0: dependencies: has-symbols "^1.0.2" +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + has@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" @@ -10859,7 +10690,7 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" -hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: +hash.js@^1.0.0, hash.js@^1.0.3: version "1.1.7" resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== @@ -10874,6 +10705,13 @@ hasown@^2.0.0: dependencies: function-bind "^1.1.2" +hasown@^2.0.1, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + hast-util-parse-selector@^2.0.0: version "2.2.5" resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz#d57c23f4da16ae3c63b3b6ca4616683313499c3a" @@ -10956,11 +10794,18 @@ html-encoding-sniffer@^3.0.0: dependencies: whatwg-encoding "^2.0.0" -html-escaper@^2.0.0: +html-escaper@^2.0.0, html-escaper@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +html-parse-stringify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" + integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== + dependencies: + void-elements "3.1.0" + http-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" @@ -10993,18 +10838,25 @@ human-signals@^3.0.1: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-3.0.1.tgz#c740920859dafa50e5a3222da9d3bf4bb0e5eef5" integrity sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ== -humanize-ms@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" - integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== - dependencies: - ms "^2.0.0" - husky@^8.0.0: version "8.0.1" resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.1.tgz#511cb3e57de3e3190514ae49ed50f6bc3f50b3e9" integrity sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw== +i18next-browser-languagedetector@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.0.tgz#de0321cba6881be37d82e20e4d6f05aa75f6e37f" + integrity sha512-U00DbDtFIYD3wkWsr2aVGfXGAj2TgnELzOX9qv8bT0aJtvPV9CRO77h+vgmHFBMe7LAxdwvT/7VkCWGya6L3tA== + dependencies: + "@babel/runtime" "^7.23.2" + +i18next@22.5.1: + version "22.5.1" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-22.5.1.tgz#99df0b318741a506000c243429a7352e5f44d424" + integrity sha512-8TGPgM3pAD+VRsMtUMNknRz3kzqwp/gPALrWMsDnmC1mKqJwpWyooQRLMcbTwq8z8YwSmuj+ZYvc+xCuEpkssA== + dependencies: + "@babel/runtime" "^7.20.6" + iconv-lite@0.6, iconv-lite@0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" @@ -11012,6 +10864,11 @@ iconv-lite@0.6, iconv-lite@0.6.3: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +icss-replace-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" + integrity sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg== + icss-utils@^5.0.0, icss-utils@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" @@ -11037,7 +10894,7 @@ immutable@^3.x.x: resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" integrity sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg== -import-fresh@^3.0.0, import-fresh@^3.2.1: +import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -11081,7 +10938,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4: +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -11109,12 +10966,21 @@ internal-slot@^1.0.4: has "^1.0.3" side-channel "^1.0.4" +internal-slot@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" + integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.0" + side-channel "^1.0.4" + "internmap@1 - 2": version "2.0.3" resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== -invariant@^2.2.2, invariant@^2.2.4: +invariant@2.2.4, invariant@^2.2.2, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== @@ -11171,11 +11037,26 @@ is-array-buffer@^3.0.1: get-intrinsic "^1.1.3" is-typed-array "^1.1.10" +is-array-buffer@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" + integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== +is-async-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.0.0.tgz#8e4418efd3e5d3a6ebb0164c05ef5afb69aa9646" + integrity sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA== + dependencies: + has-tostringtag "^1.0.0" + is-bigint@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" @@ -11210,20 +11091,27 @@ is-core-module@^2.11.0: dependencies: has "^1.0.3" -is-core-module@^2.13.0: +is-core-module@^2.13.0, is-core-module@^2.13.1: version "2.13.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== dependencies: hasown "^2.0.0" -is-core-module@^2.8.1, is-core-module@^2.9.0: +is-core-module@^2.9.0: version "2.10.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed" integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg== dependencies: has "^1.0.3" +is-data-view@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" + integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== + dependencies: + is-typed-array "^1.1.13" + is-date-object@^1.0.1, is-date-object@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" @@ -11246,6 +11134,13 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== +is-finalizationregistry@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz#c8749b65f17c133313e661b1289b95ad3dbd62e6" + integrity sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== + dependencies: + call-bind "^1.0.2" + is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" @@ -11261,7 +11156,7 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== -is-generator-function@^1.0.7: +is-generator-function@^1.0.10, is-generator-function@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== @@ -11290,6 +11185,11 @@ is-negative-zero@^2.0.2: resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== + is-number-object@^1.0.4: version "1.0.7" resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" @@ -11307,6 +11207,11 @@ is-path-inside@^3.0.3: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -11349,6 +11254,13 @@ is-shared-array-buffer@^1.0.2: dependencies: call-bind "^1.0.2" +is-shared-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" + integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== + dependencies: + call-bind "^1.0.7" + is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" @@ -11384,10 +11296,12 @@ is-typed-array@^1.1.10, is-typed-array@^1.1.3, is-typed-array@^1.1.9: gopd "^1.0.1" has-tostringtag "^1.0.0" -is-typedarray@1.0.0, is-typedarray@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== +is-typed-array@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" + integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== + dependencies: + which-typed-array "^1.1.14" is-weakmap@^2.0.1: version "2.0.1" @@ -11436,16 +11350,19 @@ isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== +isomorphic-unfetch@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz#87341d5f4f7b63843d468438128cb087b7c3e98f" + integrity sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q== + dependencies: + node-fetch "^2.6.1" + unfetch "^4.2.0" + isomorphic-ws@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf" integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw== -isomorphic-ws@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" - integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== - isows@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.3.tgz#93c1cf0575daf56e7120bab5c8c448b0809d0d74" @@ -11493,6 +11410,17 @@ istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" +iterator.prototype@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" + integrity sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w== + dependencies: + define-properties "^1.2.1" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + reflect.getprototypeof "^1.0.4" + set-function-name "^2.0.1" + jackspeak@^2.3.5: version "2.3.6" resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" @@ -11513,25 +11441,6 @@ jaeger-client@^3.15.0: uuid "^8.3.2" xorshift "^1.1.1" -jayson@^3.4.4: - version "3.7.0" - resolved "https://registry.yarnpkg.com/jayson/-/jayson-3.7.0.tgz#b735b12d06d348639ae8230d7a1e2916cb078f25" - integrity sha512-tfy39KJMrrXJ+mFcMpxwBvFDetS8LAID93+rycFglIQM4kl3uNR3W4lBLE/FFhsoUCEox5Dt2adVpDm/XtebbQ== - dependencies: - "@types/connect" "^3.4.33" - "@types/node" "^12.12.54" - "@types/ws" "^7.4.4" - JSONStream "^1.3.5" - commander "^2.20.3" - delay "^5.0.0" - es6-promisify "^5.0.0" - eyes "^0.1.8" - isomorphic-ws "^4.0.1" - json-stringify-safe "^5.0.1" - lodash "^4.17.20" - uuid "^8.3.2" - ws "^7.4.5" - jest-changed-files@^29.2.0: version "29.2.0" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.2.0.tgz#b6598daa9803ea6a4dce7968e20ab380ddbee289" @@ -11857,6 +11766,17 @@ jest-snapshot@^29.3.1: pretty-format "^29.3.1" semver "^7.3.5" +jest-transform-css@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/jest-transform-css/-/jest-transform-css-6.0.1.tgz#95c579c98945734439c1f243b9843d5f477a9c60" + integrity sha512-i78Pi2MW6vcdsUFSRx1kPbjbEIO0pBWwh1Y+PcDrLwTv/6e5p7fzsV/gxFW/SYMHS8DUvMdRVTwVCkA/y+t0iQ== + dependencies: + common-tags "1.8.2" + cross-spawn "7.0.3" + postcss-load-config "4.0.1" + postcss-modules "4.3.1" + style-inject "0.3.0" + jest-util@^29.0.0, jest-util@^29.3.1: version "29.3.1" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.3.1.tgz#1dda51e378bbcb7e3bc9d8ab651445591ed373e1" @@ -11946,16 +11866,6 @@ js-file-download@^0.4.12: resolved "https://registry.yarnpkg.com/js-file-download/-/js-file-download-0.4.12.tgz#10c70ef362559a5b23cdbdc3bd6f399c3d91d821" integrity sha512-rML+NkoD08p5Dllpjo0ffy4jRHeY6Zsapvr/W86N7E0yuzAO6qa5X9+xog6zQNlH102J7IXljNY2FtS6Lj3ucg== -js-sdsl@^4.1.4: - version "4.2.0" - resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.2.0.tgz#278e98b7bea589b8baaf048c20aeb19eb7ad09d0" - integrity sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ== - -js-sha3@0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" - integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== - "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -12030,12 +11940,17 @@ json-bigint@^1.0.0: dependencies: bignumber.js "^9.0.0" +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + json-parse-even-better-errors@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== -json-rpc-engine@6.1.0, json-rpc-engine@^6.1.0: +json-rpc-engine@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/json-rpc-engine/-/json-rpc-engine-6.1.0.tgz#bf5ff7d029e1c1bf20cb6c0e9f348dcd8be5a393" integrity sha512-NEdLrtrq1jUZyfjkr9OCz9EzCNhnRyWtt1PAnvnhwy6e8XETS0Dtc+ZNCO2gvuAoKsIn2+vCSowXTYE4CkgnAQ== @@ -12073,12 +11988,7 @@ json-stable-stringify@^1.0.2: dependencies: jsonify "^0.0.1" -json-stringify-safe@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== - -json5@^1.0.1: +json5@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== @@ -12114,12 +12024,7 @@ jsonify@^0.0.1: resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== -jsonparse@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" - integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== - -"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.2: +"jsx-ast-utils@^2.4.1 || ^3.0.0": version "3.3.3" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz#76b3e6e6cece5c69d49a5792c3d01bd1a0cdc7ea" integrity sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw== @@ -12127,15 +12032,32 @@ jsonparse@^1.2.0: array-includes "^3.1.5" object.assign "^4.1.3" -keccak@^3.0.1: - version "3.0.3" - resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.3.tgz#4bc35ad917be1ef54ff246f904c2bbbf9ac61276" - integrity sha512-JZrLIAJWuZxKbCilMpNz5Vj7Vtb4scDG3dMXLOsbzBmQGyjwE61BbW7bJkfKKCShXiQZt3T6sBgALRtmd+nZaQ== +jsx-ast-utils@^3.3.5: + version "3.3.5" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" + integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== + dependencies: + array-includes "^3.1.6" + array.prototype.flat "^1.3.1" + object.assign "^4.1.4" + object.values "^1.1.6" + +keccak@^3.0.3: + version "3.0.4" + resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.4.tgz#edc09b89e633c0549da444432ecf062ffadee86d" + integrity sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q== dependencies: node-addon-api "^2.0.0" node-gyp-build "^4.2.0" readable-stream "^3.6.0" +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + keyvaluestorage-interface@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/keyvaluestorage-interface/-/keyvaluestorage-interface-1.0.0.tgz#13ebdf71f5284ad54be94bd1ad9ed79adad515ff" @@ -12153,17 +12075,17 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -language-subtag-registry@~0.3.2: +language-subtag-registry@^0.3.20: version "0.3.22" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d" integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w== -language-tags@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.5.tgz#d321dbc4da30ba8bf3024e040fa5c14661f9193a" - integrity sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ== +language-tags@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.9.tgz#1ffdcd0ec0fafb4b1be7f8b11f306ad0f9c08777" + integrity sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA== dependencies: - language-subtag-registry "~0.3.2" + language-subtag-registry "^0.3.20" leven@^3.1.0: version "3.1.0" @@ -12191,6 +12113,11 @@ lilconfig@2.0.5: resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.5.tgz#19e57fd06ccc3848fd1891655b5a447092225b25" integrity sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg== +lilconfig@^2.0.5: + version "2.1.0" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" + integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -12316,6 +12243,11 @@ lit@3.1.0: lit-element "^4.0.0" lit-html "^3.1.0" +loader-utils@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.1.tgz#4fb104b599daafd82ef3e1a41fb9265f87e1f576" + integrity sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw== + locate-path@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" @@ -12375,7 +12307,7 @@ lodash.throttle@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ== -lodash@^4.0.0, lodash@^4.15.0, lodash@^4.17.20, lodash@^4.17.21: +lodash@^4.0.0, lodash@^4.15.0, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -12439,6 +12371,11 @@ lz-string@^1.5.0: resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== +magic-bytes.js@1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/magic-bytes.js/-/magic-bytes.js-1.8.0.tgz#8362793c60cd77c2dd77db6420be727192df68e2" + integrity sha512-lyWpfvNGVb5lu8YUAbER0+UMBTdR63w2mcSUlhhBTyVbxJvjgqwyAf3AZD6MprgK0uHuBoWXSDAMWLupX83o3Q== + magic-string@^0.30.5: version "0.30.5" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9" @@ -12501,6 +12438,13 @@ memoize-one@^6.0.0: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== +merge-options@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-3.0.4.tgz#84709c2aa2a4b24c1981f66c179fe5565cc6dbb7" + integrity sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ== + dependencies: + is-plain-obj "^2.1.0" + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -12626,6 +12570,13 @@ minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== +mipd@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/mipd/-/mipd-0.0.5.tgz#367ee796531c23f0631f129038700b1406663aec" + integrity sha512-gbKA784D2WKb5H/GtqEv+Ofd1S9Zj+Z/PGDIl1u1QAbswkxD28BQ5bSXQxkeBzPBABg1iDSbiwGG1XqlOxRspA== + dependencies: + viem "^1.1.4" + mixpanel-browser@^2.47.0: version "2.47.0" resolved "https://registry.yarnpkg.com/mixpanel-browser/-/mixpanel-browser-2.47.0.tgz#4e7fd3bb660c6f63443efbd169d1cd327db71ed4" @@ -12678,22 +12629,17 @@ mri@^1.2.0: resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== -mrmime@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27" - integrity sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw== - -ms@2.0.0: +mrmime@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-2.0.0.tgz#151082a6e06e59a9a39b46b3e14d5cfe92b3abb4" + integrity sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw== ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.0.0, ms@^2.1.1: +ms@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -12747,28 +12693,28 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -next@13.5.4: - version "13.5.4" - resolved "https://registry.yarnpkg.com/next/-/next-13.5.4.tgz#7e6a93c9c2b9a2c78bf6906a6c5cc73ae02d5b4d" - integrity sha512-+93un5S779gho8y9ASQhb/bTkQF17FNQOtXLKAj3lsNgltEcF0C5PMLLncDmH+8X1EnJH1kbqAERa29nRXqhjA== +next@14.2.3: + version "14.2.3" + resolved "https://registry.yarnpkg.com/next/-/next-14.2.3.tgz#f117dd5d5f20c307e7b8e4f9c1c97d961008925d" + integrity sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A== dependencies: - "@next/env" "13.5.4" - "@swc/helpers" "0.5.2" + "@next/env" "14.2.3" + "@swc/helpers" "0.5.5" busboy "1.6.0" - caniuse-lite "^1.0.30001406" + caniuse-lite "^1.0.30001579" + graceful-fs "^4.2.11" postcss "8.4.31" styled-jsx "5.1.1" - watchpack "2.4.0" optionalDependencies: - "@next/swc-darwin-arm64" "13.5.4" - "@next/swc-darwin-x64" "13.5.4" - "@next/swc-linux-arm64-gnu" "13.5.4" - "@next/swc-linux-arm64-musl" "13.5.4" - "@next/swc-linux-x64-gnu" "13.5.4" - "@next/swc-linux-x64-musl" "13.5.4" - "@next/swc-win32-arm64-msvc" "13.5.4" - "@next/swc-win32-ia32-msvc" "13.5.4" - "@next/swc-win32-x64-msvc" "13.5.4" + "@next/swc-darwin-arm64" "14.2.3" + "@next/swc-darwin-x64" "14.2.3" + "@next/swc-linux-arm64-gnu" "14.2.3" + "@next/swc-linux-arm64-musl" "14.2.3" + "@next/swc-linux-x64-gnu" "14.2.3" + "@next/swc-linux-x64-musl" "14.2.3" + "@next/swc-win32-arm64-msvc" "14.2.3" + "@next/swc-win32-ia32-msvc" "14.2.3" + "@next/swc-win32-x64-msvc" "14.2.3" nextjs-routes@^1.0.8: version "1.0.8" @@ -12794,6 +12740,11 @@ node-addon-api@^2.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== +node-addon-api@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762" + integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA== + node-addon-api@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.0.0.tgz#8136add2f510997b3b94814f4af1cce0b0e3962e" @@ -12817,27 +12768,27 @@ node-fetch-native@^1.4.0, node-fetch-native@^1.4.1: resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.4.1.tgz#5a336e55b4e1b1e72b9927da09fecd2b374c9be5" integrity sha512-NsXBU0UgBxo2rQLOeWNZqS3fvflWePMECr8CoSWoSTqCqGbVVsvl9vZu1HfQicYN0g5piV9Gh8RTEvo/uP752w== -node-fetch@2, node-fetch@2.6.7: +node-fetch@2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== dependencies: whatwg-url "^5.0.0" -node-fetch@^2.6.11: - version "2.6.11" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.11.tgz#cde7fc71deef3131ef80a738919f999e6edfff25" - integrity sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w== - dependencies: - whatwg-url "^5.0.0" - -node-fetch@^2.6.7, node-fetch@^2.6.9: +node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7, node-fetch@^2.6.9: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== dependencies: whatwg-url "^5.0.0" +node-fetch@^2.6.11: + version "2.6.11" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.11.tgz#cde7fc71deef3131ef80a738919f999e6edfff25" + integrity sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w== + dependencies: + whatwg-url "^5.0.0" + node-fetch@^3.2.9: version "3.2.10" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.10.tgz#e8347f94b54ae18b57c9c049ef641cef398a85c8" @@ -12911,6 +12862,15 @@ nwsapi@^2.2.2: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0" integrity sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw== +obj-multiplex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/obj-multiplex/-/obj-multiplex-1.0.0.tgz#2f2ae6bfd4ae11befe742ea9ea5b36636eabffc1" + integrity sha512-0GNJAOsHoBHeNTvl5Vt6IWnpUEcc3uSRxzBri7EDyIcMgYvnY2JL2qdeV5zTMjWQX5OHcD5amcW2HFfDh0gjIA== + dependencies: + end-of-stream "^1.4.0" + once "^1.4.0" + readable-stream "^2.3.3" + object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -12921,6 +12881,11 @@ object-inspect@^1.12.2: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== +object-inspect@^1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== + object-inspect@^1.9.0: version "1.12.3" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" @@ -12949,14 +12914,15 @@ object.assign@^4.1.3, object.assign@^4.1.4: has-symbols "^1.0.3" object-keys "^1.1.1" -object.entries@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861" - integrity sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g== +object.assign@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" + integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + has-symbols "^1.0.3" + object-keys "^1.1.1" object.entries@^1.1.6: version "1.1.6" @@ -12967,14 +12933,14 @@ object.entries@^1.1.6: define-properties "^1.1.4" es-abstract "^1.20.4" -object.fromentries@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.5.tgz#7b37b205109c21e741e605727fe8b0ad5fa08251" - integrity sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw== +object.entries@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" + integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" object.fromentries@^2.0.6: version "2.0.6" @@ -12985,13 +12951,24 @@ object.fromentries@^2.0.6: define-properties "^1.1.4" es-abstract "^1.20.4" -object.hasown@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.1.tgz#ad1eecc60d03f49460600430d97f23882cf592a3" - integrity sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A== +object.fromentries@^2.0.7: + version "2.0.8" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" + integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== dependencies: - define-properties "^1.1.4" - es-abstract "^1.19.5" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + +object.groupby@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e" + integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" object.hasown@^1.1.2: version "1.1.2" @@ -13001,14 +12978,14 @@ object.hasown@^1.1.2: define-properties "^1.1.4" es-abstract "^1.20.4" -object.values@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac" - integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg== +object.hasown@^1.1.3: + version "1.1.4" + resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.4.tgz#e270ae377e4c120cdcb7656ce66884a6218283dc" + integrity sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" object.values@^1.1.6: version "1.1.6" @@ -13019,6 +12996,15 @@ object.values@^1.1.6: define-properties "^1.1.4" es-abstract "^1.20.4" +object.values@^1.1.7: + version "1.2.0" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" + integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + obuf@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" @@ -13103,17 +13089,17 @@ optionator@^0.8.1: type-check "~0.3.2" word-wrap "~1.2.3" -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== dependencies: deep-is "^0.1.3" fast-levenshtein "^2.0.6" levn "^0.4.1" prelude-ls "^1.2.1" type-check "^0.4.0" - word-wrap "^1.2.3" + word-wrap "^1.2.5" os-tmpdir@~1.0.2: version "1.0.2" @@ -13470,6 +13456,11 @@ pngjs@^5.0.0: resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== +pony-cause@^2.1.10: + version "2.1.10" + resolved "https://registry.yarnpkg.com/pony-cause/-/pony-cause-2.1.10.tgz#828457ad6f13be401a075dbf14107a9057945174" + integrity sha512-3IKLNXclQgkU++2fSi93sQ6BznFuxSLB11HdvZQ6JW/spahf/P1pAHBQEahr20rs0htZW0UDkM1HmA+nZkXKsw== + popmotion@11.0.3: version "11.0.3" resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-11.0.3.tgz#565c5f6590bbcddab7a33a074bb2ba97e24b0cc9" @@ -13480,24 +13471,37 @@ popmotion@11.0.3: style-value-types "5.0.0" tslib "^2.1.0" +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + +postcss-load-config@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.1.tgz#152383f481c2758274404e4962743191d73875bd" + integrity sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA== + dependencies: + lilconfig "^2.0.5" + yaml "^2.1.1" + postcss-modules-extract-imports@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== postcss-modules-local-by-default@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c" - integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ== + version "4.0.4" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz#7cbed92abd312b94aaea85b68226d3dec39a14e6" + integrity sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q== dependencies: icss-utils "^5.0.0" postcss-selector-parser "^6.0.2" postcss-value-parser "^4.1.0" postcss-modules-scope@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" - integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== + version "3.1.1" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz#32cfab55e84887c079a19bbb215e721d683ef134" + integrity sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA== dependencies: postcss-selector-parser "^6.0.4" @@ -13508,10 +13512,24 @@ postcss-modules-values@^4.0.0: dependencies: icss-utils "^5.0.0" +postcss-modules@4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/postcss-modules/-/postcss-modules-4.3.1.tgz#517c06c09eab07d133ae0effca2c510abba18048" + integrity sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q== + dependencies: + generic-names "^4.0.0" + icss-replace-symbols "^1.1.0" + lodash.camelcase "^4.3.0" + postcss-modules-extract-imports "^3.0.0" + postcss-modules-local-by-default "^4.0.0" + postcss-modules-scope "^3.0.0" + postcss-modules-values "^4.0.0" + string-hash "^1.1.1" + postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: - version "6.0.11" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc" - integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== + version "6.0.15" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz#11cc2b21eebc0b99ea374ffb9887174855a01535" + integrity sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw== dependencies: cssesc "^3.0.0" util-deprecate "^1.0.2" @@ -13588,15 +13606,10 @@ postgres-range@^1.1.1: resolved "https://registry.yarnpkg.com/postgres-range/-/postgres-range-1.1.3.tgz#9ccd7b01ca2789eb3c2e0888b3184225fa859f76" integrity sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g== -preact@^10.12.0: - version "10.15.1" - resolved "https://registry.yarnpkg.com/preact/-/preact-10.15.1.tgz#a1de60c9fc0c79a522d969c65dcaddc5d994eede" - integrity sha512-qs2ansoQEwzNiV5eAcRT1p1EC/dmEzaATVDJNiB3g2sRDWdA7b7MurXdJjB2+/WQktGWZwxvDrnuRFbWuIr64g== - -preact@^10.5.9: - version "10.11.3" - resolved "https://registry.yarnpkg.com/preact/-/preact-10.11.3.tgz#8a7e4ba19d3992c488b0785afcc0f8aa13c78d19" - integrity sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg== +preact@^10.16.0: + version "10.19.6" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.19.6.tgz#66007b67aad4d11899f583df1b0116d94a89b8f5" + integrity sha512-gympg+T2Z1fG1unB8NH29yHJwnEaCH37Z32diPDku316OTnRPeMbiRV9kTrfZpocXjdfnWuFUl/Mj4BHaf6gnw== prebuild-install@^7.1.1: version "7.1.1" @@ -13654,6 +13667,11 @@ prismjs@~1.27.0: resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057" integrity sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA== +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + integrity sha512-yN0WQmuCX63LP/TMvAg31nvT6m4vDqJEiiv2CAZqWOGNWutc9DfDk1NPYYmKUFmaVM2UwDowH4u5AHWYP/jxKw== + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -13684,6 +13702,14 @@ progress@^2.0.3: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +prom-client@15.1.1: + version "15.1.1" + resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-15.1.1.tgz#71ba84371241acd173181b04a436782c246f3652" + integrity sha512-GVA2H96QCg2q71rjc3VYvSrVG7OpnJxyryC7dMzvfJfpJJHzQVwF3TJLfHzKORcwJpElWs1TwXLthlJAFJxq2A== + dependencies: + "@opentelemetry/api" "^1.4.0" + tdigest "^0.1.1" + promise-polyfill@^8.1.3: version "8.3.0" resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.3.0.tgz#9284810268138d103807b11f4e23d5e945a4db63" @@ -13764,6 +13790,23 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +qr-code-styling@^1.6.0-rc.1: + version "1.6.0-rc.1" + resolved "https://registry.yarnpkg.com/qr-code-styling/-/qr-code-styling-1.6.0-rc.1.tgz#6c89e185fa50cc9135101085c12ae95b06f1b290" + integrity sha512-ModRIiW6oUnsP18QzrRYZSc/CFKFKIdj7pUs57AEVH20ajlglRpN3HukjHk0UbNMTlKGuaYl7Gt6/O5Gg2NU2Q== + dependencies: + qrcode-generator "^1.4.3" + +qrcode-generator@^1.4.3: + version "1.4.4" + resolved "https://registry.yarnpkg.com/qrcode-generator/-/qrcode-generator-1.4.4.tgz#63f771224854759329a99048806a53ed278740e7" + integrity sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw== + +qrcode-terminal-nooctal@^0.12.1: + version "0.12.1" + resolved "https://registry.yarnpkg.com/qrcode-terminal-nooctal/-/qrcode-terminal-nooctal-0.12.1.tgz#45016aca0d82b2818de7af0a06d072ad671fbe2e" + integrity sha512-jy/kkD0iIMDjTucB+5T6KBsnirlhegDH47vHgrj5MejchSQmi/EAMM0xMFeePgV9CJkkAapNakpVUWYgHvtdKg== + qrcode@1.5.3: version "1.5.3" resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.3.tgz#03afa80912c0dccf12bc93f615a535aad1066170" @@ -13791,13 +13834,6 @@ qs@^6.10.2: dependencies: side-channel "^1.0.4" -qs@^6.10.3: - version "6.11.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" - integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== - dependencies: - side-channel "^1.0.4" - query-string@7.1.3: version "7.1.3" resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.3.tgz#a1cf90e994abb113a325804a972d98276fe02328" @@ -13808,16 +13844,6 @@ query-string@7.1.3: split-on-first "^1.0.0" strict-uri-encode "^2.0.0" -query-string@^6.13.5: - version "6.14.1" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.14.1.tgz#7ac2dca46da7f309449ba0f86b1fd28255b0c86a" - integrity sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw== - dependencies: - decode-uri-component "^0.2.0" - filter-obj "^1.1.0" - split-on-first "^1.0.0" - strict-uri-encode "^2.0.0" - querystringify@^2.1.1: version "2.2.0" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" @@ -13954,6 +13980,14 @@ react-hook-form@^7.33.1: resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.37.0.tgz#4d1738f092d3d8a3ade34ee892d97350b1032b19" integrity sha512-6NFTxsnw+EXSpNNvLr5nFMjPdYKRryQcelTHg7zwBB6vAzfPIcZq4AExP4heVlwdzntepQgwiOQW4z7Mr99Lsg== +react-i18next@^13.2.2: + version "13.5.0" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-13.5.0.tgz#44198f747628267a115c565f0c736a50a76b1ab0" + integrity sha512-CFJ5NDGJ2MUyBohEHxljOq/39NQ972rh1ajnadG9BjTk+UXbHLq4z5DKEbEQBDoIhUmmbuS/fIMJKo6VOax1HA== + dependencies: + "@babel/runtime" "^7.22.5" + html-parse-stringify "^3.0.1" + react-identicons@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/react-identicons/-/react-identicons-1.2.5.tgz#3502249e49d88f4e3500092694410a984bb102fa" @@ -14003,6 +14037,14 @@ react-jazzicon@^1.0.4: dependencies: mersenne-twister "^1.1.0" +react-native-webview@^11.26.0: + version "11.26.1" + resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-11.26.1.tgz#658c09ed5162dc170b361e48c2dd26c9712879da" + integrity sha512-hC7BkxOpf+z0UKhxFSFTPAM4shQzYmZHoELa6/8a/MspcjEP7ukYKpuSUTLDywQditT8yI9idfcKvfZDKQExGw== + dependencies: + escape-string-regexp "2.0.0" + invariant "2.2.4" + react-number-format@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/react-number-format/-/react-number-format-5.3.1.tgz#840c257da9cb4b248990d8db46e4d23e8bac67ff" @@ -14119,6 +14161,19 @@ react@18.2.0, react@>=17, react@^18.2.0: dependencies: loose-envify "^1.1.0" +readable-stream@2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" + integrity sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + safe-buffer "~5.1.1" + string_decoder "~1.0.3" + util-deprecate "~1.0.1" + readable-stream@3.6.2, readable-stream@^3.4.0: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" @@ -14141,7 +14196,20 @@ readable-stream@^2.3.3: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.1.1, readable-stream@^3.5.0, readable-stream@^3.6.0: +readable-stream@^2.3.7: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.1.1, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -14219,6 +14287,19 @@ refa@^0.9.0: dependencies: regexpp "^3.2.0" +reflect.getprototypeof@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" + integrity sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.1" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + globalthis "^1.0.3" + which-builtin-type "^1.1.3" + refractor@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.6.0.tgz#ac318f5a0715ead790fcfb0c71f4dd83d977935a" @@ -14283,7 +14364,7 @@ regexp-ast-analysis@^0.5.1: refa "^0.9.0" regexpp "^3.2.0" -regexp.prototype.flags@^1.4.1, regexp.prototype.flags@^1.4.3: +regexp.prototype.flags@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== @@ -14292,6 +14373,16 @@ regexp.prototype.flags@^1.4.1, regexp.prototype.flags@^1.4.3: define-properties "^1.1.3" functions-have-names "^1.2.2" +regexp.prototype.flags@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" + integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== + dependencies: + call-bind "^1.0.6" + define-properties "^1.2.1" + es-errors "^1.3.0" + set-function-name "^2.0.1" + regexpp@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" @@ -14385,7 +14476,7 @@ resolve.exports@^1.1.0: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== -resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0: +resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== @@ -14394,7 +14485,7 @@ resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^1.22.1: +resolve@^1.22.1, resolve@^1.22.4: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -14403,7 +14494,7 @@ resolve@^1.22.1: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^2.0.0-next.3, resolve@^2.0.0-next.4: +resolve@^2.0.0-next.4: version "2.0.0-next.4" resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.4.tgz#3d37a113d6429f496ec4752d2a2e58efb1fd4660" integrity sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ== @@ -14412,6 +14503,15 @@ resolve@^2.0.0-next.3, resolve@^2.0.0-next.4: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^2.0.0-next.5: + version "2.0.0-next.5" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" + integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + restore-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" @@ -14454,6 +14554,16 @@ robust-predicates@^3.0.0: resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.1.tgz#ecde075044f7f30118682bd9fb3f123109577f9a" integrity sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g== +rollup-plugin-visualizer@^5.9.2: + version "5.12.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.12.0.tgz#661542191ce78ee4f378995297260d0c1efb1302" + integrity sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ== + dependencies: + open "^8.4.0" + picomatch "^2.3.1" + source-map "^0.7.4" + yargs "^17.5.1" + rollup@^3.27.1: version "3.29.4" resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981" @@ -14461,19 +14571,6 @@ rollup@^3.27.1: optionalDependencies: fsevents "~2.3.2" -rpc-websockets@^7.5.0: - version "7.5.0" - resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-7.5.0.tgz#bbeb87572e66703ff151e50af1658f98098e2748" - integrity sha512-9tIRi1uZGy7YmDjErf1Ax3wtqdSSLIlnmL5OtOzgd5eqPKbsPpwDP5whUDO2LQay3Xp0CcHlcNSGzacNRluBaQ== - dependencies: - "@babel/runtime" "^7.17.2" - eventemitter3 "^4.0.7" - uuid "^8.3.2" - ws "^8.5.0" - optionalDependencies: - bufferutil "^4.0.1" - utf-8-validate "^5.0.2" - run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -14486,13 +14583,6 @@ rw@1: resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ== -rxjs@^6.6.3: - version "6.6.7" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" - integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== - dependencies: - tslib "^1.9.0" - rxjs@^7.5.5: version "7.5.7" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.7.tgz#2ec0d57fdc89ece220d2e702730ae8f1e49def39" @@ -14500,6 +14590,16 @@ rxjs@^7.5.5: dependencies: tslib "^2.1.0" +safe-array-concat@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" + integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + has-symbols "^1.0.3" + isarray "^2.0.5" + safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -14510,11 +14610,6 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-json-utils@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/safe-json-utils/-/safe-json-utils-1.1.1.tgz#0e883874467d95ab914c3f511096b89bfb3e63b1" - integrity sha512-SAJWGKDs50tAbiDXLf89PDwt9XYkWyANFWVzn4dTXl5QyI8t2o/bW5/OJl3lvc2WVU4MEpTo9Yz5NVFNsp+OJQ== - safe-regex-test@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" @@ -14524,6 +14619,15 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" +safe-regex-test@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" + integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-regex "^1.1.4" + safe-stable-stringify@^2.1.0: version "2.4.2" resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.2.tgz#ec7b037768098bf65310d1d64370de0dc02353aa" @@ -14553,11 +14657,6 @@ scheduler@^0.23.0: dependencies: loose-envify "^1.1.0" -scrypt-js@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312" - integrity sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA== - scslre@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/scslre/-/scslre-0.1.6.tgz#71a2832e4bf3a9254973a04fbed90aec94f75757" @@ -14567,6 +14666,15 @@ scslre@^0.1.6: regexp-ast-analysis "^0.2.3" regexpp "^3.2.0" +secp256k1@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-5.0.0.tgz#be6f0c8c7722e2481e9773336d351de8cddd12f7" + integrity sha512-TKWX8xvoGHrxVdqbYeZM9w+izTF4b9z3NhSaDkdn81btvuh+ivbIMGT/zQvDtTFWhRlThpoz6LEYTr7n8A5GcA== + dependencies: + elliptic "^6.5.4" + node-addon-api "^5.0.0" + node-gyp-build "^4.2.0" + secure-json-parse@^2.4.0: version "2.5.0" resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.5.0.tgz#f929829df2adc7ccfb53703569894d051493a6ac" @@ -14579,7 +14687,7 @@ semver@7.x, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.2, semver@^ dependencies: lru-cache "^6.0.0" -semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: +semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== @@ -14596,6 +14704,28 @@ set-blocking@^2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +set-function-name@^2.0.1, set-function-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + set-value@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/set-value/-/set-value-4.1.0.tgz#aa433662d87081b75ad88a4743bd450f044e7d09" @@ -14643,6 +14773,16 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" +side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -14667,14 +14807,14 @@ simple-get@^4.0.0: once "^1.3.1" simple-concat "^1.0.0" -sirv@^1.0.7: - version "1.0.19" - resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.19.tgz#1d73979b38c7fe91fcba49c85280daa9c2363b49" - integrity sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ== +sirv@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/sirv/-/sirv-2.0.4.tgz#5dd9a725c578e34e449f332703eb2a74e46a29b0" + integrity sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ== dependencies: - "@polka/url" "^1.0.0-next.20" - mrmime "^1.0.0" - totalist "^1.0.0" + "@polka/url" "^1.0.0-next.24" + mrmime "^2.0.0" + totalist "^3.0.0" sisteransi@^1.0.5: version "1.0.5" @@ -14722,6 +14862,24 @@ slice-ansi@^5.0.0: ansi-styles "^6.0.0" is-fullwidth-code-point "^4.0.0" +socket.io-client@^4.5.1: + version "4.7.4" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.7.4.tgz#5f0e060ff34ac0a4b4c5abaaa88e0d1d928c64c8" + integrity sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.2" + engine.io-client "~6.5.2" + socket.io-parser "~4.2.4" + +socket.io-parser@~4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" + integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + sonic-boom@^2.2.1: version "2.8.0" resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-2.8.0.tgz#c1def62a77425090e6ad7516aad8eb402e047611" @@ -14759,6 +14917,11 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +source-map@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" + integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== + space-separated-tokens@^1.0.0: version "1.1.5" resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899" @@ -14811,14 +14974,6 @@ std-env@^3.4.3: resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.6.0.tgz#94807562bddc68fa90f2e02c5fd5b6865bb4e98e" integrity sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg== -stream-browserify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" - integrity sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA== - dependencies: - inherits "~2.0.4" - readable-stream "^3.5.0" - stream-shift@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" @@ -14839,7 +14994,7 @@ string-argv@^0.3.1: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== -string-hash@^1.1.3: +string-hash@^1.1.1, string-hash@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" integrity sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A== @@ -14875,19 +15030,23 @@ string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" -string.prototype.matchall@^4.0.7: - version "4.0.7" - resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz#8e6ecb0d8a1fb1fda470d81acecb2dba057a481d" - integrity sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - get-intrinsic "^1.1.1" +string.prototype.matchall@^4.0.10: + version "4.0.11" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" + integrity sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" + gopd "^1.0.1" has-symbols "^1.0.3" - internal-slot "^1.0.3" - regexp.prototype.flags "^1.4.1" - side-channel "^1.0.4" + internal-slot "^1.0.7" + regexp.prototype.flags "^1.5.2" + set-function-name "^2.0.2" + side-channel "^1.0.6" string.prototype.matchall@^4.0.8: version "4.0.8" @@ -14903,6 +15062,16 @@ string.prototype.matchall@^4.0.8: regexp.prototype.flags "^1.4.3" side-channel "^1.0.4" +string.prototype.trim@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" + integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-object-atoms "^1.0.0" + string.prototype.trimend@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz#914a65baaab25fbdd4ee291ca7dde57e869cb8d0" @@ -14921,6 +15090,15 @@ string.prototype.trimend@^1.0.6: define-properties "^1.1.4" es-abstract "^1.20.4" +string.prototype.trimend@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" + integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + string.prototype.trimstart@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz#5466d93ba58cfa2134839f81d7f42437e8c01fef" @@ -14939,6 +15117,15 @@ string.prototype.trimstart@^1.0.6: define-properties "^1.1.4" es-abstract "^1.20.4" +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -14946,6 +15133,13 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" +string_decoder@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" + integrity sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ== + dependencies: + safe-buffer "~5.1.0" + string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -14987,7 +15181,7 @@ strip-final-newline@^3.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -14997,6 +15191,11 @@ strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== +style-inject@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/style-inject/-/style-inject-0.3.0.tgz#d21c477affec91811cc82355832a700d22bf8dd3" + integrity sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw== + style-loader@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575" @@ -15022,6 +15221,11 @@ stylis@4.1.3: resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.1.3.tgz#fd2fbe79f5fed17c55269e16ed8da14c84d069f7" integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA== +stylis@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" + integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== + sucrase@^3.20.3: version "3.29.0" resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.29.0.tgz#3207c5bc1b980fdae1e539df3f8a8a518236da7d" @@ -15034,11 +15238,6 @@ sucrase@^3.20.3: pirates "^4.0.1" ts-interface-checker "^0.1.9" -superstruct@^0.14.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.14.2.tgz#0dbcdf3d83676588828f1cf5ed35cda02f59025b" - integrity sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ== - superstruct@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-1.0.3.tgz#de626a5b49c6641ff4d37da3c7598e7a87697046" @@ -15227,6 +15426,13 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" +tdigest@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/tdigest/-/tdigest-0.1.2.tgz#96c64bac4ff10746b910b0e23b515794e12faced" + integrity sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA== + dependencies: + bintrees "1.0.2" + test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" @@ -15236,11 +15442,6 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" -text-encoding-utf-8@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13" - integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg== - text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -15283,7 +15484,7 @@ thriftrw@^3.5.0: error "7.0.2" long "^2.4.0" -"through@>=2.2.7 <3", through@^2.3.8: +through@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== @@ -15340,10 +15541,10 @@ toggle-selection@^1.0.6: resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ== -totalist@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" - integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g== +totalist@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8" + integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ== tough-cookie@^4.1.2: version "4.1.3" @@ -15442,13 +15643,13 @@ ts-toolbelt@^9.6.0: resolved "https://registry.yarnpkg.com/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz#50a25426cfed500d4a09bd1b3afb6f28879edfd5" integrity sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w== -tsconfig-paths@^3.14.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" - integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== +tsconfig-paths@^3.15.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" + integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== dependencies: "@types/json5" "^0.0.29" - json5 "^1.0.1" + json5 "^1.0.2" minimist "^1.2.6" strip-bom "^3.0.0" @@ -15461,7 +15662,7 @@ tsconfig-paths@^4.0.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@1.14.1, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: +tslib@1.14.1, tslib@^1.8.1, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== @@ -15476,6 +15677,11 @@ tslib@^2.3.0, tslib@^2.5.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== +tslib@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -15519,6 +15725,38 @@ type-fest@^0.21.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== +typed-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" + integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-typed-array "^1.1.13" + +typed-array-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" + integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-byte-offset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" + integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + typed-array-length@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" @@ -15528,12 +15766,17 @@ typed-array-length@^1.0.4: for-each "^0.3.3" is-typed-array "^1.1.9" -typedarray-to-buffer@3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" - integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== +typed-array-length@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" + integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== dependencies: - is-typedarray "^1.0.0" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" types-ramda@^0.29.5: version "0.29.5" @@ -15542,10 +15785,10 @@ types-ramda@^0.29.5: dependencies: ts-toolbelt "^9.6.0" -typescript@^5.1.0: - version "5.1.6" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" - integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== +typescript@5.4.2: + version "5.4.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.2.tgz#0ae9cebcfae970718474fe0da2c090cad6577372" + integrity sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ== typescript@^5.2.2: version "5.3.3" @@ -15612,6 +15855,11 @@ unenv@^1.7.4: node-fetch-native "^1.4.1" pathe "^1.1.1" +unfetch@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" + integrity sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -15732,15 +15980,15 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" -use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.0: +use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== -utf-8-validate@^5.0.2: - version "5.0.10" - resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2" - integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ== +utf-8-validate@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-6.0.3.tgz#7d8c936d854e86b24d1d655f138ee27d2636d777" + integrity sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA== dependencies: node-gyp-build "^4.3.0" @@ -15787,26 +16035,17 @@ valtio@1.11.2: proxy-compare "2.5.1" use-sync-external-store "1.2.0" -valtio@1.12.1: - version "1.12.1" - resolved "https://registry.yarnpkg.com/valtio/-/valtio-1.12.1.tgz#741f8bd46586f8c4b1a7639b1348252a052b746e" - integrity sha512-R0V4H86Xi2Pp7pmxN/EtV4Q6jr6PMN3t1IwxEvKUp6160r8FimvPh941oWyeK1iec/DTsh9Jb3Q+GputMS8SYg== - dependencies: - derive-valtio "0.1.0" - proxy-compare "2.5.1" - use-sync-external-store "1.2.0" - -viem@1.20.1: - version "1.20.1" - resolved "https://registry.yarnpkg.com/viem/-/viem-1.20.1.tgz#ea92f9bab2fded4be556be4d4be724805d11780e" - integrity sha512-PaYyfuCVkNFzdaVoV8HefMIt5YtjIG6UPqxOz0rYchN52cD05YcRJlO0h/XrehQfo8ib2RSOkZ0ChJ6Adlh1Jg== +viem@2.9.6: + version "2.9.6" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.9.6.tgz#66819684e878b6ea659ad9c49a0a864512859190" + integrity sha512-VVFWjGQei2wnWTvAB/xrIf22m6flCwxeBr8LnwejXMTSSi1EORWEswrw2lfKTmw3TlRPSG4uSiQMa/d0l0DKRg== dependencies: "@adraffy/ens-normalize" "1.10.0" "@noble/curves" "1.2.0" "@noble/hashes" "1.3.2" "@scure/bip32" "1.3.2" "@scure/bip39" "1.2.1" - abitype "0.9.8" + abitype "1.0.0" isows "1.0.3" ws "8.13.0" @@ -15825,6 +16064,20 @@ viem@^1.0.0: isomorphic-ws "5.0.0" ws "8.12.0" +viem@^1.1.4, viem@^1.20.3: + version "1.21.4" + resolved "https://registry.yarnpkg.com/viem/-/viem-1.21.4.tgz#883760e9222540a5a7e0339809202b45fe6a842d" + integrity sha512-BNVYdSaUjeS2zKQgPs+49e5JKocfo60Ib2yiXOWBT6LuVxY1I/6fFX3waEtpXvL1Xn4qu+BVitVtMh9lyThyhQ== + dependencies: + "@adraffy/ens-normalize" "1.10.0" + "@noble/curves" "1.2.0" + "@noble/hashes" "1.3.2" + "@scure/bip32" "1.3.2" + "@scure/bip39" "1.2.1" + abitype "0.9.8" + isows "1.0.3" + ws "8.13.0" + vite-plugin-svgr@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/vite-plugin-svgr/-/vite-plugin-svgr-2.2.2.tgz#c5c9cb573bf455bb079550531847ddc5d2e122af" @@ -15854,6 +16107,11 @@ vite@^4.4.12: optionalDependencies: fsevents "~2.3.2" +void-elements@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" + integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== + vscode-languageserver-types@^3.17.1: version "3.17.3" resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz#72d05e47b73be93acb84d6e311b5786390f13f64" @@ -15877,17 +16135,14 @@ w3c-xmlserializer@^3.0.0: dependencies: xml-name-validator "^4.0.0" -wagmi@1.4.12: - version "1.4.12" - resolved "https://registry.yarnpkg.com/wagmi/-/wagmi-1.4.12.tgz#e5d31c6d7621ecd9e32eded6c7b1041201223127" - integrity sha512-QRxpjhdMlZmbYTfn9VQkQMKq+l3kwA1O7tF10vaykPrjbGX+IIlyn72ib9oqW9BfQO7n/Sf/mnVz1zbxRhGPWA== +wagmi@2.5.16: + version "2.5.16" + resolved "https://registry.yarnpkg.com/wagmi/-/wagmi-2.5.16.tgz#42e4be44dd8e96deda10d953b91d13ee3a512758" + integrity sha512-nVSx65kGYcjvELJKoe+l1E78zHqtYwfg0nf2HuZkt1gVvqguo+hIQe534hSf1m+ivdtb0MPlwNCvj6K4QJ7FTw== dependencies: - "@tanstack/query-sync-storage-persister" "^4.27.1" - "@tanstack/react-query" "^4.28.0" - "@tanstack/react-query-persist-client" "^4.28.0" - "@wagmi/core" "1.4.12" - abitype "0.8.7" - use-sync-external-store "^1.2.0" + "@wagmi/connectors" "4.1.22" + "@wagmi/core" "2.6.13" + use-sync-external-store "1.2.0" walker@^1.0.8: version "1.0.8" @@ -15896,14 +16151,6 @@ walker@^1.0.8: dependencies: makeerror "1.0.12" -watchpack@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" - integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== - dependencies: - glob-to-regexp "^0.4.1" - graceful-fs "^4.1.2" - web-streams-polyfill@^3.0.3: version "3.2.1" resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" @@ -15943,19 +16190,23 @@ webidl-conversions@^7.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== -webpack-bundle-analyzer@4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.7.0.tgz#33c1c485a7fcae8627c547b5c3328b46de733c66" - integrity sha512-j9b8ynpJS4K+zfO5GGwsAcQX4ZHpWV+yRiHDiL+bE0XHJ8NiPYLTNVQdlFYWxtpg9lfAQNlwJg16J9AJtFSXRg== +webpack-bundle-analyzer@4.10.1: + version "4.10.1" + resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz#84b7473b630a7b8c21c741f81d8fe4593208b454" + integrity sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ== dependencies: + "@discoveryjs/json-ext" "0.5.7" acorn "^8.0.4" acorn-walk "^8.0.0" - chalk "^4.1.0" commander "^7.2.0" + debounce "^1.2.1" + escape-string-regexp "^4.0.0" gzip-size "^6.0.0" - lodash "^4.17.20" + html-escaper "^2.0.2" + is-plain-object "^5.0.0" opener "^1.5.2" - sirv "^1.0.7" + picocolors "^1.0.0" + sirv "^2.0.3" ws "^7.3.1" whatwg-encoding@^2.0.0: @@ -15997,6 +16248,24 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" +which-builtin-type@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.3.tgz#b1b8443707cc58b6e9bf98d32110ff0c2cbd029b" + integrity sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw== + dependencies: + function.prototype.name "^1.1.5" + has-tostringtag "^1.0.0" + is-async-function "^2.0.0" + is-date-object "^1.0.5" + is-finalizationregistry "^1.0.2" + is-generator-function "^1.0.10" + is-regex "^1.1.4" + is-weakref "^1.0.2" + isarray "^2.0.5" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.1" + which-typed-array "^1.1.9" + which-collection@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" @@ -16012,6 +16281,17 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q== +which-typed-array@^1.1.14, which-typed-array@^1.1.15: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" + which-typed-array@^1.1.2, which-typed-array@^1.1.8, which-typed-array@^1.1.9: version "1.1.9" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" @@ -16031,7 +16311,7 @@ which@^2.0.1, which@^2.0.2: dependencies: isexe "^2.0.0" -word-wrap@^1.2.3, word-wrap@~1.2.3: +word-wrap@^1.2.5, word-wrap@~1.2.3: version "1.2.5" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== @@ -16076,12 +16356,7 @@ write-file-atomic@^4.0.1: imurmurhash "^0.1.4" signal-exit "^3.0.7" -ws@7.4.6: - version "7.4.6" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" - integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== - -ws@8.12.0, ws@^8.5.0: +ws@8.12.0: version "8.12.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.0.tgz#485074cc392689da78e1828a9ff23585e06cddd8" integrity sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig== @@ -16091,12 +16366,12 @@ ws@8.13.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== -ws@^7.3.1, ws@^7.4.5, ws@^7.5.1: +ws@^7.3.1, ws@^7.5.1: version "7.5.9" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== -ws@^8.11.0, ws@^8.9.0: +ws@^8.11.0, ws@^8.9.0, ws@~8.11.0: version "8.11.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== @@ -16123,6 +16398,11 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xmlhttprequest-ssl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" + integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== + xorshift@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/xorshift/-/xorshift-1.2.0.tgz#30a4cdd8e9f8d09d959ed2a88c42a09c660e8148" @@ -16167,9 +16447,9 @@ yaml@^1.10.0: integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== yaml@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.1.3.tgz#9b3a4c8aff9821b696275c79a8bee8399d945207" - integrity sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg== + version "2.4.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.1.tgz#2e57e0b5e995292c25c75d2658f0664765210eed" + integrity sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg== yaml@^2.2.2: version "2.3.1" @@ -16219,7 +16499,7 @@ yargs@^17.3.1: y18n "^5.0.5" yargs-parser "^21.1.1" -yargs@^17.7.2: +yargs@^17.5.1, yargs@^17.7.2: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== @@ -16252,9 +16532,9 @@ zod@3.22.4: resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg== -zustand@^4.3.1: - version "4.3.8" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.8.tgz#37113df8e9e1421b0be1b2dca02b49b76210e7c4" - integrity sha512-4h28KCkHg5ii/wcFFJ5Fp+k1J3gJoasaIbppdgZFO4BPJnsNxL0mQXBSFgOgAdCdBj35aDTPvdAJReTMntFPGg== +zustand@4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.4.1.tgz#0cd3a3e4756f21811bd956418fdc686877e8b3b0" + integrity sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw== dependencies: use-sync-external-store "1.2.0"