Commit dfc33b4b authored by Max Alekseenko's avatar Max Alekseenko

Merge main into rewards

parents 3ead00d4 d5863de0
...@@ -81,6 +81,12 @@ jobs: ...@@ -81,6 +81,12 @@ jobs:
with: with:
platforms: linux/amd64,linux/arm64/v8 platforms: linux/amd64,linux/arm64/v8
sync_envs_docs:
name: Sync ENV variables docs
uses: './.github/workflows/sync-envs-docs.yml'
needs: publish_image
secrets: inherit
# Temporary disable this step because it is broken # Temporary disable this step because it is broken
# There is an issue with building web3modal deps # There is an issue with building web3modal deps
upload_source_maps: upload_source_maps:
......
name: Sync ENV variables docs
on:
workflow_dispatch:
jobs:
run:
name: Run
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Copy main ENV file to Blockscout Docs repository
uses: dmnemec/copy_file_to_another_repo_action@main
env:
API_TOKEN_GITHUB: ${{ secrets.GITHIB_BOT_TOKEN }}
with:
source_file: 'docs/ENVS.md'
destination_repo: 'blockscout/docs'
destination_folder: 'setup/env-variables/frontend-common-envs'
rename: 'envs.md'
destination_branch: 'master'
user_email: 'bot@blockscout.com'
user_name: 'blockscout-bot'
use_rsync: true
- name: Copy deprecated ENV file to Blockscout Docs repository
uses: dmnemec/copy_file_to_another_repo_action@main
env:
API_TOKEN_GITHUB: ${{ secrets.GITHIB_BOT_TOKEN }}
with:
source_file: 'docs/DEPRECATED_ENVS.md'
destination_repo: 'blockscout/docs'
destination_folder: 'setup/env-variables/frontend-common-envs'
rename: 'deprecated-envs.md'
destination_branch: 'master'
user_email: 'bot@blockscout.com'
user_name: 'blockscout-bot'
use_rsync: true
import type { Feature } from './types'; import type { Feature } from './types';
import type { MultichainProviderConfig } from 'types/client/multichainProviderConfig'; import type { MultichainProviderConfig, MultichainProviderConfigParsed } from 'types/client/multichainProviderConfig';
import { getEnvValue, parseEnvJson } from '../utils'; import { getEnvValue, parseEnvJson } from '../utils';
import marketplace from './marketplace'; import marketplace from './marketplace';
const value = parseEnvJson<MultichainProviderConfig>(getEnvValue('NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG')); const value = parseEnvJson<Array<MultichainProviderConfig>>(getEnvValue('NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG'));
const title = 'Multichain balance'; const title = 'Multichain balance';
const config: Feature<{name: string; logoUrl?: string; urlTemplate: string; dappId?: string }> = (() => { const config: Feature<{ providers: Array<MultichainProviderConfigParsed> }> = (() => {
if (value) { if (value) {
return Object.freeze({ return Object.freeze({
title, title,
isEnabled: true, isEnabled: true,
name: value.name, providers: value.map((provider) => ({
logoUrl: value.logo, name: provider.name,
urlTemplate: value.url_template, logoUrl: provider.logo,
dappId: marketplace.isEnabled ? value.dapp_id : undefined, urlTemplate: provider.url_template,
dappId: marketplace.isEnabled ? provider.dapp_id : undefined,
})),
}); });
} }
......
...@@ -38,7 +38,7 @@ NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6u ...@@ -38,7 +38,7 @@ NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6u
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_METASUITES_ENABLED=true NEXT_PUBLIC_METASUITES_ENABLED=true
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'} NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG=[{'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}]
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps'] NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps']
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
......
...@@ -39,7 +39,7 @@ NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKj ...@@ -39,7 +39,7 @@ NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKj
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_METASUITES_ENABLED=true NEXT_PUBLIC_METASUITES_ENABLED=true
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'} NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG=[{'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}, {'name': 'zerion2', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}]
NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Need gas?', 'dapp_id': 'smol-refuel', 'url_template': 'https://smolrefuel.com/?outboundChain={chainId}&partner=blockscout&utm_source=blockscout&utm_medium=address&disableBridges=true', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png'} NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Need gas?', 'dapp_id': 'smol-refuel', 'url_template': 'https://smolrefuel.com/?outboundChain={chainId}&partner=blockscout&utm_source=blockscout&utm_medium=address&disableBridges=true', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png'}
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps'] NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps']
......
...@@ -38,7 +38,7 @@ NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6u ...@@ -38,7 +38,7 @@ NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6u
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_METASUITES_ENABLED=true NEXT_PUBLIC_METASUITES_ENABLED=true
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'} NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG=[{'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}]
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps','/account/rewards'] NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps','/account/rewards']
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
......
...@@ -45,7 +45,7 @@ NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKj ...@@ -45,7 +45,7 @@ NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKj
NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/refs/heads/marketplace-graph-test/test-configs/marketplace-graph-links.json NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/refs/heads/marketplace-graph-test/test-configs/marketplace-graph-links.json
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_METASUITES_ENABLED=true NEXT_PUBLIC_METASUITES_ENABLED=true
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'} NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG=[{'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}]
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps'] NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps']
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
......
...@@ -39,7 +39,7 @@ NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6u ...@@ -39,7 +39,7 @@ NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6u
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata-test.k8s-dev.blockscout.com NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata-test.k8s-dev.blockscout.com
NEXT_PUBLIC_METASUITES_ENABLED=true NEXT_PUBLIC_METASUITES_ENABLED=true
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'} NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG=[{'name': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}]
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens-rs-test.k8s-dev.blockscout.com NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/blocks','/apps'] NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/blocks','/apps']
NEXT_PUBLIC_NAVIGATION_LAYOUT=horizontal NEXT_PUBLIC_NAVIGATION_LAYOUT=horizontal
......
...@@ -39,7 +39,7 @@ NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6u ...@@ -39,7 +39,7 @@ NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6u
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_METASUITES_ENABLED=true NEXT_PUBLIC_METASUITES_ENABLED=true
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'} NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG=[{'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}]
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps'] NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps']
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
......
...@@ -27,7 +27,7 @@ NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true ...@@ -27,7 +27,7 @@ NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_LOGOUT_URL=https://zksync.us.auth0.com/v2/logout NEXT_PUBLIC_LOGOUT_URL=https://zksync.us.auth0.com/v2/logout
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_METASUITES_ENABLED=true NEXT_PUBLIC_METASUITES_ENABLED=true
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'} NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG=[{'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}]
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=ETH NEXT_PUBLIC_NETWORK_CURRENCY_NAME=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
......
...@@ -56,5 +56,5 @@ NEXT_PUBLIC_ROLLUP_TYPE=optimistic ...@@ -56,5 +56,5 @@ NEXT_PUBLIC_ROLLUP_TYPE=optimistic
NEXT_PUBLIC_STATS_API_HOST=https://stats-l2-zora-mainnet.k8s-prod-1.blockscout.com NEXT_PUBLIC_STATS_API_HOST=https://stats-l2-zora-mainnet.k8s-prod-1.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'} NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG=[{'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}]
NEXT_PUBLIC_ADDRESS_USERNAME_TAG={'api_url_template': 'https://api.zora.co/discover/user/{address}', 'tag_link_template': 'httpszora.co/{username}', 'tag_icon': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/zora.svg', 'tag_bg_color': 'rgba(0,0,0)', 'tag_text_color': 'rgba(255,255,255)'} NEXT_PUBLIC_ADDRESS_USERNAME_TAG={'api_url_template': 'https://api.zora.co/discover/user/{address}', 'tag_link_template': 'httpszora.co/{username}', 'tag_icon': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/zora.svg', 'tag_bg_color': 'rgba(0,0,0)', 'tag_text_color': 'rgba(255,255,255)'}
\ No newline at end of file
...@@ -39,9 +39,14 @@ export_envs_from_preset ...@@ -39,9 +39,14 @@ export_envs_from_preset
./download_assets.sh ./public/assets/configs ./download_assets.sh ./public/assets/configs
# Check run-time ENVs values # Check run-time ENVs values
./validate_envs.sh if [ "$SKIP_ENVS_VALIDATION" != "true" ]; then
if [ $? -ne 0 ]; then ./validate_envs.sh
exit 1 if [ $? -ne 0 ]; then
exit 1
fi
else
echo "😱 Skipping ENVs validation."
echo
fi fi
# Generate favicons bundle # Generate favicons bundle
......
...@@ -90,7 +90,7 @@ brace-expansion@^1.1.7: ...@@ -90,7 +90,7 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0" balanced-match "^1.0.0"
concat-map "0.0.1" concat-map "0.0.1"
braces@^3.0.2: braces@^3.0.3:
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
...@@ -441,11 +441,11 @@ merge2@^1.3.0, merge2@^1.4.1: ...@@ -441,11 +441,11 @@ merge2@^1.3.0, merge2@^1.4.1:
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
micromatch@^4.0.4: micromatch@^4.0.4:
version "4.0.5" version "4.0.8"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
dependencies: dependencies:
braces "^3.0.2" braces "^3.0.3"
picomatch "^2.3.1" picomatch "^2.3.1"
minimatch@^3.1.1: minimatch@^3.1.1:
...@@ -582,9 +582,9 @@ requirejs-config-file@^4.0.0: ...@@ -582,9 +582,9 @@ requirejs-config-file@^4.0.0:
stringify-object "^3.2.1" stringify-object "^3.2.1"
requirejs@^2.3.6: requirejs@^2.3.6:
version "2.3.6" version "2.3.7"
resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.6.tgz#e5093d9601c2829251258c0b9445d4d19fa9e7c9" resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.7.tgz#0b22032e51a967900e0ae9f32762c23a87036bd0"
integrity sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg== integrity "sha1-CyIDLlGpZ5AOCunzJ2LCOocDa9A= sha512-DouTG8T1WanGok6Qjg2SXuCMzszOo0eHeH9hDZ5Y4x8Je+9JB38HdTLT4/VA8OaUhBa0JPVHJ0pyBkM1z+pDsw=="
resolve-dependency-path@^3.0.2: resolve-dependency-path@^3.0.2:
version "3.0.2" version "3.0.2"
......
...@@ -530,6 +530,13 @@ const deFiDropdownItemSchema: yup.ObjectSchema<DeFiDropdownItem> = yup ...@@ -530,6 +530,13 @@ const deFiDropdownItemSchema: yup.ObjectSchema<DeFiDropdownItem> = yup
return Boolean(value.dappId) || Boolean(value.url); return Boolean(value.dappId) || Boolean(value.url);
}) as yup.ObjectSchema<DeFiDropdownItem>; }) as yup.ObjectSchema<DeFiDropdownItem>;
const multichainProviderConfigSchema: yup.ObjectSchema<MultichainProviderConfig> = yup.object({
name: yup.string().required(),
url_template: yup.string().required(),
logo: yup.string().required(),
dapp_id: yup.string(),
});
const schema = yup const schema = yup
.object() .object()
.noUnknown(true, (params) => { .noUnknown(true, (params) => {
...@@ -768,18 +775,10 @@ const schema = yup ...@@ -768,18 +775,10 @@ const schema = yup
NEXT_PUBLIC_HAS_USER_OPS: yup.boolean(), NEXT_PUBLIC_HAS_USER_OPS: yup.boolean(),
NEXT_PUBLIC_METASUITES_ENABLED: yup.boolean(), NEXT_PUBLIC_METASUITES_ENABLED: yup.boolean(),
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG: yup NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG: yup
.mixed() .array()
.test('shape', 'Invalid schema were provided for NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG, it should have name and url template', (data) => { .transform(replaceQuotes)
const isUndefined = data === undefined; .json()
const valueSchema = yup.object<MultichainProviderConfig>().transform(replaceQuotes).json().shape({ .of(multichainProviderConfigSchema),
name: yup.string().required(),
url_template: yup.string().required(),
logo: yup.string(),
dapp_id: yup.string(),
});
return isUndefined || valueSchema.isValidSync(data);
}),
NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG: yup NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG: yup
.mixed() .mixed()
.test('shape', 'Invalid schema were provided for NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG, it should have name and url template', (data) => { .test('shape', 'Invalid schema were provided for NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG, it should have name and url template', (data) => {
......
...@@ -83,7 +83,7 @@ NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET=false ...@@ -83,7 +83,7 @@ NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET=false
NEXT_PUBLIC_WEB3_WALLETS=['coinbase','metamask','token_pocket'] NEXT_PUBLIC_WEB3_WALLETS=['coinbase','metamask','token_pocket']
NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE=stability NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE=stability
NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'uniswap'},{'text':'Payment link','icon':'payment_link','url':'https://example.com'}] NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'uniswap'},{'text':'Payment link','icon':'payment_link','url':'https://example.com'}]
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'} NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG=[{'name': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}]
NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Need gas?', 'dapp_id': 'smol-refuel', 'url_template': 'https://smolrefuel.com/?outboundChain={chainId}&partner=blockscout&utm_source=blockscout&utm_medium=address&disableBridges=true', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png'} NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Need gas?', 'dapp_id': 'smol-refuel', 'url_template': 'https://smolrefuel.com/?outboundChain={chainId}&partner=blockscout&utm_source=blockscout&utm_medium=address&disableBridges=true', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png'}
NEXT_PUBLIC_SAVE_ON_GAS_ENABLED=true NEXT_PUBLIC_SAVE_ON_GAS_ENABLED=true
NEXT_PUBLIC_REWARDS_SERVICE_API_HOST=https://example.com NEXT_PUBLIC_REWARDS_SERVICE_API_HOST=https://example.com
...@@ -21,6 +21,11 @@ ...@@ -21,6 +21,11 @@
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
"@jridgewell/resolve-uri@^3.1.0":
version "3.1.2"
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
"@jridgewell/set-array@^1.0.1": "@jridgewell/set-array@^1.0.1":
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
...@@ -44,7 +49,20 @@ ...@@ -44,7 +49,20 @@
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": "@jridgewell/sourcemap-codec@^1.4.14":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
"@jridgewell/trace-mapping@^0.3.20":
version "0.3.25"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0"
integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==
dependencies:
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@jridgewell/trace-mapping@^0.3.9":
version "0.3.18" version "0.3.18"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6"
integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==
...@@ -52,28 +70,12 @@ ...@@ -52,28 +70,12 @@
"@jridgewell/resolve-uri" "3.1.0" "@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14" "@jridgewell/sourcemap-codec" "1.4.14"
"@types/eslint-scope@^3.7.3": "@types/estree@^1.0.5":
version "3.7.4" version "1.0.6"
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
dependencies:
"@types/eslint" "*"
"@types/estree" "*"
"@types/eslint@*":
version "8.44.0"
resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.44.0.tgz#55818eabb376e2272f77fbf5c96c43137c3c1e53"
integrity sha512-gsF+c/0XOguWgaOgvFs+xnnRqt9GwgTvIks36WpE6ueeI4KCEHHd8K/CKHqhOqrJKsYH8m27kRzQEvWXAwXUTw==
dependencies:
"@types/estree" "*"
"@types/json-schema" "*"
"@types/estree@*", "@types/estree@^1.0.0": "@types/json-schema@^7.0.8":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194"
integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==
"@types/json-schema@*", "@types/json-schema@^7.0.8":
version "7.0.12" version "7.0.12"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
...@@ -83,10 +85,10 @@ ...@@ -83,10 +85,10 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.2.tgz#129cc9ae69f93824f92fac653eebfb4812ab4af9" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.2.tgz#129cc9ae69f93824f92fac653eebfb4812ab4af9"
integrity sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw== integrity sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==
"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": "@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1":
version "1.11.6" version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb"
integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==
dependencies: dependencies:
"@webassemblyjs/helper-numbers" "1.11.6" "@webassemblyjs/helper-numbers" "1.11.6"
"@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
...@@ -101,10 +103,10 @@ ...@@ -101,10 +103,10 @@
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768"
integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==
"@webassemblyjs/helper-buffer@1.11.6": "@webassemblyjs/helper-buffer@1.12.1":
version "1.11.6" version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6"
integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==
"@webassemblyjs/helper-numbers@1.11.6": "@webassemblyjs/helper-numbers@1.11.6":
version "1.11.6" version "1.11.6"
...@@ -120,15 +122,15 @@ ...@@ -120,15 +122,15 @@
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9"
integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==
"@webassemblyjs/helper-wasm-section@1.11.6": "@webassemblyjs/helper-wasm-section@1.12.1":
version "1.11.6" version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf"
integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==
dependencies: dependencies:
"@webassemblyjs/ast" "1.11.6" "@webassemblyjs/ast" "1.12.1"
"@webassemblyjs/helper-buffer" "1.11.6" "@webassemblyjs/helper-buffer" "1.12.1"
"@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
"@webassemblyjs/wasm-gen" "1.11.6" "@webassemblyjs/wasm-gen" "1.12.1"
"@webassemblyjs/ieee754@1.11.6": "@webassemblyjs/ieee754@1.11.6":
version "1.11.6" version "1.11.6"
...@@ -149,59 +151,59 @@ ...@@ -149,59 +151,59 @@
resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a"
integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==
"@webassemblyjs/wasm-edit@^1.11.5": "@webassemblyjs/wasm-edit@^1.12.1":
version "1.11.6" version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b"
integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==
dependencies: dependencies:
"@webassemblyjs/ast" "1.11.6" "@webassemblyjs/ast" "1.12.1"
"@webassemblyjs/helper-buffer" "1.11.6" "@webassemblyjs/helper-buffer" "1.12.1"
"@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
"@webassemblyjs/helper-wasm-section" "1.11.6" "@webassemblyjs/helper-wasm-section" "1.12.1"
"@webassemblyjs/wasm-gen" "1.11.6" "@webassemblyjs/wasm-gen" "1.12.1"
"@webassemblyjs/wasm-opt" "1.11.6" "@webassemblyjs/wasm-opt" "1.12.1"
"@webassemblyjs/wasm-parser" "1.11.6" "@webassemblyjs/wasm-parser" "1.12.1"
"@webassemblyjs/wast-printer" "1.11.6" "@webassemblyjs/wast-printer" "1.12.1"
"@webassemblyjs/wasm-gen@1.11.6": "@webassemblyjs/wasm-gen@1.12.1":
version "1.11.6" version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547"
integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==
dependencies: dependencies:
"@webassemblyjs/ast" "1.11.6" "@webassemblyjs/ast" "1.12.1"
"@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
"@webassemblyjs/ieee754" "1.11.6" "@webassemblyjs/ieee754" "1.11.6"
"@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/leb128" "1.11.6"
"@webassemblyjs/utf8" "1.11.6" "@webassemblyjs/utf8" "1.11.6"
"@webassemblyjs/wasm-opt@1.11.6": "@webassemblyjs/wasm-opt@1.12.1":
version "1.11.6" version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5"
integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==
dependencies: dependencies:
"@webassemblyjs/ast" "1.11.6" "@webassemblyjs/ast" "1.12.1"
"@webassemblyjs/helper-buffer" "1.11.6" "@webassemblyjs/helper-buffer" "1.12.1"
"@webassemblyjs/wasm-gen" "1.11.6" "@webassemblyjs/wasm-gen" "1.12.1"
"@webassemblyjs/wasm-parser" "1.11.6" "@webassemblyjs/wasm-parser" "1.12.1"
"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": "@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1":
version "1.11.6" version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937"
integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==
dependencies: dependencies:
"@webassemblyjs/ast" "1.11.6" "@webassemblyjs/ast" "1.12.1"
"@webassemblyjs/helper-api-error" "1.11.6" "@webassemblyjs/helper-api-error" "1.11.6"
"@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
"@webassemblyjs/ieee754" "1.11.6" "@webassemblyjs/ieee754" "1.11.6"
"@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/leb128" "1.11.6"
"@webassemblyjs/utf8" "1.11.6" "@webassemblyjs/utf8" "1.11.6"
"@webassemblyjs/wast-printer@1.11.6": "@webassemblyjs/wast-printer@1.12.1":
version "1.11.6" version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac"
integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==
dependencies: dependencies:
"@webassemblyjs/ast" "1.11.6" "@webassemblyjs/ast" "1.12.1"
"@xtuc/long" "4.2.2" "@xtuc/long" "4.2.2"
"@webpack-cli/configtest@^2.1.1": "@webpack-cli/configtest@^2.1.1":
...@@ -229,10 +231,10 @@ ...@@ -229,10 +231,10 @@
resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
acorn-import-assertions@^1.9.0: acorn-import-attributes@^1.9.5:
version "1.9.0" version "1.9.5"
resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef"
integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==
acorn@^8.7.1, acorn@^8.8.2: acorn@^8.7.1, acorn@^8.8.2:
version "8.10.0" version "8.10.0"
...@@ -261,32 +263,32 @@ ansi-styles@^4.1.0: ...@@ -261,32 +263,32 @@ ansi-styles@^4.1.0:
dependencies: dependencies:
color-convert "^2.0.1" color-convert "^2.0.1"
braces@^3.0.2: braces@^3.0.3:
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies: dependencies:
fill-range "^7.1.1" fill-range "^7.1.1"
browserslist@^4.14.5: browserslist@^4.21.10:
version "4.21.9" version "4.24.2"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.9.tgz#e11bdd3c313d7e2a9e87e8b4b0c7872b13897635" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580"
integrity sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg== integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==
dependencies: dependencies:
caniuse-lite "^1.0.30001503" caniuse-lite "^1.0.30001669"
electron-to-chromium "^1.4.431" electron-to-chromium "^1.5.41"
node-releases "^2.0.12" node-releases "^2.0.18"
update-browserslist-db "^1.0.11" update-browserslist-db "^1.1.1"
buffer-from@^1.0.0: buffer-from@^1.0.0:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
caniuse-lite@^1.0.30001503: caniuse-lite@^1.0.30001669:
version "1.0.30001517" version "1.0.30001673"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz#90fabae294215c3495807eb24fc809e11dc2f0a8" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001673.tgz#5aa291557af1c71340e809987367410aab7a5a9e"
integrity sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA== integrity sha512-WTrjUCSMp3LYX0nE12ECkV0a+e6LC85E0Auz75555/qr78Oc8YWhEPNfDd6SHdtlCMSzqtuXY0uyEMNRcsKpKw==
chalk@^4.1.0: chalk@^4.1.0:
version "4.1.2" version "4.1.2"
...@@ -366,12 +368,12 @@ dotenv@^16.0.0: ...@@ -366,12 +368,12 @@ dotenv@^16.0.0:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==
electron-to-chromium@^1.4.431: electron-to-chromium@^1.5.41:
version "1.4.467" version "1.5.47"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.467.tgz#b0660bf644baff7eedea33b8c742fb53ec60e3c2" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.47.tgz#ef0751bc19b28be8ee44cd8405309de3bf3b20c7"
integrity sha512-2qI70O+rR4poYeF2grcuS/bCps5KJh6y1jtZMDDEteyKJQrzLOEhFyXCLcHW6DTBjKjWkk26JhWoAi+Ux9A0fg== integrity sha512-zS5Yer0MOYw4rtK2iq43cJagHZ8sXN0jDHDKzB+86gSBSAI4v07S97mcq+Gs2vclAxSh1j7vOAHxSVgduiiuVQ==
enhanced-resolve@^5.0.0, enhanced-resolve@^5.15.0, enhanced-resolve@^5.7.0: enhanced-resolve@^5.0.0, enhanced-resolve@^5.7.0:
version "5.15.0" version "5.15.0"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35"
integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==
...@@ -379,6 +381,14 @@ enhanced-resolve@^5.0.0, enhanced-resolve@^5.15.0, enhanced-resolve@^5.7.0: ...@@ -379,6 +381,14 @@ enhanced-resolve@^5.0.0, enhanced-resolve@^5.15.0, enhanced-resolve@^5.7.0:
graceful-fs "^4.2.4" graceful-fs "^4.2.4"
tapable "^2.2.0" tapable "^2.2.0"
enhanced-resolve@^5.17.1:
version "5.17.1"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15"
integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==
dependencies:
graceful-fs "^4.2.4"
tapable "^2.2.0"
envinfo@^7.7.3: envinfo@^7.7.3:
version "7.10.0" version "7.10.0"
resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.10.0.tgz#55146e3909cc5fe63c22da63fb15b05aeac35b13" resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.10.0.tgz#55146e3909cc5fe63c22da63fb15b05aeac35b13"
...@@ -389,10 +399,10 @@ es-module-lexer@^1.2.1: ...@@ -389,10 +399,10 @@ es-module-lexer@^1.2.1:
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.0.tgz#6be9c9e0b4543a60cd166ff6f8b4e9dae0b0c16f" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.0.tgz#6be9c9e0b4543a60cd166ff6f8b4e9dae0b0c16f"
integrity sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA== integrity sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==
escalade@^3.1.1: escalade@^3.2.0:
version "3.1.1" version "3.2.0"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
eslint-scope@5.1.1: eslint-scope@5.1.1:
version "5.1.1" version "5.1.1"
...@@ -464,7 +474,7 @@ glob-to-regexp@^0.4.1: ...@@ -464,7 +474,7 @@ glob-to-regexp@^0.4.1:
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.9: graceful-fs@^4.1.2, graceful-fs@^4.2.11, graceful-fs@^4.2.4:
version "4.2.11" version "4.2.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" 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== integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
...@@ -577,11 +587,11 @@ merge-stream@^2.0.0: ...@@ -577,11 +587,11 @@ merge-stream@^2.0.0:
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
micromatch@^4.0.0: micromatch@^4.0.0:
version "4.0.5" version "4.0.8"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
dependencies: dependencies:
braces "^3.0.2" braces "^3.0.3"
picomatch "^2.3.1" picomatch "^2.3.1"
mime-db@1.52.0: mime-db@1.52.0:
...@@ -606,10 +616,10 @@ neo-async@^2.6.2: ...@@ -606,10 +616,10 @@ neo-async@^2.6.2:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
node-releases@^2.0.12: node-releases@^2.0.18:
version "2.0.13" version "2.0.18"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f"
integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==
p-limit@^2.2.0: p-limit@^2.2.0:
version "2.3.0" version "2.3.0"
...@@ -645,10 +655,10 @@ path-parse@^1.0.7: ...@@ -645,10 +655,10 @@ path-parse@^1.0.7:
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
picocolors@^1.0.0: picocolors@^1.1.0:
version "1.0.0" version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^2.3.1: picomatch@^2.3.1:
version "2.3.1" version "2.3.1"
...@@ -796,21 +806,21 @@ tapable@^2.1.1, tapable@^2.2.0: ...@@ -796,21 +806,21 @@ tapable@^2.1.1, tapable@^2.2.0:
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
terser-webpack-plugin@^5.3.7: terser-webpack-plugin@^5.3.10:
version "5.3.9" version "5.3.10"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199"
integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA== integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==
dependencies: dependencies:
"@jridgewell/trace-mapping" "^0.3.17" "@jridgewell/trace-mapping" "^0.3.20"
jest-worker "^27.4.5" jest-worker "^27.4.5"
schema-utils "^3.1.1" schema-utils "^3.1.1"
serialize-javascript "^6.0.1" serialize-javascript "^6.0.1"
terser "^5.16.8" terser "^5.26.0"
terser@^5.16.8: terser@^5.26.0:
version "5.19.2" version "5.36.0"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.19.2.tgz#bdb8017a9a4a8de4663a7983f45c506534f9234e" resolved "https://registry.yarnpkg.com/terser/-/terser-5.36.0.tgz#8b0dbed459ac40ff7b4c9fd5a3a2029de105180e"
integrity sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA== integrity sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==
dependencies: dependencies:
"@jridgewell/source-map" "^0.3.3" "@jridgewell/source-map" "^0.3.3"
acorn "^8.8.2" acorn "^8.8.2"
...@@ -867,13 +877,13 @@ type-fest@^2.19.0: ...@@ -867,13 +877,13 @@ type-fest@^2.19.0:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
update-browserslist-db@^1.0.11: update-browserslist-db@^1.1.1:
version "1.0.11" version "1.1.1"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5"
integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==
dependencies: dependencies:
escalade "^3.1.1" escalade "^3.2.0"
picocolors "^1.0.0" picocolors "^1.1.0"
uri-js@^4.2.2: uri-js@^4.2.2:
version "4.4.1" version "4.4.1"
...@@ -882,10 +892,10 @@ uri-js@^4.2.2: ...@@ -882,10 +892,10 @@ uri-js@^4.2.2:
dependencies: dependencies:
punycode "^2.1.0" punycode "^2.1.0"
watchpack@^2.4.0: watchpack@^2.4.1:
version "2.4.0" version "2.4.2"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da"
integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==
dependencies: dependencies:
glob-to-regexp "^0.4.1" glob-to-regexp "^0.4.1"
graceful-fs "^4.1.2" graceful-fs "^4.1.2"
...@@ -923,33 +933,32 @@ webpack-sources@^3.2.3: ...@@ -923,33 +933,32 @@ webpack-sources@^3.2.3:
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
webpack@^5.88.2: webpack@^5.88.2:
version "5.88.2" version "5.95.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.88.2.tgz#f62b4b842f1c6ff580f3fcb2ed4f0b579f4c210e" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.95.0.tgz#8fd8c454fa60dad186fbe36c400a55848307b4c0"
integrity sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ== integrity sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==
dependencies: dependencies:
"@types/eslint-scope" "^3.7.3" "@types/estree" "^1.0.5"
"@types/estree" "^1.0.0" "@webassemblyjs/ast" "^1.12.1"
"@webassemblyjs/ast" "^1.11.5" "@webassemblyjs/wasm-edit" "^1.12.1"
"@webassemblyjs/wasm-edit" "^1.11.5" "@webassemblyjs/wasm-parser" "^1.12.1"
"@webassemblyjs/wasm-parser" "^1.11.5"
acorn "^8.7.1" acorn "^8.7.1"
acorn-import-assertions "^1.9.0" acorn-import-attributes "^1.9.5"
browserslist "^4.14.5" browserslist "^4.21.10"
chrome-trace-event "^1.0.2" chrome-trace-event "^1.0.2"
enhanced-resolve "^5.15.0" enhanced-resolve "^5.17.1"
es-module-lexer "^1.2.1" es-module-lexer "^1.2.1"
eslint-scope "5.1.1" eslint-scope "5.1.1"
events "^3.2.0" events "^3.2.0"
glob-to-regexp "^0.4.1" glob-to-regexp "^0.4.1"
graceful-fs "^4.2.9" graceful-fs "^4.2.11"
json-parse-even-better-errors "^2.3.1" json-parse-even-better-errors "^2.3.1"
loader-runner "^4.2.0" loader-runner "^4.2.0"
mime-types "^2.1.27" mime-types "^2.1.27"
neo-async "^2.6.2" neo-async "^2.6.2"
schema-utils "^3.2.0" schema-utils "^3.2.0"
tapable "^2.1.1" tapable "^2.1.1"
terser-webpack-plugin "^5.3.7" terser-webpack-plugin "^5.3.10"
watchpack "^2.4.0" watchpack "^2.4.1"
webpack-sources "^3.2.3" webpack-sources "^3.2.3"
which@^2.0.1: which@^2.0.1:
......
...@@ -39,7 +39,15 @@ ...@@ -39,7 +39,15 @@
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": "@jridgewell/trace-mapping@^0.3.20":
version "0.3.25"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0"
integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==
dependencies:
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@jridgewell/trace-mapping@^0.3.9":
version "0.3.19" version "0.3.19"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811"
integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw== integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==
...@@ -68,28 +76,12 @@ ...@@ -68,28 +76,12 @@
"@nodelib/fs.scandir" "2.1.5" "@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0" fastq "^1.6.0"
"@types/eslint-scope@^3.7.3": "@types/estree@^1.0.5":
version "3.7.4" version "1.0.6"
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== integrity "sha1-Yo7/7q4gZKG055946B2Ht+X8e1A= sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="
dependencies:
"@types/eslint" "*"
"@types/estree" "*"
"@types/eslint@*": "@types/json-schema@^7.0.8":
version "8.44.2"
resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.44.2.tgz#0d21c505f98a89b8dd4d37fa162b09da6089199a"
integrity sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==
dependencies:
"@types/estree" "*"
"@types/json-schema" "*"
"@types/estree@*", "@types/estree@^1.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194"
integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==
"@types/json-schema@*", "@types/json-schema@^7.0.8":
version "7.0.12" version "7.0.12"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
...@@ -99,10 +91,10 @@ ...@@ -99,10 +91,10 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.8.tgz#b5dda19adaa473a9bf0ab5cbd8f30ec7d43f5c85" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.8.tgz#b5dda19adaa473a9bf0ab5cbd8f30ec7d43f5c85"
integrity sha512-0mHckf6D2DiIAzh8fM8f3HQCvMKDpK94YQ0DSVkfWTG9BZleYIWudw9cJxX8oCk9bM+vAkDyujDV6dmKHbvQpg== integrity sha512-0mHckf6D2DiIAzh8fM8f3HQCvMKDpK94YQ0DSVkfWTG9BZleYIWudw9cJxX8oCk9bM+vAkDyujDV6dmKHbvQpg==
"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": "@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1":
version "1.11.6" version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb"
integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==
dependencies: dependencies:
"@webassemblyjs/helper-numbers" "1.11.6" "@webassemblyjs/helper-numbers" "1.11.6"
"@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
...@@ -117,10 +109,10 @@ ...@@ -117,10 +109,10 @@
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768"
integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==
"@webassemblyjs/helper-buffer@1.11.6": "@webassemblyjs/helper-buffer@1.12.1":
version "1.11.6" version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6"
integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==
"@webassemblyjs/helper-numbers@1.11.6": "@webassemblyjs/helper-numbers@1.11.6":
version "1.11.6" version "1.11.6"
...@@ -136,15 +128,15 @@ ...@@ -136,15 +128,15 @@
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9"
integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==
"@webassemblyjs/helper-wasm-section@1.11.6": "@webassemblyjs/helper-wasm-section@1.12.1":
version "1.11.6" version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf"
integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==
dependencies: dependencies:
"@webassemblyjs/ast" "1.11.6" "@webassemblyjs/ast" "1.12.1"
"@webassemblyjs/helper-buffer" "1.11.6" "@webassemblyjs/helper-buffer" "1.12.1"
"@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
"@webassemblyjs/wasm-gen" "1.11.6" "@webassemblyjs/wasm-gen" "1.12.1"
"@webassemblyjs/ieee754@1.11.6": "@webassemblyjs/ieee754@1.11.6":
version "1.11.6" version "1.11.6"
...@@ -165,59 +157,59 @@ ...@@ -165,59 +157,59 @@
resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a"
integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==
"@webassemblyjs/wasm-edit@^1.11.5": "@webassemblyjs/wasm-edit@^1.12.1":
version "1.11.6" version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b"
integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==
dependencies: dependencies:
"@webassemblyjs/ast" "1.11.6" "@webassemblyjs/ast" "1.12.1"
"@webassemblyjs/helper-buffer" "1.11.6" "@webassemblyjs/helper-buffer" "1.12.1"
"@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
"@webassemblyjs/helper-wasm-section" "1.11.6" "@webassemblyjs/helper-wasm-section" "1.12.1"
"@webassemblyjs/wasm-gen" "1.11.6" "@webassemblyjs/wasm-gen" "1.12.1"
"@webassemblyjs/wasm-opt" "1.11.6" "@webassemblyjs/wasm-opt" "1.12.1"
"@webassemblyjs/wasm-parser" "1.11.6" "@webassemblyjs/wasm-parser" "1.12.1"
"@webassemblyjs/wast-printer" "1.11.6" "@webassemblyjs/wast-printer" "1.12.1"
"@webassemblyjs/wasm-gen@1.11.6": "@webassemblyjs/wasm-gen@1.12.1":
version "1.11.6" version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547"
integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==
dependencies: dependencies:
"@webassemblyjs/ast" "1.11.6" "@webassemblyjs/ast" "1.12.1"
"@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
"@webassemblyjs/ieee754" "1.11.6" "@webassemblyjs/ieee754" "1.11.6"
"@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/leb128" "1.11.6"
"@webassemblyjs/utf8" "1.11.6" "@webassemblyjs/utf8" "1.11.6"
"@webassemblyjs/wasm-opt@1.11.6": "@webassemblyjs/wasm-opt@1.12.1":
version "1.11.6" version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5"
integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==
dependencies: dependencies:
"@webassemblyjs/ast" "1.11.6" "@webassemblyjs/ast" "1.12.1"
"@webassemblyjs/helper-buffer" "1.11.6" "@webassemblyjs/helper-buffer" "1.12.1"
"@webassemblyjs/wasm-gen" "1.11.6" "@webassemblyjs/wasm-gen" "1.12.1"
"@webassemblyjs/wasm-parser" "1.11.6" "@webassemblyjs/wasm-parser" "1.12.1"
"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": "@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1":
version "1.11.6" version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937"
integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==
dependencies: dependencies:
"@webassemblyjs/ast" "1.11.6" "@webassemblyjs/ast" "1.12.1"
"@webassemblyjs/helper-api-error" "1.11.6" "@webassemblyjs/helper-api-error" "1.11.6"
"@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
"@webassemblyjs/ieee754" "1.11.6" "@webassemblyjs/ieee754" "1.11.6"
"@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/leb128" "1.11.6"
"@webassemblyjs/utf8" "1.11.6" "@webassemblyjs/utf8" "1.11.6"
"@webassemblyjs/wast-printer@1.11.6": "@webassemblyjs/wast-printer@1.12.1":
version "1.11.6" version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac"
integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==
dependencies: dependencies:
"@webassemblyjs/ast" "1.11.6" "@webassemblyjs/ast" "1.12.1"
"@xtuc/long" "4.2.2" "@xtuc/long" "4.2.2"
"@webpack-cli/configtest@^2.1.1": "@webpack-cli/configtest@^2.1.1":
...@@ -245,10 +237,10 @@ ...@@ -245,10 +237,10 @@
resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
acorn-import-assertions@^1.9.0: acorn-import-attributes@^1.9.5:
version "1.9.0" version "1.9.5"
resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef"
integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==
acorn@^8.7.1, acorn@^8.8.2: acorn@^8.7.1, acorn@^8.8.2:
version "8.10.0" version "8.10.0"
...@@ -288,32 +280,32 @@ binary-extensions@^2.0.0: ...@@ -288,32 +280,32 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
braces@^3.0.2, braces@~3.0.2: braces@^3.0.3, braces@~3.0.2:
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies: dependencies:
fill-range "^7.1.1" fill-range "^7.1.1"
browserslist@^4.14.5: browserslist@^4.21.10:
version "4.21.10" version "4.24.2"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580"
integrity sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ== integrity "sha1-9YRbyRBp29Ve6J+vmCLh2IXRZYA= sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg=="
dependencies: dependencies:
caniuse-lite "^1.0.30001517" caniuse-lite "^1.0.30001669"
electron-to-chromium "^1.4.477" electron-to-chromium "^1.5.41"
node-releases "^2.0.13" node-releases "^2.0.18"
update-browserslist-db "^1.0.11" update-browserslist-db "^1.1.1"
buffer-from@^1.0.0: buffer-from@^1.0.0:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
caniuse-lite@^1.0.30001517: caniuse-lite@^1.0.30001669:
version "1.0.30001519" version "1.0.30001673"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001519.tgz#3e7b8b8a7077e78b0eb054d69e6edf5c7df35601" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001673.tgz#5aa291557af1c71340e809987367410aab7a5a9e"
integrity sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg== integrity "sha1-WqKRVXrxxxNA6AmYc2dBCqt6Wp4= sha512-WTrjUCSMp3LYX0nE12ECkV0a+e6LC85E0Auz75555/qr78Oc8YWhEPNfDd6SHdtlCMSzqtuXY0uyEMNRcsKpKw=="
chokidar@^3.5.3: chokidar@^3.5.3:
version "3.5.3" version "3.5.3"
...@@ -400,15 +392,15 @@ dotenv@^16.0.0: ...@@ -400,15 +392,15 @@ dotenv@^16.0.0:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==
electron-to-chromium@^1.4.477: electron-to-chromium@^1.5.41:
version "1.4.487" version "1.5.47"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.487.tgz#e2ef8b15f2791bf68fa6f38f2656f1a551d360ae" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.47.tgz#ef0751bc19b28be8ee44cd8405309de3bf3b20c7"
integrity sha512-XbCRs/34l31np/p33m+5tdBrdXu9jJkZxSbNxj5I0H1KtV2ZMSB+i/HYqDiRzHaFx2T5EdytjoBRe8QRJE2vQg== integrity "sha1-7wdRvBmyi+juRM2EBTCd4787IMc= sha512-zS5Yer0MOYw4rtK2iq43cJagHZ8sXN0jDHDKzB+86gSBSAI4v07S97mcq+Gs2vclAxSh1j7vOAHxSVgduiiuVQ=="
enhanced-resolve@^5.15.0: enhanced-resolve@^5.17.1:
version "5.15.0" version "5.17.1"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15"
integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==
dependencies: dependencies:
graceful-fs "^4.2.4" graceful-fs "^4.2.4"
tapable "^2.2.0" tapable "^2.2.0"
...@@ -423,10 +415,10 @@ es-module-lexer@^1.2.1: ...@@ -423,10 +415,10 @@ es-module-lexer@^1.2.1:
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.0.tgz#6be9c9e0b4543a60cd166ff6f8b4e9dae0b0c16f" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.0.tgz#6be9c9e0b4543a60cd166ff6f8b4e9dae0b0c16f"
integrity sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA== integrity sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==
escalade@^3.1.1: escalade@^3.2.0:
version "3.1.1" version "3.2.0"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
eslint-scope@5.1.1: eslint-scope@5.1.1:
version "5.1.1" version "5.1.1"
...@@ -540,7 +532,7 @@ globby@^11.0.4: ...@@ -540,7 +532,7 @@ globby@^11.0.4:
merge2 "^1.4.1" merge2 "^1.4.1"
slash "^3.0.0" slash "^3.0.0"
graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.9: graceful-fs@^4.1.2, graceful-fs@^4.2.11, graceful-fs@^4.2.4:
version "4.2.11" version "4.2.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" 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== integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
...@@ -670,11 +662,11 @@ merge2@^1.3.0, merge2@^1.4.1: ...@@ -670,11 +662,11 @@ merge2@^1.3.0, merge2@^1.4.1:
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
micromatch@^4.0.4: micromatch@^4.0.4:
version "4.0.5" version "4.0.8"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
dependencies: dependencies:
braces "^3.0.2" braces "^3.0.3"
picomatch "^2.3.1" picomatch "^2.3.1"
mime-db@1.52.0: mime-db@1.52.0:
...@@ -704,10 +696,10 @@ neo-async@^2.6.2: ...@@ -704,10 +696,10 @@ neo-async@^2.6.2:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
node-releases@^2.0.13: node-releases@^2.0.18:
version "2.0.13" version "2.0.18"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f"
integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==
normalize-path@^3.0.0, normalize-path@~3.0.0: normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0" version "3.0.0"
...@@ -753,10 +745,10 @@ path-type@^4.0.0: ...@@ -753,10 +745,10 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
picocolors@^1.0.0: picocolors@^1.1.0:
version "1.0.0" version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== integrity "sha1-PTIa8+q5ObCDyPkpodEs2oHCa2s= sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
version "2.3.1" version "2.3.1"
...@@ -921,21 +913,21 @@ tapable@^2.1.1, tapable@^2.2.0: ...@@ -921,21 +913,21 @@ tapable@^2.1.1, tapable@^2.2.0:
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
terser-webpack-plugin@^5.3.7: terser-webpack-plugin@^5.3.10:
version "5.3.9" version "5.3.10"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199"
integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA== integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==
dependencies: dependencies:
"@jridgewell/trace-mapping" "^0.3.17" "@jridgewell/trace-mapping" "^0.3.20"
jest-worker "^27.4.5" jest-worker "^27.4.5"
schema-utils "^3.1.1" schema-utils "^3.1.1"
serialize-javascript "^6.0.1" serialize-javascript "^6.0.1"
terser "^5.16.8" terser "^5.26.0"
terser@^5.16.8: terser@^5.26.0:
version "5.19.2" version "5.36.0"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.19.2.tgz#bdb8017a9a4a8de4663a7983f45c506534f9234e" resolved "https://registry.yarnpkg.com/terser/-/terser-5.36.0.tgz#8b0dbed459ac40ff7b4c9fd5a3a2029de105180e"
integrity sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA== integrity "sha1-iw2+1FmsQP97TJ/Vo6ICneEFGA4= sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w=="
dependencies: dependencies:
"@jridgewell/source-map" "^0.3.3" "@jridgewell/source-map" "^0.3.3"
acorn "^8.8.2" acorn "^8.8.2"
...@@ -971,13 +963,13 @@ typescript@5.1: ...@@ -971,13 +963,13 @@ typescript@5.1:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274"
integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==
update-browserslist-db@^1.0.11: update-browserslist-db@^1.1.1:
version "1.0.11" version "1.1.1"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5"
integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== integrity "sha1-gIRvuh156CVH+2YfjRQeCUV1X+U= sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A=="
dependencies: dependencies:
escalade "^3.1.1" escalade "^3.2.0"
picocolors "^1.0.0" picocolors "^1.1.0"
uri-js@^4.2.2: uri-js@^4.2.2:
version "4.4.1" version "4.4.1"
...@@ -986,10 +978,10 @@ uri-js@^4.2.2: ...@@ -986,10 +978,10 @@ uri-js@^4.2.2:
dependencies: dependencies:
punycode "^2.1.0" punycode "^2.1.0"
watchpack@^2.4.0: watchpack@^2.4.1:
version "2.4.0" version "2.4.2"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da"
integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== integrity "sha1-L+6u1nQS58MxhOWnnKc4+9OFZNo= sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw=="
dependencies: dependencies:
glob-to-regexp "^0.4.1" glob-to-regexp "^0.4.1"
graceful-fs "^4.1.2" graceful-fs "^4.1.2"
...@@ -1027,33 +1019,32 @@ webpack-sources@^3.2.3: ...@@ -1027,33 +1019,32 @@ webpack-sources@^3.2.3:
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
webpack@^5.88.2: webpack@^5.88.2:
version "5.88.2" version "5.95.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.88.2.tgz#f62b4b842f1c6ff580f3fcb2ed4f0b579f4c210e" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.95.0.tgz#8fd8c454fa60dad186fbe36c400a55848307b4c0"
integrity sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ== integrity "sha1-j9jEVPpg2tGG++NsQApVhIMHtMA= sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q=="
dependencies: dependencies:
"@types/eslint-scope" "^3.7.3" "@types/estree" "^1.0.5"
"@types/estree" "^1.0.0" "@webassemblyjs/ast" "^1.12.1"
"@webassemblyjs/ast" "^1.11.5" "@webassemblyjs/wasm-edit" "^1.12.1"
"@webassemblyjs/wasm-edit" "^1.11.5" "@webassemblyjs/wasm-parser" "^1.12.1"
"@webassemblyjs/wasm-parser" "^1.11.5"
acorn "^8.7.1" acorn "^8.7.1"
acorn-import-assertions "^1.9.0" acorn-import-attributes "^1.9.5"
browserslist "^4.14.5" browserslist "^4.21.10"
chrome-trace-event "^1.0.2" chrome-trace-event "^1.0.2"
enhanced-resolve "^5.15.0" enhanced-resolve "^5.17.1"
es-module-lexer "^1.2.1" es-module-lexer "^1.2.1"
eslint-scope "5.1.1" eslint-scope "5.1.1"
events "^3.2.0" events "^3.2.0"
glob-to-regexp "^0.4.1" glob-to-regexp "^0.4.1"
graceful-fs "^4.2.9" graceful-fs "^4.2.11"
json-parse-even-better-errors "^2.3.1" json-parse-even-better-errors "^2.3.1"
loader-runner "^4.2.0" loader-runner "^4.2.0"
mime-types "^2.1.27" mime-types "^2.1.27"
neo-async "^2.6.2" neo-async "^2.6.2"
schema-utils "^3.2.0" schema-utils "^3.2.0"
tapable "^2.1.1" tapable "^2.1.1"
terser-webpack-plugin "^5.3.7" terser-webpack-plugin "^5.3.10"
watchpack "^2.4.0" watchpack "^2.4.1"
webpack-sources "^3.2.3" webpack-sources "^3.2.3"
which@^2.0.1: which@^2.0.1:
......
...@@ -220,6 +220,7 @@ Settings for meta tags, OG tags and SEO ...@@ -220,6 +220,7 @@ Settings for meta tags, OG tags and SEO
##### Block fields list ##### Block fields list
| Id | Description | | Id | Description |
| --- | --- | | --- | --- |
| `base_fee` | Base fee |
| `burnt_fees` | Burnt fees | | `burnt_fees` | Burnt fees |
| `total_reward` | Total block reward | | `total_reward` | Total block reward |
| `nonce` | Block nonce | | `nonce` | Block nonce |
...@@ -748,7 +749,7 @@ If the feature is enabled, a Multichain balance button will be displayed on the ...@@ -748,7 +749,7 @@ If the feature is enabled, a Multichain balance button will be displayed on the
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | | Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG | `{ name: string; url_template: string; dapp_id?: string; logo?: string }` | Multichain portfolio application config See [below](#multichain-button-configuration-properties) | - | - | `{ name: 'zerion', url_template: 'https://app.zerion.io/{address}/overview', logo: 'https://example.com/icon.svg'` | v1.31.0+ | | NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG | `[{ name: string; url_template: string; dapp_id?: string; logo: string }]` | Multichain portfolio application config See [below](#multichain-button-configuration-properties) | - | - | `[{ name: 'zerion', url_template: 'https://app.zerion.io/{address}/overview', logo: 'https://example.com/icon.svg'}]` | v1.31.0+ |
&nbsp; &nbsp;
......
...@@ -67,6 +67,8 @@ import type { ...@@ -67,6 +67,8 @@ import type {
SmartContract, SmartContract,
SmartContractVerificationConfigRaw, SmartContractVerificationConfigRaw,
SmartContractSecurityAudits, SmartContractSecurityAudits,
SmartContractMudSystemsResponse,
SmartContractMudSystemInfo,
} from 'types/api/contract'; } from 'types/api/contract';
import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts'; import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts';
import type { import type {
...@@ -849,6 +851,16 @@ export const RESOURCES = { ...@@ -849,6 +851,16 @@ export const RESOURCES = {
pathParams: [ 'hash' as const, 'table_id' as const, 'record_id' as const ], pathParams: [ 'hash' as const, 'table_id' as const, 'record_id' as const ],
}, },
contract_mud_systems: {
path: '/api/v2/mud/worlds/:hash/systems',
pathParams: [ 'hash' as const ],
},
contract_mud_system_info: {
path: '/api/v2/mud/worlds/:hash/systems/:system_address',
pathParams: [ 'hash' as const, 'system_address' as const ],
},
// arbitrum L2 // arbitrum L2
arbitrum_l2_messages: { arbitrum_l2_messages: {
path: '/api/v2/arbitrum/messages/:direction', path: '/api/v2/arbitrum/messages/:direction',
...@@ -1277,6 +1289,8 @@ Q extends 'address_mud_tables' ? AddressMudTables : ...@@ -1277,6 +1289,8 @@ Q extends 'address_mud_tables' ? AddressMudTables :
Q extends 'address_mud_tables_count' ? number : Q extends 'address_mud_tables_count' ? number :
Q extends 'address_mud_records' ? AddressMudRecords : Q extends 'address_mud_records' ? AddressMudRecords :
Q extends 'address_mud_record' ? AddressMudRecord : Q extends 'address_mud_record' ? AddressMudRecord :
Q extends 'contract_mud_systems' ? SmartContractMudSystemsResponse :
Q extends 'contract_mud_system_info' ? SmartContractMudSystemInfo :
Q extends 'address_epoch_rewards' ? AddressEpochRewardsResponse : Q extends 'address_epoch_rewards' ? AddressEpochRewardsResponse :
Q extends 'withdrawals' ? WithdrawalsResponse : Q extends 'withdrawals' ? WithdrawalsResponse :
Q extends 'withdrawals_counters' ? WithdrawalsCounters : Q extends 'withdrawals_counters' ? WithdrawalsCounters :
......
export const validator = (value: string | undefined) => {
if (!value) {
return true;
}
try {
new URL(value);
return true;
} catch (error) {
return 'Incorrect URL';
}
};
import type { SmartContract } from 'types/api/contract'; import type { SmartContract, SmartContractMudSystemsResponse } from 'types/api/contract';
import type { VerifiedContract, VerifiedContractsCounters } from 'types/api/contracts'; import type { VerifiedContract, VerifiedContractsCounters } from 'types/api/contracts';
import type { SolidityScanReport } from 'lib/solidityScan/schema'; import type { SolidityScanReport } from 'lib/solidityScan/schema';
import { ADDRESS_PARAMS } from './addressParams'; import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams';
export const CONTRACT_CODE_UNVERIFIED = { export const CONTRACT_CODE_UNVERIFIED = {
creation_bytecode: '0x60806040526e', creation_bytecode: '0x60806040526e',
...@@ -46,6 +46,7 @@ export const CONTRACT_CODE_VERIFIED = { ...@@ -46,6 +46,7 @@ export const CONTRACT_CODE_VERIFIED = {
remappings: [], remappings: [],
}, },
compiler_version: 'v0.8.7+commit.e28d00a7', compiler_version: 'v0.8.7+commit.e28d00a7',
constructor_args: '0000000000000000000000005c7bcd6e7de5423a257d81b442095a1a6ced35c5000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
creation_bytecode: '0x6080604052348', creation_bytecode: '0x6080604052348',
deployed_bytecode: '0x60806040', deployed_bytecode: '0x60806040',
evm_version: 'london', evm_version: 'london',
...@@ -98,3 +99,12 @@ export const SOLIDITY_SCAN_REPORT: SolidityScanReport = { ...@@ -98,3 +99,12 @@ export const SOLIDITY_SCAN_REPORT: SolidityScanReport = {
scanner_reference_url: 'https://solidityscan.com/quickscan/0xc1EF7811FF2ebFB74F80ed7423f2AdAA37454be2/blockscout/eth-goerli?ref=blockscout', scanner_reference_url: 'https://solidityscan.com/quickscan/0xc1EF7811FF2ebFB74F80ed7423f2AdAA37454be2/blockscout/eth-goerli?ref=blockscout',
}, },
}; };
export const MUD_SYSTEMS: SmartContractMudSystemsResponse = {
items: [
{
name: 'sy.AccessManagement',
address: ADDRESS_HASH,
},
],
};
...@@ -15,6 +15,7 @@ test.use({ viewport: { width: 150, height: 350 } }); ...@@ -15,6 +15,7 @@ test.use({ viewport: { width: 150, height: 350 } });
{ variant: 'subtle', colorScheme: 'gray', states: [ 'default', 'hovered' ], withDarkMode: true }, { variant: 'subtle', colorScheme: 'gray', states: [ 'default', 'hovered' ], withDarkMode: true },
{ variant: 'hero', states: [ 'default', 'hovered' ], withDarkMode: true }, { variant: 'hero', states: [ 'default', 'hovered' ], withDarkMode: true },
{ variant: 'header', states: [ 'default', 'hovered', 'selected' ], withDarkMode: true }, { variant: 'header', states: [ 'default', 'hovered', 'selected' ], withDarkMode: true },
{ variant: 'radio_group', states: [ 'default', 'hovered', 'selected' ], withDarkMode: true },
].forEach(({ variant, colorScheme, withDarkMode, states }) => { ].forEach(({ variant, colorScheme, withDarkMode, states }) => {
test.describe(`variant ${ variant }${ colorScheme ? ` with ${ colorScheme } color scheme` : '' }${ withDarkMode ? ' +@dark-mode' : '' }`, () => { test.describe(`variant ${ variant }${ colorScheme ? ` with ${ colorScheme } color scheme` : '' }${ withDarkMode ? ' +@dark-mode' : '' }`, () => {
test('', async({ render }) => { test('', async({ render }) => {
......
...@@ -94,6 +94,42 @@ const variantOutline = defineStyle((props) => { ...@@ -94,6 +94,42 @@ const variantOutline = defineStyle((props) => {
}; };
}); });
const variantRadioGroup = defineStyle((props) => {
const outline = runIfFn(variantOutline, props);
const bgColor = mode('blue.50', 'gray.800')(props);
const selectedTextColor = mode('blue.700', 'gray.50')(props);
return {
...outline,
fontWeight: 500,
cursor: 'pointer',
bgColor: 'none',
borderColor: bgColor,
_hover: {
borderColor: bgColor,
color: 'link_hovered',
},
_active: {
bgColor: 'none',
},
// We have a special state for this button variant that serves as a popover trigger.
// When any items (filters) are selected in the popover, the button should change its background and text color.
// The last CSS selector is for redefining styles for the TabList component.
[`
&[data-selected=true],
&[data-selected=true][aria-selected=true]
`]: {
cursor: 'initial',
bgColor,
borderColor: bgColor,
color: selectedTextColor,
_hover: {
color: selectedTextColor,
},
},
};
});
const variantSimple = defineStyle((props) => { const variantSimple = defineStyle((props) => {
const outline = runIfFn(variantOutline, props); const outline = runIfFn(variantOutline, props);
...@@ -223,6 +259,7 @@ const variants = { ...@@ -223,6 +259,7 @@ const variants = {
subtle: variantSubtle, subtle: variantSubtle,
hero: variantHero, hero: variantHero,
header: variantHeader, header: variantHeader,
radio_group: variantRadioGroup,
}; };
const baseStyle = defineStyle({ const baseStyle = defineStyle({
......
...@@ -41,6 +41,33 @@ const variantOutline = definePartsStyle((props) => { ...@@ -41,6 +41,33 @@ const variantOutline = definePartsStyle((props) => {
}; };
}); });
const variantRadioGroup = definePartsStyle((props) => {
return {
tab: {
...Button.baseStyle,
...Button.variants?.radio_group(props),
_selected: Button.variants?.radio_group(props)?.[`
&[data-selected=true],
&[data-selected=true][aria-selected=true]
`],
borderRadius: 'none',
_notFirst: {
borderLeftWidth: 0,
},
'&[role="tab"]': {
_first: {
borderTopLeftRadius: 'base',
borderBottomLeftRadius: 'base',
},
_last: {
borderTopRightRadius: 'base',
borderBottomRightRadius: 'base',
},
},
},
};
});
const sizes = { const sizes = {
sm: definePartsStyle({ sm: definePartsStyle({
tab: Button.sizes?.sm, tab: Button.sizes?.sm,
...@@ -53,6 +80,7 @@ const sizes = { ...@@ -53,6 +80,7 @@ const sizes = {
const variants = { const variants = {
'soft-rounded': variantSoftRounded, 'soft-rounded': variantSoftRounded,
outline: variantOutline, outline: variantOutline,
radio_group: variantRadioGroup,
}; };
const Tabs = defineMultiStyleConfig({ const Tabs = defineMultiStyleConfig({
......
...@@ -143,3 +143,19 @@ export type SmartContractSecurityAuditSubmission = { ...@@ -143,3 +143,19 @@ export type SmartContractSecurityAuditSubmission = {
'audit_publish_date': string; 'audit_publish_date': string;
'comment'?: string; 'comment'?: string;
} }
// MUD SYSTEM
export interface SmartContractMudSystemsResponse {
items: Array<SmartContractMudSystemItem>;
}
export interface SmartContractMudSystemItem {
address: string;
name: string;
}
export interface SmartContractMudSystemInfo {
name: string;
abi: Abi;
}
...@@ -2,5 +2,12 @@ export type MultichainProviderConfig = { ...@@ -2,5 +2,12 @@ export type MultichainProviderConfig = {
name: string; name: string;
dapp_id?: string; dapp_id?: string;
url_template: string; url_template: string;
logo?: string; logo: string;
};
export type MultichainProviderConfigParsed = {
name: string;
logoUrl: string;
urlTemplate: string;
dappId?: string;
}; };
...@@ -17,3 +17,6 @@ export type PickByType<T, X> = Record< ...@@ -17,3 +17,6 @@ export type PickByType<T, X> = Record<
{[K in keyof T]: T[K] extends X ? K : never}[keyof T], {[K in keyof T]: T[K] extends X ? K : never}[keyof T],
X X
>; >;
// Make some properties of an object optional
export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
import type { ArrayElement } from 'types/utils'; import type { ArrayElement } from 'types/utils';
export const BLOCK_FIELDS_IDS = [ export const BLOCK_FIELDS_IDS = [
'base_fee',
'burnt_fees', 'burnt_fees',
'total_reward', 'total_reward',
'nonce', 'nonce',
......
...@@ -2,8 +2,8 @@ import { useRouter } from 'next/router'; ...@@ -2,8 +2,8 @@ import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useContractTabs from 'lib/hooks/useContractTabs';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import useContractTabs from 'ui/address/contract/useContractTabs';
import AddressContract from './AddressContract'; import AddressContract from './AddressContract';
......
import { Flex, Skeleton, Button, Grid, GridItem, Alert, chakra, Box, useColorModeValue } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useQueryClient } from '@tanstack/react-query';
import type { Channel } from 'phoenix';
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 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 useSocketMessage from 'lib/socket/useSocketMessage';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
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/links/LinkExternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import RawDataSnippet from 'ui/shared/RawDataSnippet';
import ContractCodeProxyPattern from './ContractCodeProxyPattern';
import ContractSecurityAudits from './ContractSecurityAudits';
import ContractSourceCode from './ContractSourceCode';
type Props = {
addressHash?: string;
contractQuery: UseQueryResult<SmartContract, ResourceError<unknown>>;
channel: Channel | undefined;
}
type InfoItemProps = {
label: string;
content: string | React.ReactNode;
className?: string;
isLoading: boolean;
hint?: string;
}
const InfoItem = chakra(({ label, content, hint, className, isLoading }: InfoItemProps) => (
<GridItem display="flex" columnGap={ 6 } wordBreak="break-all" className={ className } alignItems="baseline">
<Skeleton isLoaded={ !isLoading } w="170px" flexShrink={ 0 } fontWeight={ 500 }>
<Flex alignItems="center">
{ label }
{ hint && (
<Hint
label={ hint }
ml={ 2 }
color={ useColorModeValue('gray.600', 'gray.400') }
tooltipProps={{ placement: 'bottom' }}
/>
) }
</Flex>
</Skeleton>
<Skeleton isLoaded={ !isLoading }>{ content }</Skeleton>
</GridItem>
));
const rollupFeature = config.features.rollup;
const ContractCode = ({ addressHash, contractQuery, channel }: Props) => {
const [ isChangedBytecodeSocket, setIsChangedBytecodeSocket ] = React.useState<boolean>();
const queryClient = useQueryClient();
const addressInfo = queryClient.getQueryData<AddressInfo>(getResourceKey('address', { pathParams: { hash: addressHash } }));
const { data, isPlaceholderData, isError } = contractQuery;
const handleChangedBytecodeMessage: SocketMessage.AddressChangedBytecode['handler'] = React.useCallback(() => {
setIsChangedBytecodeSocket(true);
}, [ ]);
const handleContractWasVerifiedMessage: SocketMessage.SmartContractWasVerified['handler'] = React.useCallback(() => {
queryClient.refetchQueries({
queryKey: getResourceKey('address', { pathParams: { hash: addressHash } }),
});
queryClient.refetchQueries({
queryKey: getResourceKey('contract', { pathParams: { hash: addressHash } }),
});
}, [ addressHash, queryClient ]);
useSocketMessage({
channel,
event: 'changed_bytecode',
handler: handleChangedBytecodeMessage,
});
useSocketMessage({
channel,
event: 'smart_contract_was_verified',
handler: handleContractWasVerifiedMessage,
});
if (isError) {
return <DataFetchAlert/>;
}
const canBeVerified = !data?.is_self_destructed && !data?.is_verified;
const verificationButton = isPlaceholderData ? (
<Skeleton
w="130px"
h={ 8 }
mr={ data?.is_partially_verified ? 0 : 3 }
ml={ data?.is_partially_verified ? 0 : 'auto' }
borderRadius="base"
flexShrink={ 0 }
/>
) : (
<Button
size="sm"
mr={ data?.is_partially_verified ? 0 : 3 }
ml={ data?.is_partially_verified ? 0 : 'auto' }
flexShrink={ 0 }
as="a"
href={ route({ pathname: '/address/[hash]/contract-verification', query: { hash: addressHash || '' } }) }
>
Verify & publish
</Button>
);
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 (
<LinkExternal href={ license.url }>
{ license.label }
</LinkExternal>
);
})();
const constructorArgs = (() => {
if (!data?.decoded_constructor_args) {
return data?.constructor_args;
}
const decoded = data.decoded_constructor_args
.map(([ value, { name, type } ], index) => {
const valueEl = type === 'address' ? (
<AddressEntity
address={{ hash: value }}
noIcon
display="inline-flex"
maxW="100%"
/>
) : <span>{ value }</span>;
return (
<Box key={ index }>
<span>Arg [{ index }] { name || '' } ({ type }): </span>
{ valueEl }
</Box>
);
});
return (
<>
<span>{ data.constructor_args }</span>
<br/><br/>
{ decoded }
</>
);
})();
const verificationAlert = (() => {
if (data?.is_verified_via_eth_bytecode_db) {
return (
<Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap">
<span>This contract has been { data.is_partially_verified ? 'partially ' : '' }verified using </span>
<LinkExternal
href="https://docs.blockscout.com/about/features/ethereum-bytecode-database-microservice"
fontSize="md"
>
Blockscout Bytecode Database
</LinkExternal>
</Alert>
);
}
if (data?.is_verified_via_sourcify) {
return (
<Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap">
<span>This contract has been { data.is_partially_verified ? 'partially ' : '' }verified via Sourcify. </span>
{ data.sourcify_repo_url && <LinkExternal href={ data.sourcify_repo_url } fontSize="md">View contract in Sourcify repository</LinkExternal> }
</Alert>
);
}
return null;
})();
const contractNameWithCertifiedIcon = data?.is_verified ? (
<Flex alignItems="center">
{ data.name }
{ data.certified && <ContractCertifiedLabel iconSize={ 5 } boxSize={ 5 } ml={ 2 }/> }
</Flex>
) : null;
return (
<>
<Flex flexDir="column" rowGap={ 2 } mb={ 6 } _empty={{ display: 'none' }}>
{ data?.is_blueprint && (
<Box>
<span>This is an </span>
<LinkExternal href="https://eips.ethereum.org/EIPS/eip-5202">
ERC-5202 Blueprint contract
</LinkExternal>
</Box>
) }
{ data?.is_verified && (
<Skeleton isLoaded={ !isPlaceholderData }>
<Alert status="success" flexWrap="wrap" rowGap={ 3 } columnGap={ 5 }>
<span>Contract Source Code Verified ({ data.is_partially_verified ? 'Partial' : 'Exact' } Match)</span>
{ data.is_partially_verified ? verificationButton : null }
</Alert>
</Skeleton>
) }
{ verificationAlert }
{ (data?.is_changed_bytecode || isChangedBytecodeSocket) && (
<Alert status="warning">
Warning! Contract bytecode has been changed and does not match the verified one. Therefore, interaction with this smart contract may be risky.
</Alert>
) }
{ !data?.is_verified && data?.verified_twin_address_hash && (!data?.proxy_type || data.proxy_type === 'unknown') && (
<Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap">
<span>Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB </span>
<AddressEntity
address={{ hash: data.verified_twin_address_hash, is_contract: true }}
truncation="constant"
fontSize="sm"
fontWeight="500"
/>
<chakra.span mt={ 1 }>All functions displayed below are from ABI of that contract. In order to verify current contract, proceed with </chakra.span>
<LinkInternal href={ route({ pathname: '/address/[hash]/contract-verification', query: { hash: addressHash || '' } }) }>
Verify & Publish
</LinkInternal>
<span> page</span>
</Alert>
) }
{ data?.proxy_type && <ContractCodeProxyPattern type={ data.proxy_type }/> }
</Flex>
{ data?.is_verified && (
<Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 6 } mb={ 8 }>
{ data.name && <InfoItem label="Contract name" content={ contractNameWithCertifiedIcon } isLoading={ isPlaceholderData }/> }
{ data.compiler_version && <InfoItem label="Compiler version" content={ data.compiler_version } isLoading={ isPlaceholderData }/> }
{ data.zk_compiler_version && <InfoItem label="ZK compiler version" content={ data.zk_compiler_version } isLoading={ isPlaceholderData }/> }
{ data.evm_version && <InfoItem label="EVM version" content={ data.evm_version } textTransform="capitalize" isLoading={ isPlaceholderData }/> }
{ licenseLink && (
<InfoItem
label="License"
content={ licenseLink }
hint="License type is entered manually during verification. The initial source code may contain a different license type than the one displayed."
isLoading={ isPlaceholderData }
/>
) }
{ typeof data.optimization_enabled === 'boolean' &&
<InfoItem label="Optimization enabled" content={ data.optimization_enabled ? 'true' : 'false' } isLoading={ isPlaceholderData }/> }
{ data.optimization_runs !== null && (
<InfoItem
label={ rollupFeature.isEnabled && rollupFeature.type === 'zkSync' ? 'Optimization mode' : 'Optimization runs' }
content={ String(data.optimization_runs) }
isLoading={ isPlaceholderData }
/>
) }
{ data.verified_at &&
<InfoItem label="Verified at" content={ dayjs(data.verified_at).format('llll') } wordBreak="break-word" isLoading={ isPlaceholderData }/> }
{ data.file_path && <InfoItem label="Contract file path" content={ data.file_path } wordBreak="break-word" isLoading={ isPlaceholderData }/> }
{ config.UI.hasContractAuditReports && (
<InfoItem
label="Security audit"
content={ <ContractSecurityAudits addressHash={ addressHash }/> }
isLoading={ isPlaceholderData }
/>
) }
</Grid>
) }
<Flex flexDir="column" rowGap={ 6 }>
{ constructorArgs && (
<RawDataSnippet
data={ constructorArgs }
title="Constructor Arguments"
textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/>
) }
{ data?.source_code && addressHash && (
<ContractSourceCode
address={ addressHash }
implementations={ addressInfo?.implementations || undefined }
/>
) }
{ data?.compiler_settings ? (
<RawDataSnippet
data={ JSON.stringify(data.compiler_settings, undefined, 4) }
title="Compiler Settings"
textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/>
) : null }
{ data?.abi && (
<RawDataSnippet
data={ JSON.stringify(data.abi, undefined, 4) }
title="Contract ABI"
textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/>
) }
{ data?.creation_bytecode && (
<RawDataSnippet
data={ data.creation_bytecode }
title="Contract creation code"
rightSlot={ canBeVerified ? verificationButton : null }
beforeSlot={ data.is_self_destructed ? (
<Alert status="info" whiteSpace="pre-wrap" mb={ 3 }>
Contracts that self destruct in their constructors have no contract code published and cannot be verified.
Displaying the init data provided of the creating transaction.
</Alert>
) : null }
textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/>
) }
{ data?.deployed_bytecode && (
<RawDataSnippet
data={ data.deployed_bytecode }
title="Deployed ByteCode"
rightSlot={ !data?.creation_bytecode && canBeVerified ? verificationButton : null }
textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/>
) }
</Flex>
</>
);
};
export default ContractCode;
import React from 'react'; import React from 'react';
import * as addressMock from 'mocks/address/address'; import * as addressMock from 'mocks/address/address';
import { contractAudits } from 'mocks/contract/audits';
import * as contractMock from 'mocks/contract/info'; import * as contractMock from 'mocks/contract/info';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import * as socketServer from 'playwright/fixtures/socketServer'; import * as socketServer from 'playwright/fixtures/socketServer';
import { test, expect } from 'playwright/lib'; import { test, expect } from 'playwright/lib';
import * as pwConfig from 'playwright/utils/config';
import ContractCode from './specs/ContractCode'; import ContractDetails from './specs/ContractDetails';
const hooksConfig = { const hooksConfig = {
router: { router: {
...@@ -28,30 +27,72 @@ test.beforeEach(async({ mockApiResponse, page }) => { ...@@ -28,30 +27,72 @@ test.beforeEach(async({ mockApiResponse, page }) => {
addressApiUrl = await mockApiResponse('address', addressMock.contract, { pathParams: { hash: addressMock.contract.hash } }); addressApiUrl = await mockApiResponse('address', addressMock.contract, { pathParams: { hash: addressMock.contract.hash } });
}); });
test('full view +@mobile +@dark-mode', async({ render, mockApiResponse, createSocket }) => { test.describe('full view', () => {
await mockApiResponse('contract', contractMock.withChangedByteCode, { pathParams: { hash: addressMock.contract.hash } }); test.beforeEach(async({ mockApiResponse }) => {
await mockApiResponse('contract', contractMock.withChangedByteCode, { pathParams: { hash: addressMock.contract.implementations?.[0].address as string } }); await mockApiResponse('contract', contractMock.withChangedByteCode, { pathParams: { hash: addressMock.contract.hash } });
await mockApiResponse('contract', contractMock.withChangedByteCode, { pathParams: { hash: addressMock.contract.implementations?.[0].address as string } });
});
const component = await render(<ContractCode/>, { hooksConfig }, { withSocket: true }); test('source code +@dark-mode', async({ render, createSocket }) => {
await createSocket(); const hooksConfig = {
router: {
query: { hash: addressMock.contract.hash, tab: 'contract_source_code' },
},
};
const component = await render(<ContractDetails/>, { hooksConfig }, { withSocket: true });
await createSocket();
await expect(component).toHaveScreenshot();
});
await expect(component).toHaveScreenshot(); test('compiler', async({ render, createSocket }) => {
}); const hooksConfig = {
router: {
query: { hash: addressMock.contract.hash, tab: 'contract_compiler' },
},
};
const component = await render(<ContractDetails/>, { hooksConfig }, { withSocket: true });
await createSocket();
await expect(component).toHaveScreenshot();
});
test('verified with changed byte code socket', async({ render, mockApiResponse, createSocket }) => { test('abi', async({ render, createSocket }) => {
await mockApiResponse('contract', contractMock.verified, { pathParams: { hash: addressMock.contract.hash } }); const hooksConfig = {
router: {
query: { hash: addressMock.contract.hash, tab: 'contract_abi' },
},
};
const component = await render(<ContractDetails/>, { hooksConfig }, { withSocket: true });
await createSocket();
await expect(component).toHaveScreenshot();
});
const component = await render(<ContractCode/>, { hooksConfig }, { withSocket: true }); test('bytecode', async({ render, createSocket }) => {
const socket = await createSocket(); const hooksConfig = {
const channel = await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase()); router: {
socketServer.sendMessage(socket, channel, 'changed_bytecode', {}); query: { hash: addressMock.contract.hash, tab: 'contract_bytecode' },
},
};
const component = await render(<ContractDetails/>, { hooksConfig }, { withSocket: true });
await createSocket();
await expect(component).toHaveScreenshot();
});
});
await expect(component).toHaveScreenshot(); test.describe('mobile view', () => {
test.use({ viewport: pwConfig.viewport.mobile });
test('source code', async({ render, createSocket, mockApiResponse }) => {
await mockApiResponse('contract', contractMock.withChangedByteCode, { pathParams: { hash: addressMock.contract.hash } });
await mockApiResponse('contract', contractMock.withChangedByteCode, { pathParams: { hash: addressMock.contract.implementations?.[0].address as string } });
const component = await render(<ContractDetails/>, { hooksConfig }, { withSocket: true });
await createSocket();
await expect(component).toHaveScreenshot();
});
}); });
test('verified via lookup in eth_bytecode_db', async({ render, mockApiResponse, createSocket, page }) => { test('verified via lookup in eth_bytecode_db', async({ render, mockApiResponse, createSocket, page }) => {
const contractApiUrl = await mockApiResponse('contract', contractMock.nonVerified, { pathParams: { hash: addressMock.contract.hash } }); const contractApiUrl = await mockApiResponse('contract', contractMock.nonVerified, { pathParams: { hash: addressMock.contract.hash } });
await render(<ContractCode/>, { hooksConfig }, { withSocket: true }); await render(<ContractDetails/>, { hooksConfig }, { withSocket: true });
const socket = await createSocket(); const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase()); const channel = await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase());
...@@ -64,7 +105,7 @@ test('verified via lookup in eth_bytecode_db', async({ render, mockApiResponse, ...@@ -64,7 +105,7 @@ test('verified via lookup in eth_bytecode_db', async({ render, mockApiResponse,
test('verified with multiple sources', async({ render, page, mockApiResponse }) => { test('verified with multiple sources', async({ render, page, mockApiResponse }) => {
await mockApiResponse('contract', contractMock.withMultiplePaths, { pathParams: { hash: addressMock.contract.hash } }); await mockApiResponse('contract', contractMock.withMultiplePaths, { pathParams: { hash: addressMock.contract.hash } });
await render(<ContractCode/>, { hooksConfig }, { withSocket: true }); await render(<ContractDetails/>, { hooksConfig }, { withSocket: true });
const section = page.locator('section', { hasText: 'Contract source code' }); const section = page.locator('section', { hasText: 'Contract source code' });
await expect(section).toHaveScreenshot(); await expect(section).toHaveScreenshot();
...@@ -76,83 +117,55 @@ test('verified with multiple sources', async({ render, page, mockApiResponse }) ...@@ -76,83 +117,55 @@ test('verified with multiple sources', async({ render, page, mockApiResponse })
await expect(section).toHaveScreenshot(); await expect(section).toHaveScreenshot();
}); });
test('verified via sourcify', async({ render, mockApiResponse, page }) => {
await mockApiResponse('contract', contractMock.verifiedViaSourcify, { pathParams: { hash: addressMock.contract.hash } });
await render(<ContractCode/>, { hooksConfig }, { withSocket: true });
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 110 } });
});
test('verified via eth bytecode db', async({ render, mockApiResponse, page }) => {
await mockApiResponse('contract', contractMock.verifiedViaEthBytecodeDb, { pathParams: { hash: addressMock.contract.hash } });
await render(<ContractCode/>, { hooksConfig }, { withSocket: true });
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 110 } });
});
test('self destructed', async({ render, mockApiResponse, page }) => { test('self destructed', async({ render, mockApiResponse, page }) => {
const hooksConfig = {
router: {
query: { hash: addressMock.contract.hash, tab: 'contract_bytecode' },
},
};
await mockApiResponse('contract', contractMock.selfDestructed, { pathParams: { hash: addressMock.contract.hash } }); await mockApiResponse('contract', contractMock.selfDestructed, { pathParams: { hash: addressMock.contract.hash } });
await render(<ContractCode/>, { hooksConfig }, { withSocket: true }); await render(<ContractDetails/>, { hooksConfig }, { withSocket: true });
const section = page.locator('section', { hasText: 'Contract creation code' }); const section = page.locator('section', { hasText: 'Contract creation code' });
await expect(section).toHaveScreenshot(); await expect(section).toHaveScreenshot();
}); });
test('with twin address alert +@mobile', async({ render, mockApiResponse }) => {
await mockApiResponse('contract', contractMock.withTwinAddress, { pathParams: { hash: addressMock.contract.hash } });
const component = await render(<ContractCode/>, { hooksConfig }, { withSocket: true });
await expect(component.getByRole('alert')).toHaveScreenshot();
});
test('with proxy address alert +@mobile', async({ render, mockApiResponse }) => {
await mockApiResponse('contract', contractMock.withProxyAddress, { pathParams: { hash: addressMock.contract.hash } });
const component = await render(<ContractCode/>, { hooksConfig }, { withSocket: true });
await expect(component.getByRole('alert')).toHaveScreenshot();
});
test('with certified icon +@mobile', async({ render, mockApiResponse, page }) => {
await mockApiResponse('contract', contractMock.certified, { pathParams: { hash: addressMock.contract.hash } });
await render(<ContractCode/>, { hooksConfig });
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 120 } });
});
test('non verified', async({ render, mockApiResponse }) => { test('non verified', async({ render, mockApiResponse }) => {
await mockApiResponse('contract', contractMock.nonVerified, { pathParams: { hash: addressMock.contract.hash } }); await mockApiResponse('contract', contractMock.nonVerified, { pathParams: { hash: addressMock.contract.hash } });
const component = await render(<ContractCode/>, { hooksConfig }, { withSocket: true }); const component = await render(<ContractDetails/>, { hooksConfig }, { withSocket: true });
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('zkSync contract', async({ render, mockApiResponse, page, mockEnvs }) => { test('implementation info', async({ render, mockApiResponse }) => {
await mockEnvs(ENVS_MAP.zkSyncRollup); const hooksConfig = {
await mockApiResponse('contract', contractMock.zkSync, { pathParams: { hash: addressMock.contract.hash } }); router: {
await render(<ContractCode/>, { hooksConfig }, { withSocket: true }); query: { hash: addressMock.contract.hash, tab: 'contract_compiler' },
},
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 300 } }); };
});
const implementationName = addressMock.contract.implementations?.[0].name as string;
test.describe('with audits feature', () => { const implementationAddress = addressMock.contract.implementations?.[0].address as string;
const implementationContract = {
test.beforeEach(async({ mockEnvs }) => { ...contractMock.verified,
await mockEnvs(ENVS_MAP.hasContractAuditReports); compiler_settings: {
}); evmVersion: 'london',
libraries: {},
test('no audits', async({ render, mockApiResponse }) => { metadata: {
await mockApiResponse('contract', contractMock.verified, { pathParams: { hash: addressMock.contract.hash } }); bytecodeHash: 'ipfs',
await mockApiResponse('contract_security_audits', { items: [] }, { pathParams: { hash: addressMock.contract.hash } }); useLiteralContent: false,
const component = await render(<ContractCode/>, { hooksConfig }, { withSocket: true }); },
optimizer: {
await expect(component).toHaveScreenshot(); enabled: true,
}); runs: 1000000,
},
},
};
await mockApiResponse('contract', contractMock.verified, { pathParams: { hash: addressMock.contract.hash } });
await mockApiResponse('contract', implementationContract, { pathParams: { hash: implementationAddress } });
test('has audits', async({ render, mockApiResponse }) => { const component = await render(<ContractDetails/>, { hooksConfig }, { withSocket: true });
await mockApiResponse('contract', contractMock.verified, { pathParams: { hash: addressMock.contract.hash } }); await component.getByRole('combobox').selectOption(implementationName);
await mockApiResponse('contract_security_audits', contractAudits, { pathParams: { hash: addressMock.contract.hash } });
const component = await render(<ContractCode/>, { hooksConfig }, { withSocket: true });
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
});
}); });
import type { UseQueryResult } from '@tanstack/react-query';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
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 { AddressImplementation } from 'types/api/addressParams';
import type { SmartContract } from 'types/api/contract';
import type { ResourceError } from 'lib/api/resources';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketMessage from 'lib/socket/useSocketMessage';
import * as stubs from 'stubs/contract';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import ContractDetailsAlerts from './alerts/ContractDetailsAlerts';
import ContractSourceAddressSelector from './ContractSourceAddressSelector';
import ContractDetailsInfo from './info/ContractDetailsInfo';
import useContractDetailsTabs from './useContractDetailsTabs';
const TAB_LIST_PROPS = { flexWrap: 'wrap', rowGap: 2 };
type Props = {
addressHash: string;
channel: Channel | undefined;
mainContractQuery: UseQueryResult<SmartContract, ResourceError>;
}
const ContractDetails = ({ addressHash, channel, mainContractQuery }: Props) => {
const router = useRouter();
const sourceAddress = getQueryParamString(router.query.source_address);
const queryClient = useQueryClient();
const addressInfo = queryClient.getQueryData<AddressInfo>(getResourceKey('address', { pathParams: { hash: addressHash } }));
const sourceItems: Array<AddressImplementation> = React.useMemo(() => {
const currentAddressItem = { address: addressHash, name: addressInfo?.name || 'Contract' };
if (!addressInfo || !addressInfo.implementations || addressInfo.implementations.length === 0) {
return [ currentAddressItem ];
}
return [
currentAddressItem,
...(addressInfo?.implementations.filter((item) => item.address !== addressHash && item.name) || []),
];
}, [ addressInfo, addressHash ]);
const [ selectedItem, setSelectedItem ] = React.useState(sourceItems.find((item) => item.address === sourceAddress) || sourceItems[0]);
const contractQuery = useApiQuery('contract', {
pathParams: { hash: selectedItem?.address },
queryOptions: {
enabled: Boolean(selectedItem?.address),
refetchOnMount: false,
placeholderData: addressInfo?.is_verified ? stubs.CONTRACT_CODE_VERIFIED : stubs.CONTRACT_CODE_UNVERIFIED,
},
});
const { data, isPlaceholderData, isError } = contractQuery;
const tabs = useContractDetailsTabs({ data, isLoading: isPlaceholderData, addressHash, sourceAddress: selectedItem.address });
const handleContractWasVerifiedMessage: SocketMessage.SmartContractWasVerified['handler'] = React.useCallback(() => {
queryClient.refetchQueries({
queryKey: getResourceKey('address', { pathParams: { hash: addressHash } }),
});
queryClient.refetchQueries({
queryKey: getResourceKey('contract', { pathParams: { hash: addressHash } }),
});
}, [ addressHash, queryClient ]);
useSocketMessage({
channel,
event: 'smart_contract_was_verified',
handler: handleContractWasVerifiedMessage,
});
if (isError) {
return <DataFetchAlert/>;
}
const addressSelector = sourceItems.length > 1 ? (
<ContractSourceAddressSelector
isLoading={ mainContractQuery.isPlaceholderData }
label="Source code"
items={ sourceItems }
selectedItem={ selectedItem }
onItemSelect={ setSelectedItem }
mr={{ lg: 8 }}
/>
) : null;
return (
<>
<ContractDetailsAlerts
data={ mainContractQuery.data }
isLoading={ mainContractQuery.isPlaceholderData }
addressHash={ addressHash }
channel={ channel }
/>
{ mainContractQuery.data?.is_verified && (
<ContractDetailsInfo
data={ mainContractQuery.data }
isLoading={ mainContractQuery.isPlaceholderData }
addressHash={ addressHash }
/>
) }
<RoutedTabs
tabs={ tabs }
isLoading={ isPlaceholderData }
variant="radio_group"
size="sm"
leftSlot={ addressSelector }
tabListProps={ TAB_LIST_PROPS }
/>
</>
);
};
export default ContractDetails;
import { Button, Skeleton } from '@chakra-ui/react';
import React from 'react';
import { route } from 'nextjs-routes';
interface Props {
isLoading: boolean;
addressHash: string;
isPartiallyVerified: boolean;
}
const ContractDetailsVerificationButton = ({ isLoading, addressHash, isPartiallyVerified }: Props) => {
if (isLoading) {
return (
<Skeleton
w="130px"
h={ 8 }
mr={ isPartiallyVerified ? 0 : 3 }
ml={ isPartiallyVerified ? 0 : 'auto' }
borderRadius="base"
flexShrink={ 0 }
/>
);
}
return (
<Button
size="sm"
mr={ isPartiallyVerified ? 0 : 3 }
ml={ isPartiallyVerified ? 0 : 'auto' }
flexShrink={ 0 }
as="a"
href={ route({ pathname: '/address/[hash]/contract-verification', query: { hash: addressHash } }) }
>
Verify & publish
</Button>
);
};
export default React.memo(ContractDetailsVerificationButton);
import { chakra, Flex, Select, Skeleton } from '@chakra-ui/react';
import React from 'react';
import { route } from 'nextjs-routes';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import LinkNewTab from 'ui/shared/links/LinkNewTab';
export interface Item {
address: string;
name?: string | null | undefined;
}
interface Props {
className?: string;
label: string;
selectedItem: Item;
onItemSelect: (item: Item) => void;
items: Array<Item>;
isLoading?: boolean;
}
const ContractSourceAddressSelector = ({ className, selectedItem, onItemSelect, items, isLoading, label }: Props) => {
const handleItemSelect = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
const nextOption = items.find(({ address }) => address === event.target.value);
if (nextOption) {
onItemSelect(nextOption);
}
}, [ items, onItemSelect ]);
if (isLoading) {
return <Skeleton h={ 6 } w={{ base: '300px', lg: '500px' }} className={ className }/>;
}
if (items.length === 0) {
return null;
}
if (items.length === 1) {
return (
<Flex flexWrap="wrap" columnGap={ 3 } rowGap={ 2 } className={ className }>
<chakra.span fontWeight={ 500 } fontSize="sm">{ label }</chakra.span>
<AddressEntity
address={{ hash: items[0].address, is_contract: true, is_verified: true }}
/>
</Flex>
);
}
return (
<Flex columnGap={ 3 } rowGap={ 2 } alignItems="center" className={ className }>
<chakra.span fontWeight={ 500 } fontSize="sm">{ label }</chakra.span>
<Select
size="xs"
value={ selectedItem.address }
onChange={ handleItemSelect }
w="auto"
fontWeight={ 600 }
borderRadius="base"
>
{ items.map((item) => (
<option key={ item.address } value={ item.address }>
{ item.name }
</option>
)) }
</Select>
<Flex columnGap={ 2 } alignItems="center">
<CopyToClipboard text={ selectedItem.address } ml={ 0 }/>
<LinkNewTab
label="Open contract details page in new tab"
href={ route({ pathname: '/address/[hash]', query: { hash: selectedItem.address, tab: 'contract' } }) }
/>
</Flex>
</Flex>
);
};
export default React.memo(chakra(ContractSourceAddressSelector));
import { Flex, Select, Skeleton, Text, Tooltip } from '@chakra-ui/react'; import { Flex, Skeleton, Text, Tooltip } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { AddressImplementation } from 'types/api/addressParams';
import type { SmartContract } from 'types/api/contract'; import type { SmartContract } from 'types/api/contract';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import useApiQuery from 'lib/api/useApiQuery';
import * as stubs from 'stubs/contract';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import LinkInternal from 'ui/shared/links/LinkInternal'; import LinkInternal from 'ui/shared/links/LinkInternal';
import CodeEditor from 'ui/shared/monaco/CodeEditor'; import CodeEditor from 'ui/shared/monaco/CodeEditor';
...@@ -38,82 +35,34 @@ function getEditorData(contractInfo: SmartContract | undefined) { ...@@ -38,82 +35,34 @@ function getEditorData(contractInfo: SmartContract | undefined) {
]; ];
} }
interface SourceContractOption {
address: string;
label: string;
}
interface Props { interface Props {
address: string; data: SmartContract | undefined;
implementations?: Array<AddressImplementation>; sourceAddress: string;
isLoading?: boolean;
} }
export const ContractSourceCode = ({ address, implementations }: Props) => { export const ContractSourceCode = ({ data, isLoading, sourceAddress }: Props) => {
const options: Array<SourceContractOption> = React.useMemo(() => {
return [
{ label: 'Proxy', address },
...(implementations || [])
.filter((item) => item.name && item.address !== address)
.map(({ name, address }, item, array) => ({ address, label: array.length === 1 ? 'Implementation' : `Impl: ${ name }` })),
];
}, [ address, implementations ]);
const [ sourceContract, setSourceContract ] = React.useState<SourceContractOption>(options[0]);
const contractQuery = useApiQuery('contract', {
pathParams: { hash: sourceContract.address },
queryOptions: {
refetchOnMount: false,
placeholderData: stubs.CONTRACT_CODE_VERIFIED,
},
});
const editorData = React.useMemo(() => { const editorData = React.useMemo(() => {
return getEditorData(contractQuery.data); return getEditorData(data);
}, [ contractQuery.data ]); }, [ data ]);
const isLoading = contractQuery.isPlaceholderData;
const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
const nextOption = options.find(({ address }) => address === event.target.value);
if (nextOption) {
setSourceContract(nextOption);
}
}, [ options ]);
const heading = ( const heading = (
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 }> <Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>
<span>Contract source code</span> <span>Contract source code</span>
{ contractQuery.data?.language && { data?.language &&
<Text whiteSpace="pre" as="span" variant="secondary" textTransform="capitalize"> ({ contractQuery.data.language })</Text> } <Text whiteSpace="pre" as="span" variant="secondary" textTransform="capitalize"> ({ data.language })</Text> }
</Skeleton> </Skeleton>
); );
const select = options.length > 1 ? ( const externalLibraries = data?.external_libraries ?
<Select <ContractExternalLibraries data={ data.external_libraries } isLoading={ isLoading }/> :
size="xs"
value={ sourceContract.address }
onChange={ handleSelectChange }
w="auto"
maxW={{ lg: '200px', xl: '400px' }}
whiteSpace="nowrap"
textOverflow="ellipsis"
fontWeight={ 600 }
borderRadius="base"
>
{ options.map((option) => <option key={ option.address } value={ option.address }>{ option.label }</option>) }
</Select>
) : null;
const externalLibraries = contractQuery.data?.external_libraries ?
<ContractExternalLibraries data={ contractQuery.data.external_libraries } isLoading={ isLoading }/> :
null; null;
const diagramLink = contractQuery?.data?.can_be_visualized_via_sol2uml ? ( const diagramLink = data?.can_be_visualized_via_sol2uml ? (
<Tooltip label="Visualize contract code using Sol2Uml JS library"> <Tooltip label="Visualize contract code using Sol2Uml JS library">
<LinkInternal <LinkInternal
href={ route({ pathname: '/visualize/sol2uml', query: { address: sourceContract.address } }) } href={ route({ pathname: '/visualize/sol2uml', query: { address: sourceAddress } }) }
ml={{ base: '0', lg: 'auto' }} ml={{ base: '0', lg: 'auto' }}
isLoading={ isLoading } isLoading={ isLoading }
> >
...@@ -124,11 +73,11 @@ export const ContractSourceCode = ({ address, implementations }: Props) => { ...@@ -124,11 +73,11 @@ export const ContractSourceCode = ({ address, implementations }: Props) => {
</Tooltip> </Tooltip>
) : null; ) : null;
const ides = <ContractCodeIdes hash={ sourceContract.address } isLoading={ isLoading }/>; const ides = <ContractCodeIdes hash={ sourceAddress } isLoading={ isLoading }/>;
const copyToClipboard = contractQuery.data && editorData?.length === 1 ? ( const copyToClipboard = data && editorData?.length === 1 ? (
<CopyToClipboard <CopyToClipboard
text={ contractQuery.data.source_code } text={ data.source_code }
isLoading={ isLoading } isLoading={ isLoading }
ml={{ base: 'auto', lg: diagramLink ? '0' : 'auto' }} ml={{ base: 'auto', lg: diagramLink ? '0' : 'auto' }}
/> />
...@@ -146,13 +95,13 @@ export const ContractSourceCode = ({ address, implementations }: Props) => { ...@@ -146,13 +95,13 @@ export const ContractSourceCode = ({ address, implementations }: Props) => {
return ( return (
<CodeEditor <CodeEditor
key={ sourceContract.address } key={ sourceAddress }
data={ editorData } data={ editorData }
remappings={ contractQuery.data?.compiler_settings?.remappings } remappings={ data?.compiler_settings?.remappings }
libraries={ contractQuery.data?.external_libraries ?? undefined } libraries={ data?.external_libraries ?? undefined }
language={ contractQuery.data?.language ?? undefined } language={ data?.language ?? undefined }
mainFile={ editorData[0]?.file_path } mainFile={ editorData[0]?.file_path }
contractName={ contractQuery.data?.name ?? undefined } contractName={ data?.name ?? undefined }
/> />
); );
})(); })();
...@@ -161,7 +110,6 @@ export const ContractSourceCode = ({ address, implementations }: Props) => { ...@@ -161,7 +110,6 @@ export const ContractSourceCode = ({ address, implementations }: Props) => {
<section> <section>
<Flex alignItems="center" mb={ 3 } columnGap={ 3 } rowGap={ 2 } flexWrap="wrap"> <Flex alignItems="center" mb={ 3 } columnGap={ 3 } rowGap={ 2 } flexWrap="wrap">
{ heading } { heading }
{ select }
{ externalLibraries } { externalLibraries }
{ diagramLink } { diagramLink }
{ ides } { ides }
......
...@@ -2,19 +2,19 @@ import React from 'react'; ...@@ -2,19 +2,19 @@ import React from 'react';
import { test, expect } from 'playwright/lib'; import { test, expect } from 'playwright/lib';
import ContractCodeProxyPattern from './ContractCodeProxyPattern'; import ContractDetailsAlertProxyPattern from './ContractDetailsAlertProxyPattern';
test('proxy type with link +@mobile', async({ render }) => { test('proxy type with link +@mobile', async({ render }) => {
const component = await render(<ContractCodeProxyPattern type="eip1167"/>); const component = await render(<ContractDetailsAlertProxyPattern type="eip1167"/>);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('proxy type with link but without description', async({ render }) => { test('proxy type with link but without description', async({ render }) => {
const component = await render(<ContractCodeProxyPattern type="master_copy"/>); const component = await render(<ContractDetailsAlertProxyPattern type="master_copy"/>);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('proxy type without link', async({ render }) => { test('proxy type without link', async({ render }) => {
const component = await render(<ContractCodeProxyPattern type="basic_implementation"/>); const component = await render(<ContractDetailsAlertProxyPattern type="basic_implementation"/>);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
import { Alert } from '@chakra-ui/react';
import React from 'react';
import type { SmartContract } from 'types/api/contract';
import LinkExternal from 'ui/shared/links/LinkExternal';
interface Props {
data: SmartContract | undefined;
}
const ContractDetailsAlertVerificationSource = ({ data }: Props) => {
if (data?.is_verified_via_eth_bytecode_db) {
return (
<Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap">
<span>This contract has been { data.is_partially_verified ? 'partially ' : '' }verified using </span>
<LinkExternal
href="https://docs.blockscout.com/about/features/ethereum-bytecode-database-microservice"
fontSize="md"
>
Blockscout Bytecode Database
</LinkExternal>
</Alert>
);
}
if (data?.is_verified_via_sourcify) {
return (
<Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap">
<span>This contract has been { data.is_partially_verified ? 'partially ' : '' }verified via Sourcify. </span>
{ data.sourcify_repo_url && <LinkExternal href={ data.sourcify_repo_url } fontSize="md">View contract in Sourcify repository</LinkExternal> }
</Alert>
);
}
return null;
};
export default React.memo(ContractDetailsAlertVerificationSource);
import React from 'react';
import * as addressMock from 'mocks/address/address';
import * as contractMock from 'mocks/contract/info';
import * as socketServer from 'playwright/fixtures/socketServer';
import { test, expect } from 'playwright/lib';
import ContractDetailsAlerts from './ContractDetailsAlerts.pwstory';
// 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('verified with changed byte code socket', async({ render, createSocket }) => {
const props = {
data: contractMock.verified,
isLoading: false,
addressHash: addressMock.contract.hash,
};
const component = await render(<ContractDetailsAlerts { ...props }/>, undefined, { withSocket: true });
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase());
socketServer.sendMessage(socket, channel, 'changed_bytecode', {});
await expect(component).toHaveScreenshot();
});
test('verified via sourcify', async({ render }) => {
const props = {
data: contractMock.verifiedViaSourcify,
isLoading: false,
addressHash: addressMock.contract.hash,
};
const component = await render(<ContractDetailsAlerts { ...props }/>, undefined, { withSocket: true });
await expect(component).toHaveScreenshot();
});
test('verified via eth bytecode db', async({ render }) => {
const props = {
data: contractMock.verifiedViaEthBytecodeDb,
isLoading: false,
addressHash: addressMock.contract.hash,
};
const component = await render(<ContractDetailsAlerts { ...props }/>, undefined, { withSocket: true });
await expect(component).toHaveScreenshot();
});
test('with twin address alert +@mobile', async({ render }) => {
const props = {
data: contractMock.withTwinAddress,
isLoading: false,
addressHash: addressMock.contract.hash,
};
const component = await render(<ContractDetailsAlerts { ...props }/>, undefined, { withSocket: true });
await expect(component).toHaveScreenshot();
});
import React from 'react';
import useSocketChannel from 'lib/socket/useSocketChannel';
import type { Props } from './ContractDetailsAlerts';
import ContractDetailsAlerts from './ContractDetailsAlerts';
const ContractDetailsAlertsPwStory = (props: Props) => {
const channel = useSocketChannel({
topic: `addresses:${ props.addressHash.toLowerCase() }`,
isDisabled: false,
});
return <ContractDetailsAlerts { ...props } channel={ channel }/>;
};
export default ContractDetailsAlertsPwStory;
import { chakra, Alert, Box, Flex, Skeleton } from '@chakra-ui/react';
import type { Channel } from 'phoenix';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { SmartContract } from 'types/api/contract';
import { route } from 'nextjs-routes';
import useSocketMessage from 'lib/socket/useSocketMessage';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import LinkExternal from 'ui/shared/links/LinkExternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ContractDetailsVerificationButton from '../ContractDetailsVerificationButton';
import ContractDetailsAlertProxyPattern from './ContractDetailsAlertProxyPattern';
import ContractDetailsAlertVerificationSource from './ContractDetailsAlertVerificationSource';
export interface Props {
data: SmartContract | undefined;
isLoading: boolean;
addressHash: string;
channel?: Channel;
}
const ContractDetailsAlerts = ({ data, isLoading, addressHash, channel }: Props) => {
const [ isChangedBytecodeSocket, setIsChangedBytecodeSocket ] = React.useState<boolean>();
const handleChangedBytecodeMessage: SocketMessage.AddressChangedBytecode['handler'] = React.useCallback(() => {
setIsChangedBytecodeSocket(true);
}, [ ]);
useSocketMessage({
channel,
event: 'changed_bytecode',
handler: handleChangedBytecodeMessage,
});
return (
<Flex flexDir="column" rowGap={ 2 } mb={ 6 } _empty={{ display: 'none' }}>
{ data?.is_blueprint && (
<Box>
<span>This is an </span>
<LinkExternal href="https://eips.ethereum.org/EIPS/eip-5202">
ERC-5202 Blueprint contract
</LinkExternal>
</Box>
) }
{ data?.is_verified && (
<Skeleton isLoaded={ !isLoading }>
<Alert status="success" flexWrap="wrap" rowGap={ 3 } columnGap={ 5 }>
<span>Contract Source Code Verified ({ data.is_partially_verified ? 'Partial' : 'Exact' } Match)</span>
{
data.is_partially_verified ? (
<ContractDetailsVerificationButton
isLoading={ isLoading }
addressHash={ addressHash }
isPartiallyVerified
/>
) : null
}
</Alert>
</Skeleton>
) }
<ContractDetailsAlertVerificationSource data={ data }/>
{ (data?.is_changed_bytecode || isChangedBytecodeSocket) && (
<Alert status="warning">
Warning! Contract bytecode has been changed and does not match the verified one. Therefore, interaction with this smart contract may be risky.
</Alert>
) }
{ !data?.is_verified && data?.verified_twin_address_hash && (!data?.proxy_type || data.proxy_type === 'unknown') && (
<Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap">
<span>Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB </span>
<AddressEntity
address={{ hash: data.verified_twin_address_hash, is_contract: true }}
truncation="constant"
fontSize="sm"
fontWeight="500"
/>
<chakra.span mt={ 1 }>All functions displayed below are from ABI of that contract. In order to verify current contract, proceed with </chakra.span>
<LinkInternal href={ route({ pathname: '/address/[hash]/contract-verification', query: { hash: addressHash } }) }>
Verify & Publish
</LinkInternal>
<span> page</span>
</Alert>
) }
{ data?.proxy_type && <ContractDetailsAlertProxyPattern type={ data.proxy_type }/> }
</Flex>
);
};
export default React.memo(ContractDetailsAlerts);
...@@ -9,7 +9,7 @@ import ContainerWithScrollY from 'ui/shared/ContainerWithScrollY'; ...@@ -9,7 +9,7 @@ import ContainerWithScrollY from 'ui/shared/ContainerWithScrollY';
import FormModal from 'ui/shared/FormModal'; import FormModal from 'ui/shared/FormModal';
import LinkExternal from 'ui/shared/links/LinkExternal'; import LinkExternal from 'ui/shared/links/LinkExternal';
import ContractSubmitAuditForm from './contractSubmitAuditForm/ContractSubmitAuditForm'; import ContractSubmitAuditForm from './ContractSubmitAuditForm';
type Props = { type Props = {
addressHash?: string; addressHash?: string;
......
import { Button, VStack } from '@chakra-ui/react'; import { Button, VStack } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import type { SmartContractSecurityAuditSubmission } from 'types/api/contract'; import type { SmartContractSecurityAuditSubmission } from 'types/api/contract';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import dayjs from 'lib/date/dayjs';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
import FormFieldCheckbox from 'ui/shared/forms/fields/FormFieldCheckbox';
import AuditComment from './fields/AuditComment'; import FormFieldEmail from 'ui/shared/forms/fields/FormFieldEmail';
import AuditCompanyName from './fields/AuditCompanyName'; import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import AuditProjectName from './fields/AuditProjectName'; import FormFieldUrl from 'ui/shared/forms/fields/FormFieldUrl';
import AuditProjectUrl from './fields/AuditProjectUrl';
import AuditReportDate from './fields/AuditReportDate';
import AuditReportUrl from './fields/AuditReportUrl';
import AuditSubmitterEmail from './fields/AuditSubmitterEmail';
import AuditSubmitterIsOwner from './fields/AuditSubmitterIsOwner';
import AuditSubmitterName from './fields/AuditSubmitterName';
interface Props { interface Props {
address?: string; address?: string;
...@@ -46,10 +41,11 @@ const ContractSubmitAuditForm = ({ address, onSuccess }: Props) => { ...@@ -46,10 +41,11 @@ const ContractSubmitAuditForm = ({ address, onSuccess }: Props) => {
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const toast = useToast(); const toast = useToast();
const { handleSubmit, formState, control, setError } = useForm<Inputs>({ const formApi = useForm<Inputs>({
mode: 'onTouched', mode: 'onTouched',
defaultValues: { is_project_owner: false }, defaultValues: { is_project_owner: false },
}); });
const { handleSubmit, formState, setError } = formApi;
const onFormSubmit: SubmitHandler<Inputs> = React.useCallback(async(data) => { const onFormSubmit: SubmitHandler<Inputs> = React.useCallback(async(data) => {
try { try {
...@@ -94,30 +90,46 @@ const ContractSubmitAuditForm = ({ address, onSuccess }: Props) => { ...@@ -94,30 +90,46 @@ const ContractSubmitAuditForm = ({ address, onSuccess }: Props) => {
}, [ apiFetch, address, toast, setError, onSuccess ]); }, [ apiFetch, address, toast, setError, onSuccess ]);
return ( return (
<form noValidate onSubmit={ handleSubmit(onFormSubmit) } autoComplete="off" ref={ containerRef }> <FormProvider { ...formApi }>
<VStack gap={ 5 }> <form noValidate onSubmit={ handleSubmit(onFormSubmit) } autoComplete="off" ref={ containerRef }>
<AuditSubmitterName control={ control }/> <VStack gap={ 5 } alignItems="flex-start">
<AuditSubmitterEmail control={ control }/> <FormFieldText<Inputs> name="submitter_name" isRequired placeholder="Submitter name"/>
<AuditSubmitterIsOwner control={ control }/> <FormFieldEmail<Inputs> name="submitter_email" isRequired placeholder="Submitter email"/>
<AuditProjectName control={ control }/> <FormFieldCheckbox<Inputs, 'is_project_owner'>
<AuditProjectUrl control={ control }/> name="is_project_owner"
<AuditCompanyName control={ control }/> label="I'm the contract owner"
<AuditReportUrl control={ control }/> />
<AuditReportDate control={ control }/> <FormFieldText<Inputs> name="project_name" isRequired placeholder="Project name"/>
<FormFieldUrl<Inputs> name="project_url" isRequired placeholder="Project URL"/>
<AuditComment control={ control }/> <FormFieldText<Inputs> name="audit_company_name" isRequired placeholder="Audit company name"/>
</VStack> <FormFieldUrl<Inputs> name="audit_report_url" isRequired placeholder="Audit report URL"/>
<Button <FormFieldText<Inputs>
type="submit" name="audit_publish_date"
size="lg" type="date"
mt={ 8 } max={ dayjs().format('YYYY-MM-DD') }
isLoading={ formState.isSubmitting } isRequired
loadingText="Send request" placeholder="Audit publish date"
isDisabled={ !formState.isDirty } />
> <FormFieldText<Inputs>
name="comment"
placeholder="Comment"
maxH="160px"
rules={{ maxLength: 300 }}
asComponent="Textarea"
/>
</VStack>
<Button
type="submit"
size="lg"
mt={ 8 }
isLoading={ formState.isSubmitting }
loadingText="Send request"
isDisabled={ !formState.isDirty }
>
Send request Send request
</Button> </Button>
</form> </form>
</FormProvider>
); );
}; };
......
import { FormControl, Textarea } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import type { Inputs } from '../ContractSubmitAuditForm';
interface Props {
control: Control<Inputs>;
}
const AuditComment = ({ control }: Props) => {
const renderControl: ControllerProps<Inputs, 'comment'>['render'] = React.useCallback(({ field, fieldState }) => {
return (
<FormControl variant="floating" id={ field.name }>
<Textarea
{ ...field }
isInvalid={ Boolean(fieldState.error) }
autoComplete="off"
maxH="160px"
maxLength={ 300 }
/>
<InputPlaceholder text="Comment" error={ fieldState.error }/>
</FormControl>
);
}, [ ]);
return (
<Controller
name="comment"
control={ control }
render={ renderControl }
rules={{ maxLength: 300 }}
/>
);
};
export default React.memo(AuditComment);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import type { Inputs } from '../ContractSubmitAuditForm';
interface Props {
control: Control<Inputs>;
}
const AuditCompanyName = ({ control }: Props) => {
const renderControl: ControllerProps<Inputs, 'audit_company_name'>['render'] = React.useCallback(({ field, fieldState }) => {
return (
<FormControl variant="floating" id={ field.name } isRequired>
<Input
{ ...field }
required
isInvalid={ Boolean(fieldState.error) }
autoComplete="off"
/>
<InputPlaceholder text="Audit company name" error={ fieldState.error }/>
</FormControl>
);
}, [ ]);
return (
<Controller
name="audit_company_name"
control={ control }
render={ renderControl }
rules={{ required: true }}
/>
);
};
export default React.memo(AuditCompanyName);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import type { Inputs } from '../ContractSubmitAuditForm';
interface Props {
control: Control<Inputs>;
}
const AuditProjectName = ({ control }: Props) => {
const renderControl: ControllerProps<Inputs, 'project_name'>['render'] = React.useCallback(({ field, fieldState }) => {
return (
<FormControl variant="floating" id={ field.name } isRequired>
<Input
{ ...field }
required
isInvalid={ Boolean(fieldState.error) }
autoComplete="off"
/>
<InputPlaceholder text="Project name" error={ fieldState.error }/>
</FormControl>
);
}, [ ]);
return (
<Controller
name="project_name"
control={ control }
render={ renderControl }
rules={{ required: true }}
/>
);
};
export default React.memo(AuditProjectName);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import { validator as validateUrl } from 'lib/validations/url';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import type { Inputs } from '../ContractSubmitAuditForm';
interface Props {
control: Control<Inputs>;
}
const AuditProjectUrl = ({ control }: Props) => {
const renderControl: ControllerProps<Inputs, 'project_url'>['render'] = React.useCallback(({ field, fieldState }) => {
return (
<FormControl variant="floating" id={ field.name } isRequired>
<Input
{ ...field }
required
isInvalid={ Boolean(fieldState.error) }
autoComplete="off"
/>
<InputPlaceholder text="Project URL" error={ fieldState.error }/>
</FormControl>
);
}, [ ]);
return (
<Controller
name="project_url"
control={ control }
render={ renderControl }
rules={{ required: true, validate: { url: validateUrl } }}
/>
);
};
export default React.memo(AuditProjectUrl);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import dayjs from 'lib/date/dayjs';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import type { Inputs } from '../ContractSubmitAuditForm';
interface Props {
control: Control<Inputs>;
}
const AuditCompanyName = ({ control }: Props) => {
const renderControl: ControllerProps<Inputs, 'audit_publish_date'>['render'] = React.useCallback(({ field, fieldState }) => {
return (
<FormControl variant="floating" id={ field.name } isRequired>
<Input
{ ...field }
required
isInvalid={ Boolean(fieldState.error) }
autoComplete="off"
type="date"
max={ dayjs().format('YYYY-MM-DD') }
/>
<InputPlaceholder text="Audit publish date" error={ fieldState.error }/>
</FormControl>
);
}, [ ]);
return (
<Controller
name="audit_publish_date"
control={ control }
render={ renderControl }
rules={{ required: true }}
/>
);
};
export default React.memo(AuditCompanyName);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import { validator as validateUrl } from 'lib/validations/url';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import type { Inputs } from '../ContractSubmitAuditForm';
interface Props {
control: Control<Inputs>;
}
const AuditReportUrl = ({ control }: Props) => {
const renderControl: ControllerProps<Inputs, 'audit_report_url'>['render'] = React.useCallback(({ field, fieldState }) => {
return (
<FormControl variant="floating" id={ field.name } isRequired>
<Input
{ ...field }
required
isInvalid={ Boolean(fieldState.error) }
autoComplete="off"
/>
<InputPlaceholder text="Audit report URL" error={ fieldState.error }/>
</FormControl>
);
}, [ ]);
return (
<Controller
name="audit_report_url"
control={ control }
render={ renderControl }
rules={{ required: true, validate: { url: validateUrl } }}
/>
);
};
export default React.memo(AuditReportUrl);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import { EMAIL_REGEXP } from 'lib/validations/email';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import type { Inputs } from '../ContractSubmitAuditForm';
interface Props {
control: Control<Inputs>;
}
const AuditSubmitterEmail = ({ control }: Props) => {
const renderControl: ControllerProps<Inputs, 'submitter_email'>['render'] = React.useCallback(({ field, fieldState }) => {
return (
<FormControl variant="floating" id={ field.name } isRequired>
<Input
{ ...field }
required
isInvalid={ Boolean(fieldState.error) }
/>
<InputPlaceholder text="Submitter email" error={ fieldState.error }/>
</FormControl>
);
}, [ ]);
return (
<Controller
name="submitter_email"
control={ control }
render={ renderControl }
rules={{ required: true, pattern: EMAIL_REGEXP }}
/>
);
};
export default React.memo(AuditSubmitterEmail);
import { FormControl } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import CheckboxInput from 'ui/shared/CheckboxInput';
import type { Inputs } from '../ContractSubmitAuditForm';
interface Props {
control: Control<Inputs>;
}
const AuditSubmitterIsOwner = ({ control }: Props) => {
const renderControl: ControllerProps<Inputs, 'is_project_owner'>['render'] = React.useCallback(({ field }) => {
return (
<FormControl id={ field.name }>
<CheckboxInput<Inputs, 'is_project_owner'>
text="I'm the contract owner"
field={ field }
/>
</FormControl>
);
}, [ ]);
return (
<Controller
name="is_project_owner"
control={ control }
render={ renderControl }
/>
);
};
export default React.memo(AuditSubmitterIsOwner);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import type { Inputs } from '../ContractSubmitAuditForm';
interface Props {
control: Control<Inputs>;
}
const AuditSubmitterName = ({ control }: Props) => {
const renderControl: ControllerProps<Inputs, 'submitter_name'>['render'] = React.useCallback(({ field, fieldState }) => {
return (
<FormControl variant="floating" id={ field.name } isRequired>
<Input
{ ...field }
required
isInvalid={ Boolean(fieldState.error) }
/>
<InputPlaceholder text="Submitter name" error={ fieldState.error }/>
</FormControl>
);
}, [ ]);
return (
<Controller
name="submitter_name"
control={ control }
render={ renderControl }
rules={{ required: true }}
/>
);
};
export default React.memo(AuditSubmitterName);
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 { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';
import ContractDetailsInfo from './ContractDetailsInfo';
test('with certified icon', async({ render }) => {
const props = {
data: contractMock.certified,
isLoading: false,
addressHash: addressMock.contract.hash,
};
const component = await render(<ContractDetailsInfo { ...props }/>);
await expect(component).toHaveScreenshot();
});
test('zkSync contract', async({ render, mockEnvs }) => {
await mockEnvs(ENVS_MAP.zkSyncRollup);
const props = {
data: contractMock.zkSync,
isLoading: false,
addressHash: addressMock.contract.hash,
};
const component = await render(<ContractDetailsInfo { ...props }/>);
await expect(component).toHaveScreenshot();
});
test.describe('with audits feature', () => {
test.beforeEach(async({ mockEnvs }) => {
await mockEnvs(ENVS_MAP.hasContractAuditReports);
});
test('no audits', async({ render, mockApiResponse }) => {
await mockApiResponse('contract_security_audits', { items: [] }, { pathParams: { hash: addressMock.contract.hash } });
const props = {
data: contractMock.verified,
isLoading: false,
addressHash: addressMock.contract.hash,
};
const component = await render(<ContractDetailsInfo { ...props }/>);
await expect(component).toHaveScreenshot();
});
test('has audits', async({ render, mockApiResponse }) => {
await mockApiResponse('contract_security_audits', contractAudits, { pathParams: { hash: addressMock.contract.hash } });
const props = {
data: contractMock.verified,
isLoading: false,
addressHash: addressMock.contract.hash,
};
const component = await render(<ContractDetailsInfo { ...props }/>);
await expect(component).toHaveScreenshot();
});
});
import { Flex, Grid } from '@chakra-ui/react';
import React from 'react';
import type { SmartContract } from 'types/api/contract';
import config from 'configs/app';
import { CONTRACT_LICENSES } from 'lib/contracts/licenses';
import dayjs from 'lib/date/dayjs';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
import LinkExternal from 'ui/shared/links/LinkExternal';
import ContractSecurityAudits from '../audits/ContractSecurityAudits';
import ContractDetailsInfoItem from './ContractDetailsInfoItem';
const rollupFeature = config.features.rollup;
interface Props {
data: SmartContract;
isLoading: boolean;
addressHash: string;
}
const ContractDetailsInfo = ({ data, isLoading, addressHash }: Props) => {
const contractNameWithCertifiedIcon = data ? (
<Flex alignItems="center">
{ data.name }
{ data.certified && <ContractCertifiedLabel iconSize={ 5 } boxSize={ 5 } ml={ 2 }/> }
</Flex>
) : null;
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 (
<LinkExternal href={ license.url }>
{ license.label }
</LinkExternal>
);
})();
return (
<Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 6 } mb={ 8 }>
{ data.name && (
<ContractDetailsInfoItem
label="Contract name"
content={ contractNameWithCertifiedIcon }
isLoading={ isLoading }
/>
) }
{ data.compiler_version && (
<ContractDetailsInfoItem
label="Compiler version"
content={ data.compiler_version }
isLoading={ isLoading }
/>
) }
{ data.zk_compiler_version && (
<ContractDetailsInfoItem
label="ZK compiler version"
content={ data.zk_compiler_version }
isLoading={ isLoading }
/>
) }
{ data.evm_version && (
<ContractDetailsInfoItem
label="EVM version"
content={ data.evm_version }
textTransform="capitalize"
isLoading={ isLoading }
/>
) }
{ licenseLink && (
<ContractDetailsInfoItem
label="License"
content={ licenseLink }
hint="License type is entered manually during verification. The initial source code may contain a different license type than the one displayed."
isLoading={ isLoading }
/>
) }
{ typeof data.optimization_enabled === 'boolean' && (
<ContractDetailsInfoItem
label="Optimization enabled"
content={ data.optimization_enabled ? 'true' : 'false' }
isLoading={ isLoading }
/>
) }
{ data.optimization_runs !== null && (
<ContractDetailsInfoItem
label={ rollupFeature.isEnabled && rollupFeature.type === 'zkSync' ? 'Optimization mode' : 'Optimization runs' }
content={ String(data.optimization_runs) }
isLoading={ isLoading }
/>
) }
{ data.verified_at && (
<ContractDetailsInfoItem
label="Verified at"
content={ dayjs(data.verified_at).format('llll') }
wordBreak="break-word"
isLoading={ isLoading }
/>
) }
{ data.file_path && (
<ContractDetailsInfoItem
label="Contract file path"
content={ data.file_path }
wordBreak="break-word"
isLoading={ isLoading }
/>
) }
{ config.UI.hasContractAuditReports && (
<ContractDetailsInfoItem
label="Security audit"
content={ <ContractSecurityAudits addressHash={ addressHash }/> }
isLoading={ isLoading }
/>
) }
</Grid>
);
};
export default React.memo(ContractDetailsInfo);
import { chakra, useColorModeValue, Flex, GridItem, Skeleton } from '@chakra-ui/react';
import React from 'react';
import Hint from 'ui/shared/Hint';
interface Props {
label: string;
content: string | React.ReactNode;
className?: string;
isLoading: boolean;
hint?: string;
}
const ContractDetailsInfoItem = ({ label, content, className, isLoading, hint }: Props) => {
const hintIconColor = useColorModeValue('gray.600', 'gray.400');
return (
<GridItem display="flex" columnGap={ 6 } wordBreak="break-all" className={ className } alignItems="baseline">
<Skeleton isLoaded={ !isLoading } w="170px" flexShrink={ 0 } fontWeight={ 500 }>
<Flex alignItems="center">
{ label }
{ hint && (
<Hint
label={ hint }
ml={ 2 }
color={ hintIconColor }
tooltipProps={{ placement: 'bottom' }}
/>
) }
</Flex>
</Skeleton>
<Skeleton isLoaded={ !isLoading }>{ content }</Skeleton>
</GridItem>
);
};
export default React.memo(chakra(ContractDetailsInfoItem));
...@@ -12,9 +12,10 @@ interface Props { ...@@ -12,9 +12,10 @@ interface Props {
abi: Array<SmartContractMethod>; abi: Array<SmartContractMethod>;
addressHash: string; addressHash: string;
tab: string; tab: string;
sourceAddress?: string;
} }
const ContractAbi = ({ abi, addressHash, tab }: Props) => { const ContractAbi = ({ abi, addressHash, sourceAddress, tab }: Props) => {
const [ expandedSections, setExpandedSections ] = React.useState<Array<number>>(abi.length === 1 ? [ 0 ] : []); const [ expandedSections, setExpandedSections ] = React.useState<Array<number>>(abi.length === 1 ? [ 0 ] : []);
const [ id, setId ] = React.useState(0); const [ id, setId ] = React.useState(0);
...@@ -61,6 +62,7 @@ const ContractAbi = ({ abi, addressHash, tab }: Props) => { ...@@ -61,6 +62,7 @@ const ContractAbi = ({ abi, addressHash, tab }: Props) => {
id={ id } id={ id }
index={ index } index={ index }
addressHash={ addressHash } addressHash={ addressHash }
sourceAddress={ sourceAddress }
tab={ tab } tab={ tab }
onSubmit={ handleFormSubmit } onSubmit={ handleFormSubmit }
/> />
......
...@@ -19,11 +19,12 @@ interface Props { ...@@ -19,11 +19,12 @@ interface Props {
index: number; index: number;
id: number; id: number;
addressHash: string; addressHash: string;
sourceAddress?: string;
tab: string; tab: string;
onSubmit: FormSubmitHandler; onSubmit: FormSubmitHandler;
} }
const ContractAbiItem = ({ data, index, id, addressHash, tab, onSubmit }: Props) => { const ContractAbiItem = ({ data, index, id, addressHash, sourceAddress, tab, onSubmit }: Props) => {
const [ attempt, setAttempt ] = React.useState(0); const [ attempt, setAttempt ] = React.useState(0);
const url = React.useMemo(() => { const url = React.useMemo(() => {
...@@ -36,10 +37,11 @@ const ContractAbiItem = ({ data, index, id, addressHash, tab, onSubmit }: Props) ...@@ -36,10 +37,11 @@ const ContractAbiItem = ({ data, index, id, addressHash, tab, onSubmit }: Props)
query: { query: {
hash: addressHash ?? '', hash: addressHash ?? '',
tab, tab,
...(sourceAddress ? { source_address: sourceAddress } : {}),
}, },
hash: data.method_id, hash: data.method_id,
}); });
}, [ addressHash, data, tab ]); }, [ addressHash, data, tab, sourceAddress ]);
const handleCopyLinkClick = React.useCallback((event: React.MouseEvent) => { const handleCopyLinkClick = React.useCallback((event: React.MouseEvent) => {
event.stopPropagation(); event.stopPropagation();
......
import { Flex, Select, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { AddressImplementation } from 'types/api/addressParams';
import { route } from 'nextjs-routes';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import LinkNewTab from 'ui/shared/links/LinkNewTab';
interface Props {
selectedItem: AddressImplementation;
onItemSelect: (event: React.ChangeEvent<HTMLSelectElement>) => void;
implementations: Array<AddressImplementation>;
isLoading?: boolean;
}
const ContractImplementationAddress = ({ selectedItem, onItemSelect, implementations, isLoading }: Props) => {
if (isLoading) {
return <Skeleton mb={ 6 } h={ 6 } w={{ base: '300px', lg: '500px' }}/>;
}
if (implementations.length === 0) {
return null;
}
if (implementations.length === 1) {
return (
<Flex mb={ 6 } flexWrap="wrap" columnGap={ 2 } rowGap={ 2 }>
<span>Implementation address:</span>
<AddressEntity
address={{ hash: implementations[0].address, is_contract: true, is_verified: true }}
/>
</Flex>
);
}
return (
<Flex mb={ 6 } flexWrap="wrap" columnGap={ 2 } rowGap={ 2 } alignItems="center">
<span>Implementation address:</span>
<Select
size="xs"
value={ selectedItem.address }
onChange={ onItemSelect }
w="auto"
fontWeight={ 600 }
borderRadius="base"
>
{ implementations.map((implementation) => (
<option key={ implementation.address } value={ implementation.address }>
{ implementation.name }
</option>
)) }
</Select>
<CopyToClipboard text={ selectedItem.address } ml={ 1 }/>
<LinkNewTab
label="Open contract details page in new tab"
href={ route({ pathname: '/address/[hash]', query: { hash: selectedItem.address, tab: 'contract' } }) }
/>
</Flex>
);
};
export default React.memo(ContractImplementationAddress);
...@@ -14,9 +14,10 @@ interface Props { ...@@ -14,9 +14,10 @@ interface Props {
isLoading?: boolean; isLoading?: boolean;
isError?: boolean; isError?: boolean;
type: MethodType; type: MethodType;
sourceAddress?: string;
} }
const ContractMethods = ({ abi, isLoading, isError, type }: Props) => { const ContractMethods = ({ abi, isLoading, isError, type, sourceAddress }: Props) => {
const router = useRouter(); const router = useRouter();
...@@ -32,10 +33,11 @@ const ContractMethods = ({ abi, isLoading, isError, type }: Props) => { ...@@ -32,10 +33,11 @@ const ContractMethods = ({ abi, isLoading, isError, type }: Props) => {
} }
if (abi.length === 0) { if (abi.length === 0) {
return <span>No public { type } functions were found for this contract.</span>; const typeText = type === 'all' ? '' : type;
return <span>No public { typeText } functions were found for this contract.</span>;
} }
return <ContractAbi abi={ abi } tab={ tab } addressHash={ addressHash }/>; return <ContractAbi abi={ abi } tab={ tab } addressHash={ addressHash } sourceAddress={ sourceAddress }/>;
}; };
export default React.memo(ContractMethods); export default React.memo(ContractMethods);
import { Box } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { SmartContractMudSystemItem } from 'types/api/contract';
import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import type { Item } from '../ContractSourceAddressSelector';
import ContractSourceAddressSelector from '../ContractSourceAddressSelector';
import ContractConnectWallet from './ContractConnectWallet';
import ContractMethods from './ContractMethods';
import { enrichWithMethodId, isMethod } from './utils';
interface Props {
items: Array<SmartContractMudSystemItem>;
}
const ContractMethodsMudSystem = ({ items }: Props) => {
const router = useRouter();
const addressHash = getQueryParamString(router.query.hash);
const sourceAddress = getQueryParamString(router.query.source_address);
const [ selectedItem, setSelectedItem ] = React.useState(items.find((item) => item.address === sourceAddress) || items[0]);
const systemInfoQuery = useApiQuery('contract_mud_system_info', {
pathParams: { hash: addressHash, system_address: selectedItem.address },
queryOptions: {
enabled: Boolean(selectedItem?.address),
refetchOnMount: false,
},
});
const handleItemSelect = React.useCallback((item: Item) => {
setSelectedItem(item as SmartContractMudSystemItem);
}, []);
if (items.length === 0) {
return <span>No MUD System found for this contract.</span>;
}
const abi = systemInfoQuery.data?.abi?.filter(isMethod).map(enrichWithMethodId) || [];
return (
<Box>
<ContractConnectWallet/>
<ContractSourceAddressSelector
items={ items }
selectedItem={ selectedItem }
onItemSelect={ handleItemSelect }
label="System address"
mb={ 6 }
/>
<ContractMethods
key={ selectedItem.address }
abi={ abi }
isLoading={ systemInfoQuery.isPending }
isError={ systemInfoQuery.isError }
sourceAddress={ selectedItem.address }
type="all"
/>
</Box>
);
};
export default React.memo(ContractMethodsMudSystem);
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { MethodType } from './types'; import type { MethodType } from './types';
import type { AddressImplementation } from 'types/api/addressParams'; import type { AddressImplementation } from 'types/api/addressParams';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import ContractSourceAddressSelector from '../ContractSourceAddressSelector';
import ContractConnectWallet from './ContractConnectWallet'; import ContractConnectWallet from './ContractConnectWallet';
import ContractImplementationAddress from './ContractImplementationAddress';
import ContractMethods from './ContractMethods'; import ContractMethods from './ContractMethods';
import { isReadMethod, isWriteMethod } from './utils'; import { enrichWithMethodId, isReadMethod, isWriteMethod } from './utils';
interface Props { interface Props {
type: MethodType; type: MethodType;
...@@ -18,8 +20,10 @@ interface Props { ...@@ -18,8 +20,10 @@ interface Props {
} }
const ContractMethodsProxy = ({ type, implementations, isLoading: isInitialLoading }: Props) => { const ContractMethodsProxy = ({ type, implementations, isLoading: isInitialLoading }: Props) => {
const router = useRouter();
const contractAddress = getQueryParamString(router.query.source_address);
const [ selectedItem, setSelectedItem ] = React.useState(implementations[0]); const [ selectedItem, setSelectedItem ] = React.useState(implementations.find((item) => item.address === contractAddress) || implementations[0]);
const contractQuery = useApiQuery('contract', { const contractQuery = useApiQuery('contract', {
pathParams: { hash: selectedItem.address }, pathParams: { hash: selectedItem.address },
...@@ -29,29 +33,25 @@ const ContractMethodsProxy = ({ type, implementations, isLoading: isInitialLoadi ...@@ -29,29 +33,25 @@ const ContractMethodsProxy = ({ type, implementations, isLoading: isInitialLoadi
}, },
}); });
const handleItemSelect = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => { const abi = contractQuery.data?.abi?.filter(type === 'read' ? isReadMethod : isWriteMethod).map(enrichWithMethodId) || [];
const nextOption = implementations.find(({ address }) => address === event.target.value);
if (nextOption) {
setSelectedItem(nextOption);
}
}, [ implementations ]);
const abi = contractQuery.data?.abi?.filter(type === 'read' ? isReadMethod : isWriteMethod) || [];
return ( return (
<Box> <Box>
<ContractConnectWallet isLoading={ isInitialLoading }/> <ContractConnectWallet isLoading={ isInitialLoading }/>
<ContractImplementationAddress <ContractSourceAddressSelector
implementations={ implementations } items={ implementations }
selectedItem={ selectedItem } selectedItem={ selectedItem }
onItemSelect={ handleItemSelect } onItemSelect={ setSelectedItem }
isLoading={ isInitialLoading } isLoading={ isInitialLoading }
label="Implementation address"
mb={ 6 }
/> />
<ContractMethods <ContractMethods
key={ selectedItem.address } key={ selectedItem.address }
abi={ abi } abi={ abi }
isLoading={ isInitialLoading || contractQuery.isPending } isLoading={ isInitialLoading || contractQuery.isPending }
isError={ contractQuery.isError } isError={ contractQuery.isError }
sourceAddress={ selectedItem.address }
type={ type } type={ type }
/> />
</Box> </Box>
......
import { Button, Tooltip } from '@chakra-ui/react';
import React from 'react';
import useAccount from 'lib/web3/useAccount';
interface Props {
onClick: (address: string) => void;
isDisabled?: boolean;
}
const ContractMethodAddressButton = ({ onClick, isDisabled }: Props) => {
const { address } = useAccount();
const handleClick = React.useCallback(() => {
address && onClick(address);
}, [ address, onClick ]);
return (
<Tooltip label={ !address ? 'Connect your wallet to enter your address.' : undefined }>
<Button
variant="subtle"
colorScheme="gray"
size="xs"
fontSize="normal"
fontWeight={ 500 }
ml={ 1 }
onClick={ handleClick }
isDisabled={ isDisabled || !address }
>
Self
</Button>
</Tooltip>
);
};
export default React.memo(ContractMethodAddressButton);
import { Box, Flex, FormControl, Input, InputGroup, InputRightElement, chakra, useColorModeValue } from '@chakra-ui/react'; import { Box, Button, Flex, FormControl, Input, InputGroup, InputRightElement, chakra, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { useController, useFormContext } from 'react-hook-form'; import { useController, useFormContext } from 'react-hook-form';
import { NumericFormat } from 'react-number-format'; import { NumericFormat } from 'react-number-format';
import type { ContractAbiItemInput } from '../types'; import type { ContractAbiItemInput } from '../types';
import { HOUR, SECOND } from 'lib/consts';
import ClearButton from 'ui/shared/ClearButton'; import ClearButton from 'ui/shared/ClearButton';
import ContractMethodAddressButton from './ContractMethodAddressButton';
import ContractMethodFieldLabel from './ContractMethodFieldLabel'; import ContractMethodFieldLabel from './ContractMethodFieldLabel';
import ContractMethodMultiplyButton from './ContractMethodMultiplyButton'; import ContractMethodMultiplyButton from './ContractMethodMultiplyButton';
import useFormatFieldValue from './useFormatFieldValue'; import useFormatFieldValue from './useFormatFieldValue';
import useValidateField from './useValidateField'; import useValidateField from './useValidateField';
import { matchInt } from './utils'; import { matchInt } from './utils';
const TIMESTAMP_BUTTON_REGEXP = /time|deadline|expiration|expiry/i;
interface Props { interface Props {
data: ContractAbiItemInput; data: ContractAbiItemInput;
hideLabel?: boolean; hideLabel?: boolean;
...@@ -30,6 +34,7 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi ...@@ -30,6 +34,7 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi
const isOptional = isOptionalProp || isNativeCoin; const isOptional = isOptionalProp || isNativeCoin;
const argTypeMatchInt = React.useMemo(() => matchInt(data.type), [ data.type ]); const argTypeMatchInt = React.useMemo(() => matchInt(data.type), [ data.type ]);
const hasTimestampButton = React.useMemo(() => TIMESTAMP_BUTTON_REGEXP.test(data.name || ''), [ data.name ]);
const validate = useValidateField({ isOptional, argType: data.type, argTypeMatchInt }); const validate = useValidateField({ isOptional, argType: data.type, argTypeMatchInt });
const format = useFormatFieldValue({ argType: data.type, argTypeMatchInt }); const format = useFormatFieldValue({ argType: data.type, argTypeMatchInt });
...@@ -56,9 +61,28 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi ...@@ -56,9 +61,28 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi
const zeroes = Array(power).fill('0').join(''); const zeroes = Array(power).fill('0').join('');
const value = getValues(name); const value = getValues(name);
const newValue = format(value ? value + zeroes : '1' + zeroes); const newValue = format(value ? value + zeroes : '1' + zeroes);
setValue(name, newValue); setValue(name, newValue, { shouldValidate: true });
}, [ format, getValues, name, setValue ]); }, [ format, getValues, name, setValue ]);
const handleMaxIntButtonClick = React.useCallback(() => {
if (!argTypeMatchInt) {
return;
}
const newValue = format(argTypeMatchInt.max.toString());
setValue(name, newValue, { shouldValidate: true });
}, [ format, name, setValue, argTypeMatchInt ]);
const handleAddressButtonClick = React.useCallback((address: string) => {
const newValue = format(address);
setValue(name, newValue, { shouldValidate: true });
}, [ format, name, setValue ]);
const handleTimestampButtonClick = React.useCallback(() => {
const newValue = format(String(Math.floor((Date.now() + HOUR) / SECOND)));
setValue(name, newValue, { shouldValidate: true });
}, [ format, name, setValue ]);
const error = fieldState.error; const error = fieldState.error;
return ( return (
...@@ -90,11 +114,40 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi ...@@ -90,11 +114,40 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi
isInvalid={ Boolean(error) } isInvalid={ Boolean(error) }
placeholder={ data.type } placeholder={ data.type }
autoComplete="off" autoComplete="off"
data-1p-ignore
bgColor={ inputBgColor } bgColor={ inputBgColor }
paddingRight={ hasMultiplyButton ? '120px' : '40px' } paddingRight={ hasMultiplyButton ? '120px' : '40px' }
/> />
<InputRightElement w="auto" right={ 1 }> <InputRightElement w="auto" right={ 1 } bgColor={ inputBgColor } h="calc(100% - 4px)" top="2px" borderRadius="base">
{ field.value !== undefined && field.value !== '' && <ClearButton onClick={ handleClear } isDisabled={ isDisabled }/> } { field.value !== undefined && field.value !== '' && <ClearButton onClick={ handleClear } isDisabled={ isDisabled }/> }
{ data.type === 'address' && <ContractMethodAddressButton onClick={ handleAddressButtonClick } isDisabled={ isDisabled }/> }
{ argTypeMatchInt && (hasTimestampButton ? (
<Button
variant="subtle"
colorScheme="gray"
size="xs"
fontSize="normal"
fontWeight={ 500 }
ml={ 1 }
onClick={ handleTimestampButtonClick }
isDisabled={ isDisabled }
>
Now+1h
</Button>
) : (
<Button
variant="subtle"
colorScheme="gray"
size="xs"
fontSize="normal"
fontWeight={ 500 }
ml={ 1 }
onClick={ handleMaxIntButtonClick }
isDisabled={ isDisabled }
>
Max
</Button>
)) }
{ hasMultiplyButton && <ContractMethodMultiplyButton onClick={ handleMultiplyButtonClick } isDisabled={ isDisabled }/> } { hasMultiplyButton && <ContractMethodMultiplyButton onClick={ handleMultiplyButtonClick } isDisabled={ isDisabled }/> }
</InputRightElement> </InputRightElement>
</InputGroup> </InputGroup>
......
...@@ -62,7 +62,7 @@ const data: SmartContractMethod = { ...@@ -62,7 +62,7 @@ const data: SmartContractMethod = {
// LITERALS // LITERALS
{ internalType: 'bytes32', name: 'fulfillerConduitKey', type: 'bytes32' }, { internalType: 'bytes32', name: 'fulfillerConduitKey', type: 'bytes32' },
{ internalType: 'address', name: 'recipient', type: 'address' }, { internalType: 'address', name: 'recipient', type: 'address' },
{ internalType: 'uint256', name: 'maximumFulfilled', type: 'uint256' }, { internalType: 'uint256', name: 'startTime', type: 'uint256' },
{ internalType: 'int8[]', name: 'criteriaProof', type: 'int8[]' }, { internalType: 'int8[]', name: 'criteriaProof', type: 'int8[]' },
], ],
method_id: '87201b41', method_id: '87201b41',
......
import { Box, Button, Flex, Tooltip, chakra } from '@chakra-ui/react'; import { Box, Button, Flex, Tooltip, chakra, useDisclosure } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm, FormProvider } from 'react-hook-form'; import { useForm, FormProvider } from 'react-hook-form';
import type { AbiFunction } from 'viem'; import { encodeFunctionData, type AbiFunction } from 'viem';
import type { FormSubmitHandler, FormSubmitResult, MethodCallStrategy, SmartContractMethod } from '../types'; import type { FormSubmitHandler, FormSubmitResult, MethodCallStrategy, SmartContractMethod } from '../types';
import config from 'configs/app'; import config from 'configs/app';
import { SECOND } from 'lib/consts';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
...@@ -43,17 +44,42 @@ const ContractMethodForm = ({ data, attempt, onSubmit, onReset, isOpen }: Props) ...@@ -43,17 +44,42 @@ const ContractMethodForm = ({ data, attempt, onSubmit, onReset, isOpen }: Props)
shouldUnregister: true, shouldUnregister: true,
}); });
const calldataButtonTooltip = useDisclosure();
const handleButtonClick = React.useCallback((event: React.MouseEvent) => { const handleButtonClick = React.useCallback((event: React.MouseEvent) => {
const callStrategy = event?.currentTarget.getAttribute('data-call-strategy'); const callStrategy = event?.currentTarget.getAttribute('data-call-strategy');
setCallStrategy(callStrategy as MethodCallStrategy); setCallStrategy(callStrategy as MethodCallStrategy);
callStrategyRef.current = callStrategy as MethodCallStrategy; callStrategyRef.current = callStrategy as MethodCallStrategy;
}, []);
if (callStrategy === 'copy_calldata') {
calldataButtonTooltip.onOpen();
window.setTimeout(() => {
calldataButtonTooltip.onClose();
}, SECOND);
}
}, [ calldataButtonTooltip ]);
const methodType = isReadMethod(data) ? 'read' : 'write'; const methodType = isReadMethod(data) ? 'read' : 'write';
const onFormSubmit: SubmitHandler<ContractMethodFormFields> = React.useCallback(async(formData) => { const onFormSubmit: SubmitHandler<ContractMethodFormFields> = React.useCallback(async(formData) => {
const args = transformFormDataToMethodArgs(formData); const args = transformFormDataToMethodArgs(formData);
if (callStrategyRef.current === 'copy_calldata') {
if (!('name' in data) || !data.name) {
return;
}
const callData = encodeFunctionData({
abi: [ data ],
functionName: data.name,
// since we have added additional input for native coin value
// we need to slice it off
args: args.slice(0, data.inputs.length),
});
await navigator.clipboard.writeText(callData);
return;
}
setResult(undefined); setResult(undefined);
setLoading(true); setLoading(true);
...@@ -166,6 +192,50 @@ const ContractMethodForm = ({ data, attempt, onSubmit, onReset, isOpen }: Props) ...@@ -166,6 +192,50 @@ const ContractMethodForm = ({ data, attempt, onSubmit, onReset, isOpen }: Props)
); );
})(); })();
const copyCallDataButton = (() => {
if (inputs.length === 0) {
return null;
}
if (inputs.length === 1) {
const [ input ] = inputs;
if ('fieldType' in input && input.fieldType === 'native_coin') {
return null;
}
}
const text = 'Copy calldata';
const buttonCallStrategy = 'copy_calldata';
const isDisabled = isLoading || !formApi.formState.isValid;
return (
<Tooltip
isDisabled={ isDisabled }
label="Copied"
closeDelay={ SECOND }
isOpen={ calldataButtonTooltip.isOpen }
onClose={ calldataButtonTooltip.onClose }
>
<Button
isLoading={ callStrategy === buttonCallStrategy && isLoading }
isDisabled={ isDisabled }
onClick={ handleButtonClick }
loadingText={ text }
variant="outline"
size="sm"
flexShrink={ 0 }
width="min-content"
px={ 4 }
ml={ 3 }
type="submit"
data-call-strategy={ buttonCallStrategy }
>
{ text }
</Button>
</Tooltip>
);
})();
return ( return (
<Box> <Box>
<FormProvider { ...formApi }> <FormProvider { ...formApi }>
...@@ -214,6 +284,7 @@ const ContractMethodForm = ({ data, attempt, onSubmit, onReset, isOpen }: Props) ...@@ -214,6 +284,7 @@ const ContractMethodForm = ({ data, attempt, onSubmit, onReset, isOpen }: Props)
</Flex> </Flex>
{ secondaryButton } { secondaryButton }
{ primaryButton } { primaryButton }
{ copyCallDataButton }
{ result && !isLoading && ( { result && !isLoading && (
<Button <Button
variant="simple" variant="simple"
...@@ -223,7 +294,7 @@ const ContractMethodForm = ({ data, attempt, onSubmit, onReset, isOpen }: Props) ...@@ -223,7 +294,7 @@ const ContractMethodForm = ({ data, attempt, onSubmit, onReset, isOpen }: Props)
ml={ 1 } ml={ 1 }
> >
<IconSvg name="repeat" boxSize={ 5 } mr={ 1 }/> <IconSvg name="repeat" boxSize={ 5 } mr={ 1 }/>
Reset Reset
</Button> </Button>
) } ) }
</chakra.form> </chakra.form>
......
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ import {
ListItem, ListItem,
useDisclosure, useDisclosure,
Input, Input,
useColorModeValue,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
...@@ -26,6 +27,8 @@ const ContractMethodMultiplyButton = ({ onClick, isDisabled }: Props) => { ...@@ -26,6 +27,8 @@ const ContractMethodMultiplyButton = ({ onClick, isDisabled }: Props) => {
const [ customValue, setCustomValue ] = React.useState<number>(); const [ customValue, setCustomValue ] = React.useState<number>();
const { isOpen, onToggle, onClose } = useDisclosure(); const { isOpen, onToggle, onClose } = useDisclosure();
const dividerColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const handleOptionClick = React.useCallback((event: React.MouseEvent) => { const handleOptionClick = React.useCallback((event: React.MouseEvent) => {
const id = Number((event.currentTarget as HTMLDivElement).getAttribute('data-id')); const id = Number((event.currentTarget as HTMLDivElement).getAttribute('data-id'));
if (!Object.is(id, NaN)) { if (!Object.is(id, NaN)) {
...@@ -60,6 +63,8 @@ const ContractMethodMultiplyButton = ({ onClick, isDisabled }: Props) => { ...@@ -60,6 +63,8 @@ const ContractMethodMultiplyButton = ({ onClick, isDisabled }: Props) => {
display="inline" display="inline"
onClick={ handleButtonClick } onClick={ handleButtonClick }
isDisabled={ isDisabled } isDisabled={ isDisabled }
borderBottomRightRadius={ 0 }
borderTopRightRadius={ 0 }
> >
{ times } { times }
<chakra.span>10</chakra.span> <chakra.span>10</chakra.span>
...@@ -73,11 +78,14 @@ const ContractMethodMultiplyButton = ({ onClick, isDisabled }: Props) => { ...@@ -73,11 +78,14 @@ const ContractMethodMultiplyButton = ({ onClick, isDisabled }: Props) => {
colorScheme="gray" colorScheme="gray"
size="xs" size="xs"
cursor="pointer" cursor="pointer"
ml={ 1 }
p={ 0 } p={ 0 }
onClick={ onToggle } onClick={ onToggle }
isActive={ isOpen } isActive={ isOpen }
isDisabled={ isDisabled } isDisabled={ isDisabled }
borderBottomLeftRadius={ 0 }
borderTopLeftRadius={ 0 }
borderLeftWidth="1px"
borderLeftColor={ dividerColor }
> >
<IconSvg <IconSvg
name="arrows/east-mini" name="arrows/east-mini"
......
...@@ -2,8 +2,8 @@ import type { AbiFunction, AbiFallback, AbiReceive } from 'abitype'; ...@@ -2,8 +2,8 @@ import type { AbiFunction, AbiFallback, AbiReceive } from 'abitype';
export type ContractAbiItemInput = AbiFunction['inputs'][number] & { fieldType?: 'native_coin' }; export type ContractAbiItemInput = AbiFunction['inputs'][number] & { fieldType?: 'native_coin' };
export type MethodType = 'read' | 'write'; export type MethodType = 'read' | 'write' | 'all';
export type MethodCallStrategy = 'read' | 'write' | 'simulate'; export type MethodCallStrategy = 'read' | 'write' | 'simulate' | 'copy_calldata';
export type ResultViewMode = 'preview' | 'result'; export type ResultViewMode = 'preview' | 'result';
export type SmartContractMethodCustomFields = { method_id: string } | { is_invalid: boolean }; export type SmartContractMethodCustomFields = { method_id: string } | { is_invalid: boolean };
......
import type { Abi } from 'abitype'; import type { Abi, AbiFallback, AbiReceive } from 'abitype';
import type { AbiFunction } from 'viem'; import type { AbiFunction } from 'viem';
import { toFunctionSelector } from 'viem'; import { toFunctionSelector } from 'viem';
import type { SmartContractMethodCustomFields, SmartContractMethodRead, SmartContractMethodWrite } from './types'; import type { SmartContractMethod, SmartContractMethodRead, SmartContractMethodWrite } from './types';
export const getNativeCoinValue = (value: unknown) => { export const getNativeCoinValue = (value: unknown) => {
if (typeof value !== 'string') { if (typeof value !== 'string') {
...@@ -17,6 +17,9 @@ interface DividedAbi { ...@@ -17,6 +17,9 @@ interface DividedAbi {
write: Array<SmartContractMethodWrite>; write: Array<SmartContractMethodWrite>;
} }
export const isMethod = (method: Abi[number]): method is SmartContractMethod =>
(method.type === 'function' || method.type === 'fallback' || method.type === 'receive');
export const isReadMethod = (method: Abi[number]): method is SmartContractMethodRead => export const isReadMethod = (method: Abi[number]): method is SmartContractMethodRead =>
method.type === 'function' && ( method.type === 'function' && (
method.constant || method.stateMutability === 'view' || method.stateMutability === 'pure' method.constant || method.stateMutability === 'view' || method.stateMutability === 'pure'
...@@ -26,13 +29,19 @@ export const isWriteMethod = (method: Abi[number]): method is SmartContractMetho ...@@ -26,13 +29,19 @@ export const isWriteMethod = (method: Abi[number]): method is SmartContractMetho
(method.type === 'function' || method.type === 'fallback' || method.type === 'receive') && (method.type === 'function' || method.type === 'fallback' || method.type === 'receive') &&
!isReadMethod(method); !isReadMethod(method);
const enrichWithMethodId = (method: AbiFunction): SmartContractMethodCustomFields => { export const enrichWithMethodId = (method: AbiFunction | AbiFallback | AbiReceive): SmartContractMethod => {
if (method.type !== 'function') {
return method;
}
try { try {
return { return {
...method,
method_id: toFunctionSelector(method).slice(2), method_id: toFunctionSelector(method).slice(2),
}; };
} catch (error) { } catch (error) {
return { return {
...method,
is_invalid: true, is_invalid: true,
}; };
} }
...@@ -42,22 +51,9 @@ export function divideAbiIntoMethodTypes(abi: Abi): DividedAbi { ...@@ -42,22 +51,9 @@ export function divideAbiIntoMethodTypes(abi: Abi): DividedAbi {
return { return {
read: abi read: abi
.filter(isReadMethod) .filter(isReadMethod)
.map((method) => ({ .map(enrichWithMethodId) as Array<SmartContractMethodRead>,
...method,
...enrichWithMethodId(method),
})),
write: abi write: abi
.filter(isWriteMethod) .filter(isWriteMethod)
.map((method) => { .map(enrichWithMethodId) as Array<SmartContractMethodWrite>,
if (method.type !== 'function') {
return method;
}
return {
...method,
...enrichWithMethodId(method),
};
}),
}; };
} }
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useContractTabs from 'lib/hooks/useContractTabs';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
const ContractCode = () => { import useContractTabs from '../useContractTabs';
const ContractDetails = () => {
const router = useRouter(); const router = useRouter();
const hash = getQueryParamString(router.query.hash); const hash = getQueryParamString(router.query.hash);
const addressQuery = useApiQuery('address', { pathParams: { hash } }); const addressQuery = useApiQuery('address', { pathParams: { hash } });
...@@ -13,4 +14,4 @@ const ContractCode = () => { ...@@ -13,4 +14,4 @@ const ContractCode = () => {
return content ?? null; return content ?? null;
}; };
export default ContractCode; export default ContractDetails;
import { Alert, Box, Flex } from '@chakra-ui/react';
import React from 'react';
import type { SmartContract } from 'types/api/contract';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import RawDataSnippet from 'ui/shared/RawDataSnippet';
import ContractDetailsVerificationButton from './ContractDetailsVerificationButton';
import ContractSourceCode from './ContractSourceCode';
import type { CONTRACT_DETAILS_TAB_IDS } from './utils';
interface Tab {
id: typeof CONTRACT_DETAILS_TAB_IDS[number];
title: string;
component: React.ReactNode;
}
interface Props {
data: SmartContract | undefined;
isLoading: boolean;
addressHash: string;
sourceAddress: string;
}
export default function useContractDetailsTabs({ data, isLoading, addressHash, sourceAddress }: Props): Array<Tab> {
const constructorArgs = React.useMemo(() => {
if (!data?.decoded_constructor_args) {
return data?.constructor_args;
}
const decoded = data.decoded_constructor_args
.map(([ value, { name, type } ], index) => {
const valueEl = type === 'address' ? (
<AddressEntity
address={{ hash: value }}
noIcon
display="inline-flex"
maxW="100%"
/>
) : <span>{ value }</span>;
return (
<Box key={ index }>
<span>Arg [{ index }] { name || '' } ({ type }): </span>
{ valueEl }
</Box>
);
});
return (
<>
<span>{ data.constructor_args }</span>
<br/><br/>
{ decoded }
</>
);
}, [ data?.decoded_constructor_args, data?.constructor_args ]);
const canBeVerified = !data?.is_self_destructed && !data?.is_verified;
return React.useMemo(() => {
const verificationButton = (
<ContractDetailsVerificationButton
isLoading={ isLoading }
addressHash={ addressHash }
isPartiallyVerified={ Boolean(data?.is_partially_verified) }
/>
);
return [
(constructorArgs || data?.source_code) ? {
id: 'contract_source_code' as const,
title: 'Code',
component: (
<Flex flexDir="column" rowGap={ 6 }>
{ constructorArgs && (
<RawDataSnippet
data={ constructorArgs }
title="Constructor Arguments"
textareaMaxHeight="200px"
isLoading={ isLoading }
/>
) }
{ data?.source_code && (
<ContractSourceCode
data={ data }
isLoading={ isLoading }
sourceAddress={ sourceAddress }
/>
) }
</Flex>
),
} : undefined,
data?.compiler_settings ? {
id: 'contract_compiler' as const,
title: 'Compiler',
component: (
<RawDataSnippet
data={ JSON.stringify(data.compiler_settings, undefined, 4) }
title="Compiler Settings"
textareaMaxHeight="600px"
isLoading={ isLoading }
/>
),
} : undefined,
data?.abi ? {
id: 'contract_abi' as const,
title: 'ABI',
component: (
<RawDataSnippet
data={ JSON.stringify(data.abi, undefined, 4) }
title="Contract ABI"
textareaMaxHeight="600px"
isLoading={ isLoading }
/>
),
} : undefined,
(data?.creation_bytecode || data?.deployed_bytecode) ? {
id: 'contract_bytecode' as const,
title: 'ByteCode',
component: (
<Flex flexDir="column" rowGap={ 6 }>
{ data?.creation_bytecode && (
<RawDataSnippet
data={ data.creation_bytecode }
title="Contract creation code"
rightSlot={ canBeVerified ? verificationButton : null }
beforeSlot={ data.is_self_destructed ? (
<Alert status="info" whiteSpace="pre-wrap" mb={ 3 }>
Contracts that self destruct in their constructors have no contract code published and cannot be verified.
Displaying the init data provided of the creating transaction.
</Alert>
) : null }
textareaMaxHeight="300px"
isLoading={ isLoading }
/>
) }
{ data?.deployed_bytecode && (
<RawDataSnippet
data={ data.deployed_bytecode }
title="Deployed ByteCode"
rightSlot={ !data?.creation_bytecode && canBeVerified ? verificationButton : null }
textareaMaxHeight="300px"
isLoading={ isLoading }
/>
) }
</Flex>
),
} : undefined,
].filter(Boolean);
}, [ isLoading, addressHash, data, constructorArgs, sourceAddress, canBeVerified ]);
}
...@@ -8,28 +8,22 @@ import * as cookies from 'lib/cookies'; ...@@ -8,28 +8,22 @@ import * as cookies from 'lib/cookies';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import * as stubs from 'stubs/contract'; import * as stubs from 'stubs/contract';
import ContractCode from 'ui/address/contract/ContractCode'; import ContractDetails from 'ui/address/contract/ContractDetails';
import ContractMethodsCustom from 'ui/address/contract/methods/ContractMethodsCustom'; import ContractMethodsCustom from 'ui/address/contract/methods/ContractMethodsCustom';
import ContractMethodsMudSystem from 'ui/address/contract/methods/ContractMethodsMudSystem';
import ContractMethodsProxy from 'ui/address/contract/methods/ContractMethodsProxy'; import ContractMethodsProxy from 'ui/address/contract/methods/ContractMethodsProxy';
import ContractMethodsRegular from 'ui/address/contract/methods/ContractMethodsRegular'; import ContractMethodsRegular from 'ui/address/contract/methods/ContractMethodsRegular';
import { divideAbiIntoMethodTypes } from 'ui/address/contract/methods/utils'; import { divideAbiIntoMethodTypes } from 'ui/address/contract/methods/utils';
import ContentLoader from 'ui/shared/ContentLoader';
const CONTRACT_TAB_IDS = [ import type { CONTRACT_MAIN_TAB_IDS } from './utils';
'contract_code', import { CONTRACT_DETAILS_TAB_IDS, CONTRACT_TAB_IDS } from './utils';
'read_contract',
'read_contract_rpc',
'read_proxy',
'read_custom_methods',
'write_contract',
'write_contract_rpc',
'write_proxy',
'write_custom_methods',
] as const;
interface ContractTab { interface ContractTab {
id: typeof CONTRACT_TAB_IDS[number]; id: typeof CONTRACT_MAIN_TAB_IDS[number];
title: string; title: string;
component: JSX.Element; component: JSX.Element;
subTabs?: Array<string>;
} }
interface ReturnType { interface ReturnType {
...@@ -37,7 +31,7 @@ interface ReturnType { ...@@ -37,7 +31,7 @@ interface ReturnType {
isLoading: boolean; isLoading: boolean;
} }
export default function useContractTabs(data: Address | undefined, isPlaceholderData: boolean): ReturnType { export default function useContractTabs(data: Address | undefined, isPlaceholderData: boolean, hasMudTab?: boolean): ReturnType {
const [ isQueryEnabled, setIsQueryEnabled ] = React.useState(false); const [ isQueryEnabled, setIsQueryEnabled ] = React.useState(false);
const router = useRouter(); const router = useRouter();
...@@ -65,6 +59,15 @@ export default function useContractTabs(data: Address | undefined, isPlaceholder ...@@ -65,6 +59,15 @@ export default function useContractTabs(data: Address | undefined, isPlaceholder
}, },
}); });
const mudSystemsQuery = useApiQuery('contract_mud_systems', {
pathParams: { hash: data?.hash },
queryOptions: {
enabled: isEnabled && isQueryEnabled && hasMudTab,
refetchOnMount: false,
placeholderData: stubs.MUD_SYSTEMS,
},
});
const channel = useSocketChannel({ const channel = useSocketChannel({
topic: `addresses:${ data?.hash?.toLowerCase() }`, topic: `addresses:${ data?.hash?.toLowerCase() }`,
isDisabled: !isEnabled, isDisabled: !isEnabled,
...@@ -89,10 +92,11 @@ export default function useContractTabs(data: Address | undefined, isPlaceholder ...@@ -89,10 +92,11 @@ export default function useContractTabs(data: Address | undefined, isPlaceholder
return React.useMemo(() => { return React.useMemo(() => {
return { return {
tabs: [ tabs: [
{ data?.hash && {
id: 'contract_code' as const, id: 'contract_code' as const,
title: 'Code', title: 'Code',
component: <ContractCode contractQuery={ contractQuery } channel={ channel } addressHash={ data?.hash }/>, component: <ContractDetails mainContractQuery={ contractQuery } channel={ channel } addressHash={ data.hash }/>,
subTabs: CONTRACT_DETAILS_TAB_IDS as unknown as Array<string>,
}, },
methods.read.length > 0 && { methods.read.length > 0 && {
id: 'read_contract' as const, id: 'read_contract' as const,
...@@ -136,8 +140,26 @@ export default function useContractTabs(data: Address | undefined, isPlaceholder ...@@ -136,8 +140,26 @@ export default function useContractTabs(data: Address | undefined, isPlaceholder
/> />
), ),
}, },
hasMudTab && {
id: 'mud_system' as const,
title: 'MUD System',
component: mudSystemsQuery.isPlaceholderData ?
<ContentLoader/> :
<ContractMethodsMudSystem items={ mudSystemsQuery.data?.items ?? [] }/>,
},
].filter(Boolean), ].filter(Boolean),
isLoading: contractQuery.isPlaceholderData, isLoading: contractQuery.isPlaceholderData,
}; };
}, [ contractQuery, channel, data?.hash, verifiedImplementations, methods.read, methods.write, methodsCustomAbi.read, methodsCustomAbi.write ]); }, [
contractQuery,
channel,
data?.hash,
methods.read,
methods.write,
methodsCustomAbi.read,
methodsCustomAbi.write,
verifiedImplementations,
mudSystemsQuery,
hasMudTab,
]);
} }
export const CONTRACT_MAIN_TAB_IDS = [
'contract_code',
'read_contract',
'read_contract_rpc',
'read_proxy',
'read_custom_methods',
'write_contract',
'write_contract_rpc',
'write_proxy',
'write_custom_methods',
'mud_system',
] as const;
export const CONTRACT_DETAILS_TAB_IDS = [
'contract_source_code',
'contract_compiler',
'contract_abi',
'contract_bytecode',
] as const;
export const CONTRACT_TAB_IDS = (CONTRACT_MAIN_TAB_IDS as unknown as Array<string>).concat(CONTRACT_DETAILS_TAB_IDS as unknown as Array<string>);
import { Image, Tooltip } from '@chakra-ui/react';
import _capitalize from 'lodash/capitalize';
import React from 'react';
import type { MultichainProviderConfigParsed } from 'types/client/multichainProviderConfig';
import { route } from 'nextjs-routes';
import LinkExternal from 'ui/shared/links/LinkExternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
const TEMPLATE_ADDRESS = '{address}';
type Props = {
hasSingleProvider: boolean;
item: MultichainProviderConfigParsed;
addressHash: string;
onClick?: () => void;
}
const AddressMultichainButton = ({ item, addressHash, onClick, hasSingleProvider }: Props) => {
const buttonIcon = <Image src={ item.logoUrl } alt={ item.name } boxSize={ 5 } mr={ hasSingleProvider ? 2 : 0 } borderRadius="4px" overflow="hidden"/>;
const buttonContent = hasSingleProvider ? (
<>
{ buttonIcon }
{ _capitalize(item.name) }
</>
) : (
<Tooltip label={ _capitalize(item.name) }>{ buttonIcon }</Tooltip>
);
const linkProps = {
variant: hasSingleProvider ? 'subtle' as const : undefined,
display: 'flex',
alignItems: 'center',
fontSize: 'sm',
lineHeight: 5,
fontWeight: 500,
onClick,
};
try {
const portfolioUrlString = item.urlTemplate.replace(TEMPLATE_ADDRESS, addressHash);
const portfolioUrl = new URL(portfolioUrlString);
portfolioUrl.searchParams.append('utm_source', 'blockscout');
portfolioUrl.searchParams.append('utm_medium', 'address');
const dappId = item.dappId;
return typeof dappId === 'string' ? (
<LinkInternal
href={ route({ pathname: '/apps/[id]', query: { id: dappId, url: portfolioUrl.toString() } }) }
{ ...linkProps }
>
{ buttonContent }
</LinkInternal>
) : (
<LinkExternal
href={ portfolioUrl.toString() }
{ ...linkProps }
>
{ buttonContent }
</LinkExternal>
);
} catch (error) {}
return null;
};
export default AddressMultichainButton;
...@@ -23,11 +23,11 @@ test('base view', async({ render }) => { ...@@ -23,11 +23,11 @@ test('base view', async({ render }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('with multichain button internal +@dark-mode', async({ render, mockEnvs, mockAssetResponse }) => { test('with single multichain button internal +@dark-mode', async({ render, mockEnvs, mockAssetResponse }) => {
await mockEnvs([ await mockEnvs([
[ [
'NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG', 'NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG',
`{"name": "duck", "dapp_id": "duck", "url_template": "https://duck.url/{address}", "logo": "${ ICON_URL }"}` ], `[{"name": "duck", "dapp_id": "duck", "url_template": "https://duck.url/{address}", "logo": "${ ICON_URL }"}]` ],
]); ]);
await mockAssetResponse(ICON_URL, './playwright/mocks/image_svg.svg'); await mockAssetResponse(ICON_URL, './playwright/mocks/image_svg.svg');
...@@ -36,9 +36,23 @@ test('with multichain button internal +@dark-mode', async({ render, mockEnvs, mo ...@@ -36,9 +36,23 @@ test('with multichain button internal +@dark-mode', async({ render, mockEnvs, mo
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('with multichain button external', async({ render, mockEnvs, mockAssetResponse }) => { test('with single multichain button external', async({ render, mockEnvs, mockAssetResponse }) => {
await mockEnvs([ await mockEnvs([
[ 'NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG', `{"name": "duck", "url_template": "https://duck.url/{address}", "logo": "${ ICON_URL }"}` ], [ 'NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG', `[{"name": "duck", "url_template": "https://duck.url/{address}", "logo": "${ ICON_URL }"}]` ],
]);
await mockAssetResponse(ICON_URL, './playwright/mocks/image_svg.svg');
const component = await render(<AddressNetWorth addressData={ addressMock.eoa } addressHash={ ADDRESS_HASH }/>);
await expect(component).toHaveScreenshot();
});
test('with two multichain button external', async({ render, mockEnvs, mockAssetResponse }) => {
await mockEnvs([
[ 'NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG', `[
{"name": "duck", "url_template": "https://duck.url/{address}", "logo": "${ ICON_URL }"},
{"name": "duck2", "url_template": "https://duck.url/{address}", "logo": "${ ICON_URL }"}
]` ],
]); ]);
await mockAssetResponse(ICON_URL, './playwright/mocks/image_svg.svg'); await mockAssetResponse(ICON_URL, './playwright/mocks/image_svg.svg');
...@@ -51,7 +65,7 @@ test('with multichain button internal small screen', async({ render, mockEnvs, m ...@@ -51,7 +65,7 @@ test('with multichain button internal small screen', async({ render, mockEnvs, m
await mockEnvs([ await mockEnvs([
[ [
'NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG', 'NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG',
`{"name": "duck", "dapp_id": "duck", "url_template": "https://duck.url/{address}", "logo": "${ ICON_URL }"}` ], `[{"name": "duck", "dapp_id": "duck", "url_template": "https://duck.url/{address}", "logo": "${ ICON_URL }"}]` ],
]); ]);
await mockAssetResponse(ICON_URL, './playwright/mocks/image_svg.svg'); await mockAssetResponse(ICON_URL, './playwright/mocks/image_svg.svg');
......
import { Image, Skeleton, Text, Flex } from '@chakra-ui/react'; import { Skeleton, Text, Flex } from '@chakra-ui/react';
import _capitalize from 'lodash/capitalize';
import React from 'react'; import React from 'react';
import type { Address } from 'types/api/address'; import type { Address } from 'types/api/address';
import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import LinkExternal from 'ui/shared/links/LinkExternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import TextSeparator from 'ui/shared/TextSeparator'; import TextSeparator from 'ui/shared/TextSeparator';
import { getTokensTotalInfo } from '../utils/tokenUtils'; import { getTokensTotalInfo } from '../utils/tokenUtils';
import useFetchTokens from '../utils/useFetchTokens'; import useFetchTokens from '../utils/useFetchTokens';
import AddressMultichainButton from './AddressMultichainButton';
const TEMPLATE_ADDRESS = '{address}';
const multichainFeature = config.features.multichainButton; const multichainFeature = config.features.multichainButton;
...@@ -46,58 +40,30 @@ const AddressNetWorth = ({ addressData, isLoading, addressHash }: Props) => { ...@@ -46,58 +40,30 @@ const AddressNetWorth = ({ addressData, isLoading, addressHash }: Props) => {
mixpanel.logEvent(mixpanel.EventTypes.BUTTON_CLICK, { Content: 'Multichain', Source: 'address' }); mixpanel.logEvent(mixpanel.EventTypes.BUTTON_CLICK, { Content: 'Multichain', Source: 'address' });
}, []); }, []);
let multichainItem = null; let multichainItems = null;
if (multichainFeature.isEnabled && !addressData?.is_contract) { if (multichainFeature.isEnabled && !addressData?.is_contract) {
const buttonContent = ( const { providers } = multichainFeature;
const hasSingleProvider = providers.length === 1;
multichainItems = (
<> <>
{ multichainFeature.logoUrl && <TextSeparator mx={ 0 } color="gray.500"/>
<Image src={ multichainFeature.logoUrl } alt={ multichainFeature.name } boxSize={ 5 } mr={ 2 } borderRadius="4px" overflow="hidden"/> <Flex alignItems="center" gap={ 2 }>
} <Text>Multichain</Text>
{ _capitalize(multichainFeature.name) }</> { providers.map((item) => (
<AddressMultichainButton
key={ item.name }
item={ item }
addressHash={ addressHash }
onClick={ onMultichainClick }
hasSingleProvider={ hasSingleProvider }
/>
))
}
</Flex>
</>
); );
const linkProps = {
variant: 'subtle' as const,
display: 'flex',
alignItems: 'center',
fontSize: 'sm',
lineHeight: 5,
fontWeight: 500,
onClick: onMultichainClick,
};
try {
const portfolioUrlString = multichainFeature.urlTemplate.replace(TEMPLATE_ADDRESS, addressHash);
const portfolioUrl = new URL(portfolioUrlString);
portfolioUrl.searchParams.append('utm_source', 'blockscout');
portfolioUrl.searchParams.append('utm_medium', 'address');
const dappId = multichainFeature.dappId;
multichainItem = (
<>
<TextSeparator mx={ 0 } color="gray.500"/>
<Flex alignItems="center" gap={ 2 }>
<Text>Multichain</Text>
{ typeof dappId === 'string' ? (
<LinkInternal
href={ route({ pathname: '/apps/[id]', query: { id: dappId, url: portfolioUrl.toString() } }) }
{ ...linkProps }
>
{ buttonContent }
</LinkInternal>
) : (
<LinkExternal
href={ portfolioUrl.toString() }
{ ...linkProps }
>
{ buttonContent }
</LinkExternal>
) }
</Flex>
</>
);
} catch (error) {}
} }
return ( return (
...@@ -105,7 +71,7 @@ const AddressNetWorth = ({ addressData, isLoading, addressHash }: Props) => { ...@@ -105,7 +71,7 @@ const AddressNetWorth = ({ addressData, isLoading, addressHash }: Props) => {
<Text> <Text>
{ (isError || !addressData?.exchange_rate) ? 'N/A' : `${ prefix }$${ totalUsd.toFormat(2) }` } { (isError || !addressData?.exchange_rate) ? 'N/A' : `${ prefix }$${ totalUsd.toFormat(2) }` }
</Text> </Text>
{ multichainItem } { multichainItems }
</Skeleton> </Skeleton>
); );
}; };
......
...@@ -77,7 +77,7 @@ const AddressSaveOnGas = ({ gasUsed, address }: Props) => { ...@@ -77,7 +77,7 @@ const AddressSaveOnGas = ({ gasUsed, address }: Props) => {
<TextSeparator color="divider"/> <TextSeparator color="divider"/>
<Skeleton isLoaded={ !query.isPlaceholderData } display="flex" alignItems="center" columnGap={ 2 }> <Skeleton isLoaded={ !query.isPlaceholderData } display="flex" alignItems="center" columnGap={ 2 }>
<Image src="/static/gas_hawk_logo.svg" w="15px" h="20px" alt="GasHawk logo"/> <Image src="/static/gas_hawk_logo.svg" w="15px" h="20px" alt="GasHawk logo"/>
<LinkExternal href="https://www.gashawk.io" fontSize="sm"> <LinkExternal href="https://www.gashawk.io?utm_source=blockscout&utm_medium=address" fontSize="sm">
Save { percent.toLocaleString(undefined, { maximumFractionDigits: 0 }) }% with GasHawk Save { percent.toLocaleString(undefined, { maximumFractionDigits: 0 }) }% with GasHawk
</LinkExternal> </LinkExternal>
</Skeleton> </Skeleton>
......
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { AddressVerificationFormFirstStepFields, RootFields } from '../types';
import { ADDRESS_REGEXP, ADDRESS_LENGTH } from 'lib/validations/address';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Fields = RootFields & AddressVerificationFormFirstStepFields;
interface Props {
formState: FormState<Fields>;
control: Control<Fields>;
}
const AddressVerificationFieldAddress = ({ formState, control }: Props) => {
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'address'>}) => {
const error = 'address' in formState.errors ? formState.errors.address : undefined;
return (
<FormControl variant="floating" id={ field.name } isRequired size="md" bgColor="dialog_bg" mt={ 8 }>
<Input
{ ...field }
required
isInvalid={ Boolean(error) }
maxLength={ ADDRESS_LENGTH }
isDisabled={ formState.isSubmitting }
autoComplete="off"
bgColor="dialog_bg"
/>
<InputPlaceholder text="Smart contract address (0x...)" error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting ]);
return (
<Controller
name="address"
control={ control }
render={ renderControl }
rules={{ required: true, pattern: ADDRESS_REGEXP }}
/>
);
};
export default React.memo(AddressVerificationFieldAddress);
import { FormControl, Textarea } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { AddressVerificationFormSecondStepFields, RootFields } from '../types';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Fields = RootFields & AddressVerificationFormSecondStepFields;
interface Props {
formState: FormState<Fields>;
control: Control<Fields>;
}
const AddressVerificationFieldMessage = ({ formState, control }: Props) => {
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'message'>}) => {
const error = 'message' in formState.errors ? formState.errors.message : undefined;
return (
<FormControl variant="floating" id={ field.name } isRequired size="md" bgColor="dialog_bg">
<Textarea
{ ...field }
required
isInvalid={ Boolean(error) }
isReadOnly
autoComplete="off"
maxH={{ base: '140px', lg: '80px' }}
bgColor="dialog_bg"
/>
<InputPlaceholder text="Message to sign" error={ error }/>
</FormControl>
);
}, [ formState.errors ]);
return (
<Controller
defaultValue="some value"
name="message"
control={ control }
render={ renderControl }
rules={{ required: true }}
/>
);
};
export default React.memo(AddressVerificationFieldMessage);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { AddressVerificationFormSecondStepFields, RootFields } from '../types';
import { SIGNATURE_REGEXP } from 'lib/validations/signature';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Fields = RootFields & AddressVerificationFormSecondStepFields;
interface Props {
formState: FormState<Fields>;
control: Control<Fields>;
}
const AddressVerificationFieldSignature = ({ formState, control }: Props) => {
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'signature'>}) => {
const error = 'signature' in formState.errors ? formState.errors.signature : undefined;
return (
<FormControl variant="floating" id={ field.name } isRequired size="md" bgColor="dialog_bg">
<Input
{ ...field }
required
isInvalid={ Boolean(error) }
isDisabled={ formState.isSubmitting }
autoComplete="off"
bgColor="dialog_bg"
/>
<InputPlaceholder text="Signature hash" error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting ]);
return (
<Controller
name="signature"
control={ control }
render={ renderControl }
rules={{ required: true, pattern: SIGNATURE_REGEXP }}
/>
);
};
export default React.memo(AddressVerificationFieldSignature);
import { Alert, Box, Button, Flex } from '@chakra-ui/react'; import { Alert, Box, Button, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import type { import type {
AddressVerificationResponseError, AddressVerificationResponseError,
...@@ -16,10 +16,10 @@ import { route } from 'nextjs-routes'; ...@@ -16,10 +16,10 @@ import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import FormFieldAddress from 'ui/shared/forms/fields/FormFieldAddress';
import LinkInternal from 'ui/shared/links/LinkInternal'; import LinkInternal from 'ui/shared/links/LinkInternal';
import AdminSupportText from 'ui/shared/texts/AdminSupportText'; import AdminSupportText from 'ui/shared/texts/AdminSupportText';
import AddressVerificationFieldAddress from '../fields/AddressVerificationFieldAddress';
type Fields = RootFields & AddressVerificationFormFirstStepFields; type Fields = RootFields & AddressVerificationFormFirstStepFields;
interface Props { interface Props {
...@@ -34,7 +34,7 @@ const AddressVerificationStepAddress = ({ defaultAddress, onContinue }: Props) = ...@@ -34,7 +34,7 @@ const AddressVerificationStepAddress = ({ defaultAddress, onContinue }: Props) =
address: defaultAddress, address: defaultAddress,
}, },
}); });
const { handleSubmit, formState, control, setError, clearErrors, watch } = formApi; const { handleSubmit, formState, setError, clearErrors, watch } = formApi;
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const address = watch('address'); const address = watch('address');
...@@ -100,17 +100,25 @@ const AddressVerificationStepAddress = ({ defaultAddress, onContinue }: Props) = ...@@ -100,17 +100,25 @@ const AddressVerificationStepAddress = ({ defaultAddress, onContinue }: Props) =
})(); })();
return ( return (
<form noValidate onSubmit={ onSubmit }> <FormProvider { ...formApi }>
<Box>Enter the contract address you are verifying ownership for.</Box> <form noValidate onSubmit={ onSubmit }>
{ rootError && <Alert status="warning" mt={ 3 }>{ rootError }</Alert> } <Box>Enter the contract address you are verifying ownership for.</Box>
<AddressVerificationFieldAddress formState={ formState } control={ control }/> { rootError && <Alert status="warning" mt={ 3 }>{ rootError }</Alert> }
<Flex alignItems={{ base: 'flex-start', lg: 'center' }} mt={ 8 } columnGap={ 5 } rowGap={ 2 } flexDir={{ base: 'column', lg: 'row' }}> <FormFieldAddress<Fields>
<Button size="lg" type="submit" isLoading={ formState.isSubmitting } loadingText="Continue" flexShrink={ 0 }> name="address"
isRequired
bgColor="dialog_bg"
placeholder="Smart contract address (0x...)"
mt={ 8 }
/>
<Flex alignItems={{ base: 'flex-start', lg: 'center' }} mt={ 8 } columnGap={ 5 } rowGap={ 2 } flexDir={{ base: 'column', lg: 'row' }}>
<Button size="lg" type="submit" isLoading={ formState.isSubmitting } loadingText="Continue" flexShrink={ 0 }>
Continue Continue
</Button> </Button>
<AdminSupportText/> <AdminSupportText/>
</Flex> </Flex>
</form> </form>
</FormProvider>
); );
}; };
......
...@@ -2,7 +2,7 @@ import { Alert, Box, Button, chakra, Flex, Link, Radio, RadioGroup } from '@chak ...@@ -2,7 +2,7 @@ import { Alert, Box, Button, chakra, Flex, Link, Radio, RadioGroup } from '@chak
import { useWeb3Modal } from '@web3modal/wagmi/react'; import { useWeb3Modal } from '@web3modal/wagmi/react';
import React from 'react'; import React from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { useSignMessage, useAccount } from 'wagmi'; import { useSignMessage, useAccount } from 'wagmi';
import type { import type {
...@@ -19,11 +19,10 @@ import config from 'configs/app'; ...@@ -19,11 +19,10 @@ import config from 'configs/app';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import shortenString from 'lib/shortenString'; import shortenString from 'lib/shortenString';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import { SIGNATURE_REGEXP } from 'ui/shared/forms/validators/signature';
import AdminSupportText from 'ui/shared/texts/AdminSupportText'; import AdminSupportText from 'ui/shared/texts/AdminSupportText';
import AddressVerificationFieldMessage from '../fields/AddressVerificationFieldMessage';
import AddressVerificationFieldSignature from '../fields/AddressVerificationFieldSignature';
type Fields = RootFields & AddressVerificationFormSecondStepFields; type Fields = RootFields & AddressVerificationFormSecondStepFields;
type SignMethod = 'wallet' | 'manual'; type SignMethod = 'wallet' | 'manual';
...@@ -45,7 +44,7 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre ...@@ -45,7 +44,7 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
message: signingMessage, message: signingMessage,
}, },
}); });
const { handleSubmit, formState, control, setValue, getValues, setError, clearErrors, watch } = formApi; const { handleSubmit, formState, setValue, getValues, setError, clearErrors, watch } = formApi;
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
...@@ -184,51 +183,69 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre ...@@ -184,51 +183,69 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
})(); })();
return ( return (
<form noValidate onSubmit={ onSubmit }> <FormProvider { ...formApi }>
{ rootError && <Alert status="warning" mb={ 6 }>{ rootError }</Alert> } <form noValidate onSubmit={ onSubmit }>
<Box mb={ 8 }> { rootError && <Alert status="warning" mb={ 6 }>{ rootError }</Alert> }
<span>Please select the address to sign and copy the message and sign it using the Blockscout message provider of your choice. </span> <Box mb={ 8 }>
<Link href="https://docs.blockscout.com/for-users/my-account/verified-addresses/copy-and-sign-message" target="_blank"> <span>Please select the address to sign and copy the message and sign it using the Blockscout message provider of your choice. </span>
<Link href="https://docs.blockscout.com/for-users/my-account/verified-addresses/copy-and-sign-message" target="_blank">
Additional instructions Additional instructions
</Link> </Link>
<span>. If you do not see your address here but are sure that you are the owner of the contract, kindly </span> <span>. If you do not see your address here but are sure that you are the owner of the contract, kindly </span>
{ contactUsLink } { contactUsLink }
<span> for further assistance.</span> <span> for further assistance.</span>
</Box> </Box>
{ (contractOwner || contractCreator) && ( { (contractOwner || contractCreator) && (
<Flex flexDir="column" rowGap={ 4 } mb={ 4 }> <Flex flexDir="column" rowGap={ 4 } mb={ 4 }>
{ contractCreator && ( { contractCreator && (
<Box> <Box>
<chakra.span fontWeight={ 600 }>Contract creator: </chakra.span> <chakra.span fontWeight={ 600 }>Contract creator: </chakra.span>
<chakra.span>{ contractCreator }</chakra.span> <chakra.span>{ contractCreator }</chakra.span>
</Box> </Box>
) }
{ contractOwner && (
<Box>
<chakra.span fontWeight={ 600 }>Contract owner: </chakra.span>
<chakra.span>{ contractOwner }</chakra.span>
</Box>
) }
</Flex>
) }
<Flex rowGap={ 5 } flexDir="column">
<div>
<CopyToClipboard text={ signingMessage } ml="auto" display="block"/>
<FormFieldText<Fields>
name="message"
placeholder="Message to sign"
isRequired
asComponent="Textarea"
isReadOnly
maxH={{ base: '140px', lg: '80px' }}
bgColor="dialog_bg"
/>
</div>
{ !noWeb3Provider && (
<RadioGroup onChange={ handleSignMethodChange } value={ signMethod } display="flex" flexDir="column" rowGap={ 4 }>
<Radio value="wallet">Sign via Web3 wallet</Radio>
<Radio value="manual">Sign manually</Radio>
</RadioGroup>
) } ) }
{ contractOwner && ( { signMethod === 'manual' && (
<Box> <FormFieldText<Fields>
<chakra.span fontWeight={ 600 }>Contract owner: </chakra.span> name="signature"
<chakra.span>{ contractOwner }</chakra.span> placeholder="Signature hash"
</Box> isRequired
rules={{ pattern: SIGNATURE_REGEXP }}
bgColor="dialog_bg"
/>
) } ) }
</Flex> </Flex>
) } <Flex alignItems={{ base: 'flex-start', lg: 'center' }} mt={ 8 } columnGap={ 5 } rowGap={ 2 } flexDir={{ base: 'column', lg: 'row' }}>
<Flex rowGap={ 5 } flexDir="column"> { button }
<div> <AdminSupportText/>
<CopyToClipboard text={ signingMessage } ml="auto" display="block"/> </Flex>
<AddressVerificationFieldMessage formState={ formState } control={ control }/> </form>
</div> </FormProvider>
{ !noWeb3Provider && (
<RadioGroup onChange={ handleSignMethodChange } value={ signMethod } display="flex" flexDir="column" rowGap={ 4 }>
<Radio value="wallet">Sign via Web3 wallet</Radio>
<Radio value="manual">Sign manually</Radio>
</RadioGroup>
) }
{ signMethod === 'manual' && <AddressVerificationFieldSignature formState={ formState } control={ control }/> }
</Flex>
<Flex alignItems={{ base: 'flex-start', lg: 'center' }} mt={ 8 } columnGap={ 5 } rowGap={ 2 } flexDir={{ base: 'column', lg: 'row' }}>
{ button }
<AdminSupportText/>
</Flex>
</form>
); );
}; };
......
import { import {
Box, Box,
Button, Button,
FormControl,
FormLabel,
Input,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form'; import { useForm, FormProvider } from 'react-hook-form';
import type { ApiKey, ApiKeys, ApiKeyErrors } from 'types/api/account'; import type { ApiKey, ApiKeys, ApiKeyErrors } from 'types/api/account';
...@@ -16,7 +13,7 @@ import type { ResourceErrorAccount } from 'lib/api/resources'; ...@@ -16,7 +13,7 @@ import type { ResourceErrorAccount } from 'lib/api/resources';
import { resourceKey } from 'lib/api/resources'; import { resourceKey } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
type Props = { type Props = {
data?: ApiKey; data?: ApiKey;
...@@ -32,7 +29,7 @@ type Inputs = { ...@@ -32,7 +29,7 @@ type Inputs = {
const NAME_MAX_LENGTH = 255; const NAME_MAX_LENGTH = 255;
const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const { control, handleSubmit, formState: { errors, isDirty }, setError } = useForm<Inputs>({ const formApi = useForm<Inputs>({
mode: 'onTouched', mode: 'onTouched',
defaultValues: { defaultValues: {
token: data?.api_key || '', token: data?.api_key || '',
...@@ -81,80 +78,54 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -81,80 +78,54 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
onError: (error: ResourceErrorAccount<ApiKeyErrors>) => { onError: (error: ResourceErrorAccount<ApiKeyErrors>) => {
const errorMap = error.payload?.errors; const errorMap = error.payload?.errors;
if (errorMap?.name) { if (errorMap?.name) {
setError('name', { type: 'custom', message: getErrorMessage(errorMap, 'name') }); formApi.setError('name', { type: 'custom', message: getErrorMessage(errorMap, 'name') });
} else if (errorMap?.identity_id) { } else if (errorMap?.identity_id) {
setError('name', { type: 'custom', message: getErrorMessage(errorMap, 'identity_id') }); formApi.setError('name', { type: 'custom', message: getErrorMessage(errorMap, 'identity_id') });
} else { } else {
setAlertVisible(true); setAlertVisible(true);
} }
}, },
}); });
const onSubmit: SubmitHandler<Inputs> = useCallback((data) => { const onSubmit: SubmitHandler<Inputs> = useCallback(async(data) => {
setAlertVisible(false); setAlertVisible(false);
mutation.mutate(data); await mutation.mutateAsync(data);
}, [ mutation, setAlertVisible ]); }, [ mutation, setAlertVisible ]);
const renderTokenInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'token'>}) => {
return (
<FormControl variant="floating" id="address">
<Input
{ ...field }
bgColor="dialog_bg"
isReadOnly
/>
<FormLabel>Auto-generated API key token</FormLabel>
</FormControl>
);
}, []);
const renderNameInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'name'>}) => {
return (
<FormControl variant="floating" id="name" isRequired bgColor="dialog_bg">
<Input
{ ...field }
isInvalid={ Boolean(errors.name) }
maxLength={ NAME_MAX_LENGTH }
bgColor="dialog_bg"
/>
<InputPlaceholder text="Application name for API key (e.g Web3 project)" error={ errors.name }/>
</FormControl>
);
}, [ errors ]);
return ( return (
<form noValidate onSubmit={ handleSubmit(onSubmit) }> <FormProvider { ...formApi }>
{ data && ( <form noValidate onSubmit={ formApi.handleSubmit(onSubmit) }>
<Box marginBottom={ 5 }> { data && (
<Controller <FormFieldText<Inputs>
name="token" name="token"
control={ control } placeholder="Auto-generated API key token"
render={ renderTokenInput } isReadOnly
bgColor="dialog_bg"
mb={ 5 }
/> />
</Box> ) }
) } <FormFieldText<Inputs>
<Box marginBottom={ 8 }>
<Controller
name="name" name="name"
control={ control } placeholder="Application name for API key (e.g Web3 project)"
isRequired
rules={{ rules={{
maxLength: NAME_MAX_LENGTH, maxLength: NAME_MAX_LENGTH,
required: true,
}} }}
render={ renderNameInput } bgColor="dialog_bg"
mb={ 8 }
/> />
</Box> <Box marginTop={ 8 }>
<Box marginTop={ 8 }> <Button
<Button size="lg"
size="lg" type="submit"
type="submit" isDisabled={ !formApi.formState.isDirty }
isDisabled={ !isDirty } isLoading={ mutation.isPending }
isLoading={ mutation.isPending } >
> { data ? 'Save' : 'Generate API key' }
{ data ? 'Save' : 'Generate API key' } </Button>
</Button> </Box>
</Box> </form>
</form> </FormProvider>
); );
}; };
......
...@@ -21,6 +21,8 @@ import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; ...@@ -21,6 +21,8 @@ import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip'; import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
import Utilization from 'ui/shared/Utilization/Utilization'; import Utilization from 'ui/shared/Utilization/Utilization';
import { getBaseFeeValue } from './utils';
interface Props { interface Props {
data: Block; data: Block;
isLoading?: boolean; isLoading?: boolean;
...@@ -33,6 +35,7 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => { ...@@ -33,6 +35,7 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
const totalReward = getBlockTotalReward(data); const totalReward = getBlockTotalReward(data);
const burntFees = BigNumber(data.burnt_fees || 0); const burntFees = BigNumber(data.burnt_fees || 0);
const txFees = BigNumber(data.tx_fees || 0); const txFees = BigNumber(data.tx_fees || 0);
const baseFeeValue = getBaseFeeValue(data.base_fee_per_gas);
return ( return (
<ListItemMobile rowGap={ 3 } key={ String(data.height) } isAnimated> <ListItemMobile rowGap={ 3 } key={ String(data.height) } isAnimated>
...@@ -124,6 +127,14 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => { ...@@ -124,6 +127,14 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
</Flex> </Flex>
</Box> </Box>
) } ) }
{ !isRollup && !config.UI.views.block.hiddenFields?.base_fee && baseFeeValue && (
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Base fee</Text>
<Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary">
<span>{ baseFeeValue }</span>
</Skeleton>
</Flex>
) }
</ListItemMobile> </ListItemMobile>
); );
}; };
......
...@@ -53,6 +53,8 @@ const BlocksTable = ({ data, isLoading, top, page, showSocketInfo, socketInfoNum ...@@ -53,6 +53,8 @@ const BlocksTable = ({ data, isLoading, top, page, showSocketInfo, socketInfoNum
<Th width={ `${ REWARD_COL_WEIGHT / widthBase * 100 }%` }>Reward { currencyUnits.ether }</Th> } <Th width={ `${ REWARD_COL_WEIGHT / widthBase * 100 }%` }>Reward { currencyUnits.ether }</Th> }
{ !isRollup && !config.UI.views.block.hiddenFields?.burnt_fees && { !isRollup && !config.UI.views.block.hiddenFields?.burnt_fees &&
<Th width={ `${ FEES_COL_WEIGHT / widthBase * 100 }%` }>Burnt fees { currencyUnits.ether }</Th> } <Th width={ `${ FEES_COL_WEIGHT / widthBase * 100 }%` }>Burnt fees { currencyUnits.ether }</Th> }
{ !isRollup && !config.UI.views.block.hiddenFields?.base_fee &&
<Th width="150px" isNumeric>Base fee</Th> }
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
......
...@@ -18,6 +18,8 @@ import LinkInternal from 'ui/shared/links/LinkInternal'; ...@@ -18,6 +18,8 @@ import LinkInternal from 'ui/shared/links/LinkInternal';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip'; import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
import Utilization from 'ui/shared/Utilization/Utilization'; import Utilization from 'ui/shared/Utilization/Utilization';
import { getBaseFeeValue } from './utils';
interface Props { interface Props {
data: Block; data: Block;
isLoading?: boolean; isLoading?: boolean;
...@@ -33,6 +35,8 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => { ...@@ -33,6 +35,8 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
const burntFeesIconColor = useColorModeValue('gray.500', 'inherit'); const burntFeesIconColor = useColorModeValue('gray.500', 'inherit');
const baseFeeValue = getBaseFeeValue(data.base_fee_per_gas);
return ( return (
<Tr <Tr
as={ motion.tr } as={ motion.tr }
...@@ -129,6 +133,13 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => { ...@@ -129,6 +133,13 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
</Tooltip> </Tooltip>
</Td> </Td>
) } ) }
{ !isRollup && !config.UI.views.block.hiddenFields?.base_fee && Boolean(baseFeeValue) && (
<Td fontSize="sm" isNumeric>
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ baseFeeValue }
</Skeleton>
</Td>
) }
</Tr> </Tr>
); );
}; };
......
import getValueWithUnit from 'lib/getValueWithUnit';
import { currencyUnits } from 'lib/units';
export const getBaseFeeValue = (baseFee: string | null) => {
if (!baseFee) {
return null;
}
const valGwei = getValueWithUnit(baseFee, 'gwei');
if (valGwei.isGreaterThanOrEqualTo(0.0001)) {
return `${ valGwei.toFormat(4) } ${ currencyUnits.gwei }`;
}
return `${ getValueWithUnit(baseFee, 'wei').toFormat() } ${ currencyUnits.wei }`;
};
...@@ -43,7 +43,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -43,7 +43,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
mode: 'onBlur', mode: 'onBlur',
defaultValues: getDefaultValues(methodFromQuery, config, hash, null), defaultValues: getDefaultValues(methodFromQuery, config, hash, null),
}); });
const { control, handleSubmit, watch, formState, setError, reset, getFieldState } = formApi; const { handleSubmit, watch, formState, setError, reset, getFieldState } = formApi;
const submitPromiseResolver = React.useRef<(value: unknown) => void>(); const submitPromiseResolver = React.useRef<(value: unknown) => void>();
const methodNameRef = React.useRef<string>(); const methodNameRef = React.useRef<string>();
...@@ -145,7 +145,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -145,7 +145,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
topic: `addresses:${ address?.toLowerCase() }`, topic: `addresses:${ address?.toLowerCase() }`,
onSocketClose: handleSocketError, onSocketClose: handleSocketError,
onSocketError: handleSocketError, onSocketError: handleSocketError,
isDisabled: Boolean(address && addressState.error), isDisabled: !address || Boolean(address && addressState.error),
}); });
useSocketMessage({ useSocketMessage({
channel, channel,
...@@ -191,11 +191,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -191,11 +191,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
<Grid as="section" columnGap="30px" rowGap={{ base: 2, lg: 5 }} templateColumns={{ base: '1fr', lg: 'minmax(auto, 680px) minmax(0, 340px)' }}> <Grid as="section" columnGap="30px" rowGap={{ base: 2, lg: 5 }} templateColumns={{ base: '1fr', lg: 'minmax(auto, 680px) minmax(0, 340px)' }}>
{ !hash && <ContractVerificationFieldAddress/> } { !hash && <ContractVerificationFieldAddress/> }
<ContractVerificationFieldLicenseType/> <ContractVerificationFieldLicenseType/>
<ContractVerificationFieldMethod <ContractVerificationFieldMethod methods={ config.verification_options }/>
control={ control }
methods={ config.verification_options }
isDisabled={ formState.isSubmitting }
/>
</Grid> </Grid>
{ content } { content }
{ Boolean(method) && method.value !== 'solidity-hardhat' && method.value !== 'solidity-foundry' && ( { Boolean(method) && method.value !== 'solidity-hardhat' && method.value !== 'solidity-foundry' && (
......
import { FormControl, Input, chakra } from '@chakra-ui/react'; import { chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import { ADDRESS_REGEXP, ADDRESS_LENGTH } from 'lib/validations/address'; import FormFieldAddress from 'ui/shared/forms/fields/FormFieldAddress';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
...@@ -15,26 +12,6 @@ interface Props { ...@@ -15,26 +12,6 @@ interface Props {
} }
const ContractVerificationFieldAddress = ({ isReadOnly }: Props) => { const ContractVerificationFieldAddress = ({ isReadOnly }: Props) => {
const { formState, control } = useFormContext<FormFields>();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'address'>}) => {
const error = 'address' in formState.errors ? formState.errors.address : undefined;
return (
<FormControl variant="floating" id={ field.name } isRequired size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...field }
required
isInvalid={ Boolean(error) }
maxLength={ ADDRESS_LENGTH }
isDisabled={ formState.isSubmitting || isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text="Smart contract / Address (0x...)" error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting, isReadOnly ]);
return ( return (
<> <>
<ContractVerificationFormRow> <ContractVerificationFormRow>
...@@ -43,11 +20,12 @@ const ContractVerificationFieldAddress = ({ isReadOnly }: Props) => { ...@@ -43,11 +20,12 @@ const ContractVerificationFieldAddress = ({ isReadOnly }: Props) => {
</chakra.span> </chakra.span>
</ContractVerificationFormRow> </ContractVerificationFormRow>
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <FormFieldAddress<FormFields>
name="address" name="address"
control={ control } isRequired
render={ renderControl } placeholder="Smart contract / Address (0x...)"
rules={{ required: true, pattern: ADDRESS_REGEXP }} isReadOnly={ isReadOnly }
size={{ base: 'md', lg: 'lg' }}
/> />
</ContractVerificationFormRow> </ContractVerificationFormRow>
</> </>
......
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import CheckboxInput from 'ui/shared/CheckboxInput'; import FormFieldCheckbox from 'ui/shared/forms/fields/FormFieldCheckbox';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
import ContractVerificationFieldConstructorArgs from './ContractVerificationFieldConstructorArgs'; import ContractVerificationFieldConstructorArgs from './ContractVerificationFieldConstructorArgs';
const ContractVerificationFieldAutodetectArgs = () => { const ContractVerificationFieldAutodetectArgs = () => {
const [ isOn, setIsOn ] = React.useState(true); const [ isOn, setIsOn ] = React.useState(true);
const { formState, control, resetField } = useFormContext<FormFields>(); const { resetField } = useFormContext<FormFields>();
const handleCheckboxChange = React.useCallback(() => { const handleCheckboxChange = React.useCallback(() => {
!isOn && resetField('constructor_args'); !isOn && resetField('constructor_args');
setIsOn(prev => !prev); setIsOn(prev => !prev);
}, [ isOn, resetField ]); }, [ isOn, resetField ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'autodetect_constructor_args'>}) => (
<CheckboxInput<FormFields, 'autodetect_constructor_args'>
text="Try to fetch constructor arguments automatically"
field={ field }
isDisabled={ formState.isSubmitting }
onChange={ handleCheckboxChange }
/>
), [ formState.isSubmitting, handleCheckboxChange ]);
return ( return (
<> <>
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <FormFieldCheckbox<FormFields, 'autodetect_constructor_args'>
name="autodetect_constructor_args" name="autodetect_constructor_args"
control={ control } label="Try to fetch constructor arguments automatically"
render={ renderControl } onChange={ handleCheckboxChange }
/> />
</ContractVerificationFormRow> </ContractVerificationFormRow>
{ !isOn && <ContractVerificationFieldConstructorArgs/> } { !isOn && <ContractVerificationFieldConstructorArgs/> }
......
import { FormControl, Textarea } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import FieldError from 'ui/shared/forms/FieldError'; import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
...@@ -15,32 +11,14 @@ interface Props { ...@@ -15,32 +11,14 @@ interface Props {
} }
const ContractVerificationFieldCode = ({ isVyper }: Props) => { const ContractVerificationFieldCode = ({ isVyper }: Props) => {
const { formState, control } = useFormContext<FormFields>();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'code'>}) => {
const error = 'code' in formState.errors ? formState.errors.code : undefined;
return (
<FormControl variant="floating" id={ field.name } isRequired size={{ base: 'md', lg: 'lg' }}>
<Textarea
{ ...field }
isInvalid={ Boolean(error) }
isDisabled={ formState.isSubmitting }
required
/>
<InputPlaceholder text="Contract code"/>
{ error?.message && <FieldError message={ error?.message }/> }
</FormControl>
);
}, [ formState.errors, formState.isSubmitting ]);
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <FormFieldText<FormFields>
name="code" name="code"
control={ control } isRequired
render={ renderControl } placeholder="Contract code"
rules={{ required: true }} size={{ base: 'md', lg: 'lg' }}
asComponent="Textarea"
/> />
{ isVyper ? null : ( { isVyper ? null : (
<span>If your code utilizes a library or inherits dependencies, we recommend using other verification methods instead.</span> <span>If your code utilizes a library or inherits dependencies, we recommend using other verification methods instead.</span>
......
import { chakra, Checkbox, Code } from '@chakra-ui/react'; import { chakra, Checkbox, Code } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import type { SmartContractVerificationConfig } from 'types/client/contract'; import type { SmartContractVerificationConfig } from 'types/client/contract';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
...@@ -22,8 +20,7 @@ interface Props { ...@@ -22,8 +20,7 @@ interface Props {
const ContractVerificationFieldCompiler = ({ isVyper }: Props) => { const ContractVerificationFieldCompiler = ({ isVyper }: Props) => {
const [ isNightly, setIsNightly ] = React.useState(false); const [ isNightly, setIsNightly ] = React.useState(false);
const { formState, control, getValues, resetField } = useFormContext<FormFields>(); const { formState, getValues, resetField } = useFormContext<FormFields>();
const isMobile = useIsMobile();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config')); const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config'));
...@@ -46,25 +43,6 @@ const ContractVerificationFieldCompiler = ({ isVyper }: Props) => { ...@@ -46,25 +43,6 @@ const ContractVerificationFieldCompiler = ({ isVyper }: Props) => {
.slice(0, OPTIONS_LIMIT); .slice(0, OPTIONS_LIMIT);
}, [ isNightly, options ]); }, [ isNightly, options ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'compiler'>}) => {
const error = 'compiler' in formState.errors ? formState.errors.compiler : undefined;
return (
<FancySelect
{ ...field }
loadOptions={ loadOptions }
defaultOptions
size={ isMobile ? 'md' : 'lg' }
placeholder="Compiler (enter version or use the dropdown)"
placeholderIcon={ <IconSvg name="search"/> }
isDisabled={ formState.isSubmitting }
error={ error }
isRequired
isAsync
/>
);
}, [ formState.errors, formState.isSubmitting, isMobile, loadOptions ]);
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<> <>
...@@ -78,11 +56,14 @@ const ContractVerificationFieldCompiler = ({ isVyper }: Props) => { ...@@ -78,11 +56,14 @@ const ContractVerificationFieldCompiler = ({ isVyper }: Props) => {
Include nightly builds Include nightly builds
</Checkbox> </Checkbox>
) } ) }
<Controller <FormFieldFancySelect<FormFields, 'compiler'>
name="compiler" name="compiler"
control={ control } placeholder="Compiler (enter version or use the dropdown)"
render={ renderControl } loadOptions={ loadOptions }
rules={{ required: true }} defaultOptions
placeholderIcon={ <IconSvg name="search"/> }
isRequired
isAsync
/> />
</> </>
{ isVyper ? null : ( { isVyper ? null : (
......
import { FormControl, Link, Textarea } from '@chakra-ui/react'; import { Link } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import FieldError from 'ui/shared/forms/FieldError'; import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
const ContractVerificationFieldConstructorArgs = () => { const ContractVerificationFieldConstructorArgs = () => {
const { formState, control } = useFormContext<FormFields>();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'constructor_args'>}) => {
const error = 'constructor_args' in formState.errors ? formState.errors.constructor_args : undefined;
return (
<FormControl variant="floating" id={ field.name } size={{ base: 'md', lg: 'lg' }} isRequired>
<Textarea
{ ...field }
maxLength={ 255 }
isDisabled={ formState.isSubmitting }
isInvalid={ Boolean(error) }
required
/>
<InputPlaceholder text="ABI-encoded Constructor Arguments"/>
{ error?.message && <FieldError message={ error?.message }/> }
</FormControl>
);
}, [ formState.errors, formState.isSubmitting ]);
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <FormFieldText<FormFields>
name="constructor_args" name="constructor_args"
control={ control } isRequired
render={ renderControl } rules={{ maxLength: 255 }}
rules={{ required: true }} placeholder="ABI-encoded Constructor Arguments"
size={{ base: 'md', lg: 'lg' }}
asComponent="Textarea"
/> />
<> <>
<span>Add arguments in </span> <span>Add arguments in </span>
......
import { useUpdateEffect } from '@chakra-ui/react'; import { useUpdateEffect } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import type { Option } from 'ui/shared/FancySelect/types'; import type { Option } from 'ui/shared/forms/inputs/select/types';
import useIsMobile from 'lib/hooks/useIsMobile'; import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
...@@ -15,8 +13,7 @@ const SOURCIFY_ERROR_REGEXP = /\(([^()]*)\)/; ...@@ -15,8 +13,7 @@ const SOURCIFY_ERROR_REGEXP = /\(([^()]*)\)/;
const ContractVerificationFieldContractIndex = () => { const ContractVerificationFieldContractIndex = () => {
const [ options, setOptions ] = React.useState<Array<Option>>([]); const [ options, setOptions ] = React.useState<Array<Option>>([]);
const { formState, control, watch } = useFormContext<FormFields>(); const { formState, watch } = useFormContext<FormFields>();
const isMobile = useIsMobile();
const sources = watch('sources'); const sources = watch('sources');
const sourcesError = 'sources' in formState.errors ? formState.errors.sources?.message : undefined; const sourcesError = 'sources' in formState.errors ? formState.errors.sources?.message : undefined;
...@@ -40,34 +37,18 @@ const ContractVerificationFieldContractIndex = () => { ...@@ -40,34 +37,18 @@ const ContractVerificationFieldContractIndex = () => {
setOptions([]); setOptions([]);
}, [ sources ]); }, [ sources ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'contract_index'>}) => {
const error = 'contract_index' in formState.errors ? formState.errors.contract_index : undefined;
return (
<FancySelect
{ ...field }
options={ options }
size={ isMobile ? 'md' : 'lg' }
placeholder="Contract name"
isDisabled={ formState.isSubmitting }
error={ error }
isRequired
isAsync={ false }
/>
);
}, [ formState.errors, formState.isSubmitting, isMobile, options ]);
if (options.length === 0) { if (options.length === 0) {
return null; return null;
} }
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <FormFieldFancySelect<FormFields, 'contract_index'>
name="contract_index" name="contract_index"
control={ control } placeholder="Contract name"
render={ renderControl } options={ options }
rules={{ required: true }} isRequired
isAsync={ false }
/> />
</ContractVerificationFormRow> </ContractVerificationFormRow>
); );
......
import { Link } from '@chakra-ui/react'; import { Link } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import type { SmartContractVerificationConfig } from 'types/client/contract'; import type { SmartContractVerificationConfig } from 'types/client/contract';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
...@@ -18,8 +15,6 @@ interface Props { ...@@ -18,8 +15,6 @@ interface Props {
} }
const ContractVerificationFieldEvmVersion = ({ isVyper }: Props) => { const ContractVerificationFieldEvmVersion = ({ isVyper }: Props) => {
const { formState, control } = useFormContext<FormFields>();
const isMobile = useIsMobile();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config')); const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config'));
...@@ -27,29 +22,13 @@ const ContractVerificationFieldEvmVersion = ({ isVyper }: Props) => { ...@@ -27,29 +22,13 @@ const ContractVerificationFieldEvmVersion = ({ isVyper }: Props) => {
(isVyper ? config?.vyper_evm_versions : config?.solidity_evm_versions)?.map((option) => ({ label: option, value: option })) || [] (isVyper ? config?.vyper_evm_versions : config?.solidity_evm_versions)?.map((option) => ({ label: option, value: option })) || []
), [ config?.solidity_evm_versions, config?.vyper_evm_versions, isVyper ]); ), [ config?.solidity_evm_versions, config?.vyper_evm_versions, isVyper ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'evm_version'>}) => {
const error = 'evm_version' in formState.errors ? formState.errors.evm_version : undefined;
return (
<FancySelect
{ ...field }
options={ options }
size={ isMobile ? 'md' : 'lg' }
placeholder="EVM Version"
isDisabled={ formState.isSubmitting }
error={ error }
isRequired
/>
);
}, [ formState.errors, formState.isSubmitting, isMobile, options ]);
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <FormFieldFancySelect<FormFields, 'evm_version'>
name="evm_version" name="evm_version"
control={ control } placeholder="EVM Version"
render={ renderControl } options={ options }
rules={{ required: true }} isRequired
/> />
<> <>
<span>The EVM version the contract is written for. If the bytecode does not match the version, we try to verify using the latest EVM version. </span> <span>The EVM version the contract is written for. If the bytecode does not match the version, we try to verify using the latest EVM version. </span>
......
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import CheckboxInput from 'ui/shared/CheckboxInput'; import FormFieldCheckbox from 'ui/shared/forms/fields/FormFieldCheckbox';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
const ContractVerificationFieldIsYul = () => { const ContractVerificationFieldIsYul = () => {
const { formState, control } = useFormContext<FormFields>();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'is_yul'>}) => (
<CheckboxInput<FormFields, 'is_yul'> text="Is Yul contract" field={ field } isDisabled={ formState.isSubmitting }/>
), [ formState.isSubmitting ]);
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <FormFieldCheckbox<FormFields, 'is_yul'>
name="is_yul" name="is_yul"
control={ control } label="Is Yul contract"
render={ renderControl }
/> />
</ContractVerificationFormRow> </ContractVerificationFormRow>
); );
......
...@@ -56,11 +56,9 @@ const ContractVerificationFieldLibraries = () => { ...@@ -56,11 +56,9 @@ const ContractVerificationFieldLibraries = () => {
<ContractVerificationFieldLibraryItem <ContractVerificationFieldLibraryItem
key={ field.id } key={ field.id }
index={ index } index={ index }
control={ control }
fieldsLength={ fields.length } fieldsLength={ fields.length }
onAddFieldClick={ handleAddFieldClick } onAddFieldClick={ handleAddFieldClick }
onRemoveFieldClick={ handleRemoveFieldClick } onRemoveFieldClick={ handleRemoveFieldClick }
error={ 'libraries' in formState.errors ? formState.errors.libraries?.[index] : undefined }
isDisabled={ formState.isSubmitting } isDisabled={ formState.isSubmitting }
/> />
)) } )) }
......
import { Flex, FormControl, IconButton, Input, Text } from '@chakra-ui/react'; import { Flex, IconButton, Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { Control, ControllerRenderProps, FieldError } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import { ADDRESS_REGEXP } from 'lib/validations/address'; import FormFieldAddress from 'ui/shared/forms/fields/FormFieldAddress';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
const LIMIT = 10; const LIMIT = 10;
interface Props { interface Props {
control: Control<FormFields>;
index: number; index: number;
fieldsLength: number; fieldsLength: number;
error?: {
name?: FieldError;
address?: FieldError;
};
onAddFieldClick: (index: number) => void; onAddFieldClick: (index: number) => void;
onRemoveFieldClick: (index: number) => void; onRemoveFieldClick: (index: number) => void;
isDisabled?: boolean; isDisabled?: boolean;
} }
const ContractVerificationFieldLibraryItem = ({ control, index, fieldsLength, onAddFieldClick, onRemoveFieldClick, error, isDisabled }: Props) => { const ContractVerificationFieldLibraryItem = ({ index, fieldsLength, onAddFieldClick, onRemoveFieldClick, isDisabled }: Props) => {
const ref = React.useRef<HTMLDivElement>(null); const ref = React.useRef<HTMLDivElement>(null);
const renderNameControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, `libraries.${ number }.name`>}) => {
return (
<FormControl variant="floating" id={ field.name } isRequired size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...field }
required
isInvalid={ Boolean(error?.name) }
isDisabled={ isDisabled }
maxLength={ 255 }
autoComplete="off"
/>
<InputPlaceholder text="Library name (.sol file)" error={ error?.name }/>
</FormControl>
);
}, [ error?.name, isDisabled ]);
const renderAddressControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, `libraries.${ number }.address`>}) => {
return (
<FormControl variant="floating" id={ field.name } isRequired size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...field }
isInvalid={ Boolean(error?.address) }
isDisabled={ isDisabled }
required
autoComplete="off"
/>
<InputPlaceholder text="Library address (0x...)" error={ error?.address }/>
</FormControl>
);
}, [ error?.address, isDisabled ]);
const handleAddButtonClick = React.useCallback(() => { const handleAddButtonClick = React.useCallback(() => {
onAddFieldClick(index); onAddFieldClick(index);
}, [ index, onAddFieldClick ]); }, [ index, onAddFieldClick ]);
...@@ -104,11 +66,12 @@ const ContractVerificationFieldLibraryItem = ({ control, index, fieldsLength, on ...@@ -104,11 +66,12 @@ const ContractVerificationFieldLibraryItem = ({ control, index, fieldsLength, on
</Flex> </Flex>
</ContractVerificationFormRow> </ContractVerificationFormRow>
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <FormFieldText<FormFields, `libraries.${ number }.name`>
name={ `libraries.${ index }.name` } name={ `libraries.${ index }.name` }
control={ control } isRequired
render={ renderNameControl } rules={{ maxLength: 255 }}
rules={{ required: true }} placeholder="Library name (.sol file)"
size={{ base: 'md', lg: 'lg' }}
/> />
{ index === 0 ? ( { index === 0 ? (
<> <>
...@@ -117,11 +80,11 @@ const ContractVerificationFieldLibraryItem = ({ control, index, fieldsLength, on ...@@ -117,11 +80,11 @@ const ContractVerificationFieldLibraryItem = ({ control, index, fieldsLength, on
) : null } ) : null }
</ContractVerificationFormRow> </ContractVerificationFormRow>
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <FormFieldAddress<FormFields, `libraries.${ number }.address`>
name={ `libraries.${ index }.address` } name={ `libraries.${ index }.address` }
control={ control } isRequired
render={ renderAddressControl } placeholder="Library address (0x...)"
rules={{ required: true, pattern: ADDRESS_REGEXP }} size={{ base: 'md', lg: 'lg' }}
/> />
{ index === 0 ? ( { index === 0 ? (
<> <>
......
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import { CONTRACT_LICENSES } from 'lib/contracts/licenses'; import { CONTRACT_LICENSES } from 'lib/contracts/licenses';
import useIsMobile from 'lib/hooks/useIsMobile'; import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
const options = CONTRACT_LICENSES.map(({ label, title, type }) => ({ label: `${ title } (${ label })`, value: type }));
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
const ContractVerificationFieldLicenseType = () => { const options = CONTRACT_LICENSES.map(({ label, title, type }) => ({ label: `${ title } (${ label })`, value: type }));
const { formState, control } = useFormContext<FormFields>();
const isMobile = useIsMobile();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'license_type'>}) => {
const error = 'license_type' in formState.errors ? formState.errors.license_type : undefined;
return ( const ContractVerificationFieldLicenseType = () => {
<FancySelect
{ ...field }
options={ options }
size={ isMobile ? 'md' : 'lg' }
placeholder="Contract license"
isDisabled={ formState.isSubmitting }
error={ error }
/>
);
}, [ formState.errors, formState.isSubmitting, isMobile ]);
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <FormFieldFancySelect<FormFields, 'license_type'>
name="license_type" name="license_type"
control={ control } placeholder="Contract license"
render={ renderControl } options={ options }
/> />
<span> <span>
For best practices, all contract source code holders, publishers and authors are encouraged to also 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. specify the accompanying license for their verified contract source code provided.
</span> </span>
</ContractVerificationFormRow> </ContractVerificationFormRow>
); );
......
...@@ -13,26 +13,22 @@ import { ...@@ -13,26 +13,22 @@ import {
Box, Box,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps, Control } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import type { SmartContractVerificationMethod, SmartContractVerificationConfig } from 'types/client/contract'; import type { SmartContractVerificationMethod, SmartContractVerificationConfig } from 'types/client/contract';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import Popover from 'ui/shared/chakra/Popover'; import Popover from 'ui/shared/chakra/Popover';
import FancySelect from 'ui/shared/FancySelect/FancySelect'; import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import { METHOD_LABELS } from '../utils'; import { METHOD_LABELS } from '../utils';
interface Props { interface Props {
control: Control<FormFields>;
isDisabled?: boolean;
methods: SmartContractVerificationConfig['verification_options']; methods: SmartContractVerificationConfig['verification_options'];
} }
const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props) => { const ContractVerificationFieldMethod = ({ methods }: Props) => {
const tooltipBg = useColorModeValue('gray.700', 'gray.900'); const tooltipBg = useColorModeValue('gray.700', 'gray.900');
const isMobile = useIsMobile(); const isMobile = useIsMobile();
...@@ -41,21 +37,6 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props ...@@ -41,21 +37,6 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props
label: METHOD_LABELS[method], label: METHOD_LABELS[method],
})), [ methods ]); })), [ methods ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'method'>}) => {
return (
<FancySelect
{ ...field }
options={ options }
size={ isMobile ? 'md' : 'lg' }
placeholder="Verification method (compiler type)"
isDisabled={ isDisabled }
isRequired
isAsync={ false }
isReadOnly={ options.length === 1 }
/>
);
}, [ isDisabled, isMobile, options ]);
const renderPopoverListItem = React.useCallback((method: SmartContractVerificationMethod) => { const renderPopoverListItem = React.useCallback((method: SmartContractVerificationMethod) => {
switch (method) { switch (method) {
case 'flattened-code': case 'flattened-code':
...@@ -128,11 +109,13 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props ...@@ -128,11 +109,13 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props
</Portal> </Portal>
</Popover> </Popover>
</Box> </Box>
<Controller <FormFieldFancySelect<FormFields, 'method'>
name="method" name="method"
control={ control } placeholder="Verification method (compiler type)"
render={ renderControl } options={ options }
rules={{ required: true }} isRequired
isAsync={ false }
isReadOnly={ options.length === 1 }
/> />
</> </>
); );
......
import { chakra, Code, FormControl, Input } from '@chakra-ui/react'; import { chakra, Code } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
interface Props { interface Props {
hint?: string; hint?: string;
isReadOnly?: boolean;
} }
const ContractVerificationFieldName = ({ hint, isReadOnly }: Props) => { const ContractVerificationFieldName = ({ hint }: Props) => {
const { formState, control } = useFormContext<FormFields>();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'name'>}) => {
const error = 'name' in formState.errors ? formState.errors.name : undefined;
return (
<FormControl variant="floating" id={ field.name } isRequired size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...field }
required
isInvalid={ Boolean(error) }
maxLength={ 255 }
isDisabled={ formState.isSubmitting || isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text="Contract name" error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting, isReadOnly ]);
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <FormFieldText<FormFields>
name="name" name="name"
control={ control } isRequired
render={ renderControl } placeholder="Contract name"
rules={{ required: true }} size={{ base: 'md', lg: 'lg' }}
rules={{ maxLength: 255 }}
/> />
{ hint ? <span>{ hint }</span> : ( { hint ? <span>{ hint }</span> : (
<> <>
......
import { Flex, Input } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import CheckboxInput from 'ui/shared/CheckboxInput'; import FormFieldCheckbox from 'ui/shared/forms/fields/FormFieldCheckbox';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
const ContractVerificationFieldOptimization = () => { const ContractVerificationFieldOptimization = () => {
const [ isEnabled, setIsEnabled ] = React.useState(true); const [ isEnabled, setIsEnabled ] = React.useState(true);
const { formState, control } = useFormContext<FormFields>();
const error = 'optimization_runs' in formState.errors ? formState.errors.optimization_runs : undefined;
const handleCheckboxChange = React.useCallback(() => { const handleCheckboxChange = React.useCallback(() => {
setIsEnabled(prev => !prev); setIsEnabled(prev => !prev);
}, []); }, []);
const renderCheckboxControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'is_optimization_enabled'>}) => (
<Flex flexShrink={ 0 }>
<CheckboxInput<FormFields, 'is_optimization_enabled'>
text="Optimization enabled"
field={ field }
onChange={ handleCheckboxChange }
isDisabled={ formState.isSubmitting }
/>
</Flex>
), [ formState.isSubmitting, handleCheckboxChange ]);
const renderInputControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'optimization_runs'>}) => {
return (
<Input
{ ...field }
required
isDisabled={ formState.isSubmitting }
autoComplete="off"
type="number"
placeholder="Optimization runs"
size="xs"
minW="100px"
maxW="200px"
flexShrink={ 1 }
isInvalid={ Boolean(error) }
/>
);
}, [ error, formState.isSubmitting ]);
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Flex columnGap={ 5 } h={{ base: 'auto', lg: '32px' }}> <Flex columnGap={ 5 } h={{ base: 'auto', lg: '32px' }}>
<Controller <FormFieldCheckbox<FormFields, 'is_optimization_enabled'>
name="is_optimization_enabled" name="is_optimization_enabled"
control={ control } label="Optimization enabled"
render={ renderCheckboxControl } onChange={ handleCheckboxChange }
flexShrink={ 0 }
/> />
{ isEnabled && ( { isEnabled && (
<Controller <FormFieldText<FormFields, 'optimization_runs'>
name="optimization_runs" name="optimization_runs"
control={ control } isRequired
render={ renderInputControl } placeholder="Optimization runs"
rules={{ required: true }} type="number"
size="xs"
minW="100px"
maxW="200px"
flexShrink={ 1 }
/> />
) } ) }
</Flex> </Flex>
......
...@@ -6,10 +6,10 @@ import { Controller, useFormContext } from 'react-hook-form'; ...@@ -6,10 +6,10 @@ import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import { Mb } from 'lib/consts'; import { Mb } from 'lib/consts';
import DragAndDropArea from 'ui/shared/forms/DragAndDropArea'; import FieldError from 'ui/shared/forms/components/FieldError';
import FieldError from 'ui/shared/forms/FieldError'; import DragAndDropArea from 'ui/shared/forms/inputs/file/DragAndDropArea';
import FileInput from 'ui/shared/forms/FileInput'; import FileInput from 'ui/shared/forms/inputs/file/FileInput';
import FileSnippet from 'ui/shared/forms/FileSnippet'; import FileSnippet from 'ui/shared/forms/inputs/file/FileSnippet';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
......
import { Box, Link } from '@chakra-ui/react'; import { Box, Link } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import type { SmartContractVerificationConfig } from 'types/client/contract'; import type { SmartContractVerificationConfig } from 'types/client/contract';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
...@@ -17,8 +14,6 @@ import ContractVerificationFormRow from '../ContractVerificationFormRow'; ...@@ -17,8 +14,6 @@ import ContractVerificationFormRow from '../ContractVerificationFormRow';
const OPTIONS_LIMIT = 50; const OPTIONS_LIMIT = 50;
const ContractVerificationFieldZkCompiler = () => { const ContractVerificationFieldZkCompiler = () => {
const { formState, control } = useFormContext<FormFields>();
const isMobile = useIsMobile();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config')); const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config'));
...@@ -32,33 +27,17 @@ const ContractVerificationFieldZkCompiler = () => { ...@@ -32,33 +27,17 @@ const ContractVerificationFieldZkCompiler = () => {
.slice(0, OPTIONS_LIMIT); .slice(0, OPTIONS_LIMIT);
}, [ options ]); }, [ options ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'zk_compiler'>}) => { return (
const error = 'zk_compiler' in formState.errors ? formState.errors.zk_compiler : undefined; <ContractVerificationFormRow>
<FormFieldFancySelect<FormFields, 'zk_compiler'>
return ( name="zk_compiler"
<FancySelect
{ ...field }
loadOptions={ loadOptions }
defaultOptions
size={ isMobile ? 'md' : 'lg' }
placeholder="ZK compiler (enter version or use the dropdown)" placeholder="ZK compiler (enter version or use the dropdown)"
placeholderIcon={ <IconSvg name="search"/> } placeholderIcon={ <IconSvg name="search"/> }
isDisabled={ formState.isSubmitting } loadOptions={ loadOptions }
error={ error } defaultOptions
isRequired isRequired
isAsync isAsync
/> />
);
}, [ formState.errors, formState.isSubmitting, isMobile, loadOptions ]);
return (
<ContractVerificationFormRow>
<Controller
name="zk_compiler"
control={ control }
render={ renderControl }
rules={{ required: true }}
/>
<Box> <Box>
<Link isExternal href="https://docs.zksync.io/zk-stack/components/compiler/specification#glossary">zksolc</Link> <Link isExternal href="https://docs.zksync.io/zk-stack/components/compiler/specification#glossary">zksolc</Link>
<span> compiler version.</span> <span> compiler version.</span>
......
import type { SmartContractLicenseType } from 'types/api/contract'; import type { SmartContractLicenseType } from 'types/api/contract';
import type { SmartContractVerificationMethod } from 'types/client/contract'; import type { SmartContractVerificationMethod } from 'types/client/contract';
import type { Option } from 'ui/shared/FancySelect/types'; import type { Option } from 'ui/shared/forms/inputs/select/types';
export interface ContractLibrary { export interface ContractLibrary {
name: string; name: string;
......
...@@ -165,7 +165,7 @@ export function getDefaultValues( ...@@ -165,7 +165,7 @@ export function getDefaultValues(
const method = singleMethod || methodParam; const method = singleMethod || methodParam;
if (!method) { if (!method) {
return; return { address: hash || '' };
} }
const defaultValues: FormFields = { ...DEFAULT_VALUES[method], address: hash || '', license_type: licenseType }; const defaultValues: FormFields = { ...DEFAULT_VALUES[method], address: hash || '', license_type: licenseType };
......
import { FormControl, Input } from '@chakra-ui/react';
import _capitalize from 'lodash/capitalize'; import _capitalize from 'lodash/capitalize';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps, UseFormReturn } from 'react-hook-form'; import type { UseFormReturn } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { FormFields } from './types'; import type { FormFields } from './types';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
interface Props { interface Props {
formApi: UseFormReturn<FormFields>; formApi: UseFormReturn<FormFields>;
...@@ -15,26 +13,7 @@ interface Props { ...@@ -15,26 +13,7 @@ interface Props {
} }
const CsvExportFormField = ({ formApi, name }: Props) => { const CsvExportFormField = ({ formApi, name }: Props) => {
const { formState, control, getValues, trigger } = formApi; const { formState, getValues, trigger } = formApi;
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'from' | 'to'>}) => {
const error = field.name in formState.errors ? formState.errors[field.name] : undefined;
return (
<FormControl variant="floating" id={ field.name } isRequired size={{ base: 'md', lg: 'lg' }} maxW={{ base: 'auto', lg: '220px' }}>
<Input
{ ...field }
required
isInvalid={ Boolean(error) }
type="date"
isDisabled={ formState.isSubmitting }
autoComplete="off"
max={ dayjs().format('YYYY-MM-DD') }
/>
<InputPlaceholder text={ _capitalize(field.name) } error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting ]);
const validate = React.useCallback((newValue: string) => { const validate = React.useCallback((newValue: string) => {
if (name === 'from') { if (name === 'from') {
...@@ -57,11 +36,15 @@ const CsvExportFormField = ({ formApi, name }: Props) => { ...@@ -57,11 +36,15 @@ const CsvExportFormField = ({ formApi, name }: Props) => {
}, [ formState.errors.from, formState.errors.to, getValues, name, trigger ]); }, [ formState.errors.from, formState.errors.to, getValues, name, trigger ]);
return ( return (
<Controller <FormFieldText<FormFields, typeof name>
name={ name } name={ name }
control={ control } type="date"
render={ renderControl } max={ dayjs().format('YYYY-MM-DD') }
rules={{ required: true, validate }} placeholder={ _capitalize(name) }
isRequired
rules={{ validate }}
size={{ base: 'md', lg: 'lg' }}
maxW={{ base: 'auto', lg: '220px' }}
/> />
); );
}; };
......
import { import {
Box, Box,
Button, Button,
FormControl,
Input,
Textarea,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { ControllerRenderProps, SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form'; import { useForm, FormProvider } from 'react-hook-form';
import type { CustomAbi, CustomAbis, CustomAbiErrors } from 'types/api/account'; import type { CustomAbi, CustomAbis, CustomAbiErrors } from 'types/api/account';
...@@ -16,9 +13,8 @@ import type { ResourceErrorAccount } from 'lib/api/resources'; ...@@ -16,9 +13,8 @@ import type { ResourceErrorAccount } from 'lib/api/resources';
import { resourceKey } from 'lib/api/resources'; import { resourceKey } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import { ADDRESS_REGEXP } from 'lib/validations/address'; import FormFieldAddress from 'ui/shared/forms/fields/FormFieldAddress';
import AddressInput from 'ui/shared/AddressInput'; import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Props = { type Props = {
data?: CustomAbi; data?: CustomAbi;
...@@ -35,7 +31,7 @@ type Inputs = { ...@@ -35,7 +31,7 @@ type Inputs = {
const NAME_MAX_LENGTH = 255; const NAME_MAX_LENGTH = 255;
const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const { control, formState: { errors, isDirty }, handleSubmit, setError } = useForm<Inputs>({ const formApi = useForm<Inputs>({
defaultValues: { defaultValues: {
contract_address_hash: data?.contract_address_hash || '', contract_address_hash: data?.contract_address_hash || '',
name: data?.name || '', name: data?.name || '',
...@@ -85,102 +81,64 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -85,102 +81,64 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
onError: (error: ResourceErrorAccount<CustomAbiErrors>) => { onError: (error: ResourceErrorAccount<CustomAbiErrors>) => {
const errorMap = error.payload?.errors; const errorMap = error.payload?.errors;
if (errorMap?.address_hash || errorMap?.name || errorMap?.abi) { if (errorMap?.address_hash || errorMap?.name || errorMap?.abi) {
errorMap?.address_hash && setError('contract_address_hash', { type: 'custom', message: getErrorMessage(errorMap, 'address_hash') }); errorMap?.address_hash && formApi.setError('contract_address_hash', { type: 'custom', message: getErrorMessage(errorMap, 'address_hash') });
errorMap?.name && setError('name', { type: 'custom', message: getErrorMessage(errorMap, 'name') }); errorMap?.name && formApi.setError('name', { type: 'custom', message: getErrorMessage(errorMap, 'name') });
errorMap?.abi && setError('abi', { type: 'custom', message: getErrorMessage(errorMap, 'abi') }); errorMap?.abi && formApi.setError('abi', { type: 'custom', message: getErrorMessage(errorMap, 'abi') });
} else if (errorMap?.identity_id) { } else if (errorMap?.identity_id) {
setError('contract_address_hash', { type: 'custom', message: getErrorMessage(errorMap, 'identity_id') }); formApi.setError('contract_address_hash', { type: 'custom', message: getErrorMessage(errorMap, 'identity_id') });
} else { } else {
setAlertVisible(true); setAlertVisible(true);
} }
}, },
}); });
const onSubmit: SubmitHandler<Inputs> = useCallback((formData) => { const onSubmit: SubmitHandler<Inputs> = useCallback(async(formData) => {
setAlertVisible(false); setAlertVisible(false);
mutation.mutate({ ...formData, id: data?.id ? String(data.id) : undefined }); await mutation.mutateAsync({ ...formData, id: data?.id ? String(data.id) : undefined });
}, [ mutation, data, setAlertVisible ]); }, [ mutation, data, setAlertVisible ]);
const renderContractAddressInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'contract_address_hash'>}) => {
return (
<AddressInput<Inputs, 'contract_address_hash'>
field={ field }
error={ errors.contract_address_hash }
bgColor="dialog_bg"
placeholder="Smart contract address (0x...)"
/>
);
}, [ errors ]);
const renderNameInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'name'>}) => {
return (
<FormControl variant="floating" id="name" isRequired bgColor="dialog_bg">
<Input
{ ...field }
isInvalid={ Boolean(errors.name) }
maxLength={ NAME_MAX_LENGTH }
bgColor="dialog_bg"
/>
<InputPlaceholder text="Project name" error={ errors.name }/>
</FormControl>
);
}, [ errors ]);
const renderAbiInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'abi'>}) => {
return (
<FormControl variant="floating" id="abi" isRequired bgColor="dialog_bg">
<Textarea
{ ...field }
size="lg"
minH="300px"
isInvalid={ Boolean(errors.abi) }
bgColor="dialog_bg"
/>
<InputPlaceholder text="Custom ABI [{...}] (JSON format)" error={ errors.abi }/>
</FormControl>
);
}, [ errors ]);
return ( return (
<form noValidate onSubmit={ handleSubmit(onSubmit) }> <FormProvider { ...formApi }>
<Box> <form noValidate onSubmit={ formApi.handleSubmit(onSubmit) }>
<Controller <FormFieldAddress<Inputs>
name="contract_address_hash" name="contract_address_hash"
control={ control } placeholder="Smart contract address (0x...)"
render={ renderContractAddressInput } isRequired
rules={{ bgColor="dialog_bg"
pattern: ADDRESS_REGEXP, mb={ 5 }
required: true,
}}
/> />
</Box> <FormFieldText<Inputs>
<Box marginTop={ 5 }>
<Controller
name="name" name="name"
control={ control } placeholder="Project name"
render={ renderNameInput } isRequired
rules={{ required: true }} rules={{
maxLength: NAME_MAX_LENGTH,
}}
bgColor="dialog_bg"
mb={ 5 }
/> />
</Box> <FormFieldText<Inputs>
<Box marginTop={ 5 }>
<Controller
name="abi" name="abi"
control={ control } placeholder="Custom ABI [{...}] (JSON format)"
render={ renderAbiInput } isRequired
rules={{ required: true }} asComponent="Textarea"
/> bgColor="dialog_bg"
</Box>
<Box marginTop={ 8 }>
<Button
size="lg" size="lg"
type="submit" minH="300px"
isDisabled={ !isDirty } mb={ 8 }
isLoading={ mutation.isPending } />
> <Box>
{ data ? 'Save' : 'Create custom ABI' } <Button
</Button> size="lg"
</Box> type="submit"
</form> isDisabled={ !formApi.formState.isDirty }
isLoading={ mutation.isPending }
>
{ data ? 'Save' : 'Create custom ABI' }
</Button>
</Box>
</form>
</FormProvider>
); );
}; };
......
...@@ -15,10 +15,10 @@ import getErrorObjPayload from 'lib/errors/getErrorObjPayload'; ...@@ -15,10 +15,10 @@ import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
import * as mixpanel from 'lib/mixpanel'; import * as mixpanel from 'lib/mixpanel';
import FormFieldReCaptcha from 'ui/shared/forms/fields/FormFieldReCaptcha'; import FormFieldReCaptcha from 'ui/shared/forms/fields/FormFieldReCaptcha';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import AuthModal from 'ui/snippets/auth/AuthModal'; import AuthModal from 'ui/snippets/auth/AuthModal';
import MyProfileFieldsEmail from './fields/MyProfileFieldsEmail'; import MyProfileFieldsEmail from './fields/MyProfileFieldsEmail';
import MyProfileFieldsName from './fields/MyProfileFieldsName';
const MIXPANEL_CONFIG = { const MIXPANEL_CONFIG = {
account_link_info: { account_link_info: {
...@@ -80,7 +80,7 @@ const MyProfileEmail = ({ profileQuery }: Props) => { ...@@ -80,7 +80,7 @@ const MyProfileEmail = ({ profileQuery }: Props) => {
noValidate noValidate
onSubmit={ formApi.handleSubmit(onFormSubmit) } onSubmit={ formApi.handleSubmit(onFormSubmit) }
> >
<MyProfileFieldsName/> <FormFieldText<FormFields> name="name" placeholder="Name" isReadOnly mb={ 3 }/>
<MyProfileFieldsEmail <MyProfileFieldsEmail
isReadOnly={ !config.services.reCaptchaV3.siteKey || Boolean(profileQuery.data?.email) } isReadOnly={ !config.services.reCaptchaV3.siteKey || Boolean(profileQuery.data?.email) }
defaultValue={ profileQuery.data?.email || undefined } defaultValue={ profileQuery.data?.email || undefined }
......
...@@ -4,9 +4,9 @@ import { useController, useFormContext } from 'react-hook-form'; ...@@ -4,9 +4,9 @@ import { useController, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import { EMAIL_REGEXP } from 'lib/validations/email'; import FormInputPlaceholder from 'ui/shared/forms/inputs/FormInputPlaceholder';
import { EMAIL_REGEXP } from 'ui/shared/forms/validators/email';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props { interface Props {
isReadOnly?: boolean; isReadOnly?: boolean;
...@@ -35,7 +35,7 @@ const MyProfileFieldsEmail = ({ isReadOnly, defaultValue }: Props) => { ...@@ -35,7 +35,7 @@ const MyProfileFieldsEmail = ({ isReadOnly, defaultValue }: Props) => {
isReadOnly={ isReadOnly } isReadOnly={ isReadOnly }
autoComplete="off" autoComplete="off"
/> />
<InputPlaceholder text="Email" error={ fieldState.error }/> <FormInputPlaceholder text="Email" error={ fieldState.error }/>
{ isVerified && ( { isVerified && (
<InputRightElement h="100%"> <InputRightElement h="100%">
<IconSvg name="certified" boxSize={ 5 } color="green.500"/> <IconSvg name="certified" boxSize={ 5 } color="green.500"/>
......
...@@ -11,7 +11,6 @@ import useAddressMetadataInfoQuery from 'lib/address/useAddressMetadataInfoQuery ...@@ -11,7 +11,6 @@ import useAddressMetadataInfoQuery from 'lib/address/useAddressMetadataInfoQuery
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app'; import { useAppContext } from 'lib/contexts/app';
import useAddressProfileApiQuery from 'lib/hooks/useAddressProfileApiQuery'; import useAddressProfileApiQuery from 'lib/hooks/useAddressProfileApiQuery';
import useContractTabs from 'lib/hooks/useContractTabs';
import useIsSafeAddress from 'lib/hooks/useIsSafeAddress'; import useIsSafeAddress from 'lib/hooks/useIsSafeAddress';
import getNetworkValidationActionText from 'lib/networks/getNetworkValidationActionText'; import getNetworkValidationActionText from 'lib/networks/getNetworkValidationActionText';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
...@@ -33,6 +32,8 @@ import AddressTokenTransfers from 'ui/address/AddressTokenTransfers'; ...@@ -33,6 +32,8 @@ import AddressTokenTransfers from 'ui/address/AddressTokenTransfers';
import AddressTxs from 'ui/address/AddressTxs'; import AddressTxs from 'ui/address/AddressTxs';
import AddressUserOps from 'ui/address/AddressUserOps'; import AddressUserOps from 'ui/address/AddressUserOps';
import AddressWithdrawals from 'ui/address/AddressWithdrawals'; import AddressWithdrawals from 'ui/address/AddressWithdrawals';
import useContractTabs from 'ui/address/contract/useContractTabs';
import { CONTRACT_TAB_IDS } from 'ui/address/contract/utils';
import AddressFavoriteButton from 'ui/address/details/AddressFavoriteButton'; import AddressFavoriteButton from 'ui/address/details/AddressFavoriteButton';
import AddressMetadataAlert from 'ui/address/details/AddressMetadataAlert'; import AddressMetadataAlert from 'ui/address/details/AddressMetadataAlert';
import AddressQrCode from 'ui/address/details/AddressQrCode'; import AddressQrCode from 'ui/address/details/AddressQrCode';
...@@ -138,7 +139,11 @@ const AddressPageContent = () => { ...@@ -138,7 +139,11 @@ const AddressPageContent = () => {
const isSafeAddress = useIsSafeAddress(!addressQuery.isPlaceholderData && addressQuery.data?.is_contract ? hash : undefined); const isSafeAddress = useIsSafeAddress(!addressQuery.isPlaceholderData && addressQuery.data?.is_contract ? hash : undefined);
const safeIconColor = useColorModeValue('black', 'white'); const safeIconColor = useColorModeValue('black', 'white');
const contractTabs = useContractTabs(addressQuery.data, addressQuery.isPlaceholderData); const contractTabs = useContractTabs(
addressQuery.data,
config.features.mudFramework.isEnabled ? (mudTablesCountQuery.isPlaceholderData || addressQuery.isPlaceholderData) : addressQuery.isPlaceholderData,
Boolean(config.features.mudFramework.isEnabled && mudTablesCountQuery.data && mudTablesCountQuery.data > 0),
);
const tabs: Array<RoutedTab> = React.useMemo(() => { const tabs: Array<RoutedTab> = React.useMemo(() => {
return [ return [
...@@ -245,7 +250,7 @@ const AddressPageContent = () => { ...@@ -245,7 +250,7 @@ const AddressPageContent = () => {
isLoading={ contractTabs.isLoading } isLoading={ contractTabs.isLoading }
/> />
), ),
subTabs: contractTabs.tabs.map(tab => tab.id), subTabs: CONTRACT_TAB_IDS,
} : undefined, } : undefined,
].filter(Boolean); ].filter(Boolean);
}, [ }, [
......
import { Button, Flex, Link, Text } from '@chakra-ui/react'; import { Button, Flex, Link, Skeleton, Text } from '@chakra-ui/react';
import type { NextRouter } from 'next/router'; import type { NextRouter } from 'next/router';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -179,13 +179,19 @@ const Chart = () => { ...@@ -179,13 +179,19 @@ const Chart = () => {
{ !isMobile && <Text>Period</Text> } { !isMobile && <Text>Period</Text> }
<ChartIntervalSelect interval={ interval } onIntervalChange={ onIntervalChange }/> <ChartIntervalSelect interval={ interval } onIntervalChange={ onIntervalChange }/>
</Flex> </Flex>
{ lineQuery.data?.info?.resolutions && lineQuery.data?.info?.resolutions.length > 1 && ( { (
(info?.resolutions && info?.resolutions.length > 1) ||
(!info && lineQuery.data?.info?.resolutions && lineQuery.data?.info?.resolutions.length > 1)
) && (
<Flex alignItems="center" gap={ 3 }> <Flex alignItems="center" gap={ 3 }>
<Text>{ isMobile ? 'Res.' : 'Resolution' }</Text> <Skeleton isLoaded={ !isInfoLoading }>
{ isMobile ? 'Res.' : 'Resolution' }
</Skeleton>
<ChartResolutionSelect <ChartResolutionSelect
resolution={ resolution } resolution={ resolution }
onResolutionChange={ onResolutionChange } onResolutionChange={ onResolutionChange }
resolutions={ lineQuery.data?.info?.resolutions || [] } resolutions={ lineQuery.data?.info?.resolutions || [] }
isLoading={ isInfoLoading }
/> />
</Flex> </Flex>
) } ) }
......
...@@ -9,7 +9,6 @@ import useApiQuery from 'lib/api/useApiQuery'; ...@@ -9,7 +9,6 @@ import useApiQuery from 'lib/api/useApiQuery';
import useDebounce from 'lib/hooks/useDebounce'; import useDebounce from 'lib/hooks/useDebounce';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { ADDRESS_REGEXP } from 'lib/validations/address';
import { ENS_DOMAIN } from 'stubs/ENS'; import { ENS_DOMAIN } from 'stubs/ENS';
import { generateListStub } from 'stubs/utils'; import { generateListStub } from 'stubs/utils';
import NameDomainsActionBar from 'ui/nameDomains/NameDomainsActionBar'; import NameDomainsActionBar from 'ui/nameDomains/NameDomainsActionBar';
...@@ -18,6 +17,7 @@ import NameDomainsTable from 'ui/nameDomains/NameDomainsTable'; ...@@ -18,6 +17,7 @@ import NameDomainsTable from 'ui/nameDomains/NameDomainsTable';
import type { Sort, SortField } from 'ui/nameDomains/utils'; import type { Sort, SortField } from 'ui/nameDomains/utils';
import { SORT_OPTIONS, getNextSortValue } from 'ui/nameDomains/utils'; import { SORT_OPTIONS, getNextSortValue } from 'ui/nameDomains/utils';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import { ADDRESS_REGEXP } from 'ui/shared/forms/validators/address';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue'; import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue';
......
...@@ -10,7 +10,6 @@ import type { RoutedTab } from 'ui/shared/Tabs/types'; ...@@ -10,7 +10,6 @@ import type { RoutedTab } from 'ui/shared/Tabs/types';
import config from 'configs/app'; import config from 'configs/app';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useContractTabs from 'lib/hooks/useContractTabs';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import * as metadata from 'lib/metadata'; import * as metadata from 'lib/metadata';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
...@@ -23,6 +22,8 @@ import { getTokenHoldersStub } from 'stubs/token'; ...@@ -23,6 +22,8 @@ import { getTokenHoldersStub } from 'stubs/token';
import { generateListStub } from 'stubs/utils'; import { generateListStub } from 'stubs/utils';
import AddressContract from 'ui/address/AddressContract'; import AddressContract from 'ui/address/AddressContract';
import AddressCsvExportLink from 'ui/address/AddressCsvExportLink'; import AddressCsvExportLink from 'ui/address/AddressCsvExportLink';
import useContractTabs from 'ui/address/contract/useContractTabs';
import { CONTRACT_TAB_IDS } from 'ui/address/contract/utils';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import Pagination from 'ui/shared/pagination/Pagination'; import Pagination from 'ui/shared/pagination/Pagination';
...@@ -191,7 +192,7 @@ const TokenPageContent = () => { ...@@ -191,7 +192,7 @@ const TokenPageContent = () => {
return 'Contract'; return 'Contract';
}, },
component: <AddressContract tabs={ contractTabs.tabs } isLoading={ contractTabs.isLoading } shouldRender={ !isLoading }/>, component: <AddressContract tabs={ contractTabs.tabs } isLoading={ contractTabs.isLoading } shouldRender={ !isLoading }/>,
subTabs: contractTabs.tabs.map(tab => tab.id), subTabs: CONTRACT_TAB_IDS,
} : undefined, } : undefined,
].filter(Boolean); ].filter(Boolean);
......
...@@ -3,18 +3,17 @@ import { ...@@ -3,18 +3,17 @@ import {
Button, Button,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useState } from 'react';
import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import type { AddressTag, AddressTagErrors } from 'types/api/account'; import type { AddressTag, AddressTagErrors } from 'types/api/account';
import type { ResourceErrorAccount } from 'lib/api/resources'; import type { ResourceErrorAccount } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import { ADDRESS_REGEXP } from 'lib/validations/address'; import FormFieldAddress from 'ui/shared/forms/fields/FormFieldAddress';
import AddressInput from 'ui/shared/AddressInput'; import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import TagInput from 'ui/shared/TagInput';
const TAG_MAX_LENGTH = 35; const TAG_MAX_LENGTH = 35;
...@@ -33,7 +32,7 @@ type Inputs = { ...@@ -33,7 +32,7 @@ type Inputs = {
const AddressForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisible }) => { const AddressForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisible }) => {
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const [ pending, setPending ] = useState(false); const [ pending, setPending ] = useState(false);
const { control, handleSubmit, formState: { errors, isDirty }, setError } = useForm<Inputs>({ const formApi = useForm<Inputs>({
mode: 'onTouched', mode: 'onTouched',
defaultValues: { defaultValues: {
address: data?.address_hash || '', address: data?.address_hash || '',
...@@ -41,7 +40,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisibl ...@@ -41,7 +40,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisibl
}, },
}); });
const { mutate } = useMutation({ const { mutateAsync } = useMutation({
mutationFn: (formData: Inputs) => { mutationFn: (formData: Inputs) => {
const body = { const body = {
name: formData?.tag, name: formData?.tag,
...@@ -62,10 +61,10 @@ const AddressForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisibl ...@@ -62,10 +61,10 @@ const AddressForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisibl
setPending(false); setPending(false);
const errorMap = error.payload?.errors; const errorMap = error.payload?.errors;
if (errorMap?.address_hash || errorMap?.name) { if (errorMap?.address_hash || errorMap?.name) {
errorMap?.address_hash && setError('address', { type: 'custom', message: getErrorMessage(errorMap, 'address_hash') }); errorMap?.address_hash && formApi.setError('address', { type: 'custom', message: getErrorMessage(errorMap, 'address_hash') });
errorMap?.name && setError('tag', { type: 'custom', message: getErrorMessage(errorMap, 'name') }); errorMap?.name && formApi.setError('tag', { type: 'custom', message: getErrorMessage(errorMap, 'name') });
} else if (errorMap?.identity_id) { } else if (errorMap?.identity_id) {
setError('address', { type: 'custom', message: getErrorMessage(errorMap, 'identity_id') }); formApi.setError('address', { type: 'custom', message: getErrorMessage(errorMap, 'identity_id') });
} else { } else {
setAlertVisible(true); setAlertVisible(true);
} }
...@@ -77,55 +76,43 @@ const AddressForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisibl ...@@ -77,55 +76,43 @@ const AddressForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisibl
}, },
}); });
const onSubmit: SubmitHandler<Inputs> = (formData) => { const onSubmit: SubmitHandler<Inputs> = async(formData) => {
setAlertVisible(false); setAlertVisible(false);
setPending(true); setPending(true);
mutate(formData); await mutateAsync(formData);
}; };
const renderAddressInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'address'>}) => {
return <AddressInput<Inputs, 'address'> field={ field } error={ errors.address } bgColor="dialog_bg"/>;
}, [ errors ]);
const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => {
return <TagInput<Inputs, 'tag'> field={ field } error={ errors.tag } bgColor="dialog_bg"/>;
}, [ errors ]);
return ( return (
<form noValidate onSubmit={ handleSubmit(onSubmit) }> <FormProvider { ...formApi }>
<Box marginBottom={ 5 }> <form noValidate onSubmit={ formApi.handleSubmit(onSubmit) }>
<Controller <FormFieldAddress<Inputs>
name="address" name="address"
control={ control } isRequired
rules={{ bgColor="dialog_bg"
pattern: ADDRESS_REGEXP, mb={ 5 }
required: true,
}}
render={ renderAddressInput }
/> />
</Box> <FormFieldText<Inputs>
<Box marginBottom={ 8 }>
<Controller
name="tag" name="tag"
control={ control } placeholder="Private tag (max 35 characters)"
isRequired
rules={{ rules={{
maxLength: TAG_MAX_LENGTH, maxLength: TAG_MAX_LENGTH,
required: true,
}} }}
render={ renderTagInput } bgColor="dialog_bg"
mb={ 8 }
/> />
</Box> <Box marginTop={ 8 }>
<Box marginTop={ 8 }> <Button
<Button size="lg"
size="lg" type="submit"
type="submit" isDisabled={ !formApi.formState.isDirty }
isDisabled={ !isDirty } isLoading={ pending }
isLoading={ pending } >
> { data ? 'Save changes' : 'Add tag' }
{ data ? 'Save changes' : 'Add tag' } </Button>
</Button> </Box>
</Box> </form>
</form> </FormProvider>
); );
}; };
......
...@@ -3,9 +3,9 @@ import { ...@@ -3,9 +3,9 @@ import {
Button, Button,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useState } from 'react';
import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form'; import { useForm, FormProvider } from 'react-hook-form';
import type { TransactionTag, TransactionTagErrors } from 'types/api/account'; import type { TransactionTag, TransactionTagErrors } from 'types/api/account';
...@@ -13,9 +13,8 @@ import type { ResourceErrorAccount } from 'lib/api/resources'; ...@@ -13,9 +13,8 @@ import type { ResourceErrorAccount } from 'lib/api/resources';
import { resourceKey } from 'lib/api/resources'; import { resourceKey } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import { TRANSACTION_HASH_REGEXP } from 'lib/validations/transaction'; import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import TagInput from 'ui/shared/TagInput'; import { TRANSACTION_HASH_LENGTH, TRANSACTION_HASH_REGEXP } from 'ui/shared/forms/validators/transaction';
import TransactionInput from 'ui/shared/TransactionInput';
const TAG_MAX_LENGTH = 35; const TAG_MAX_LENGTH = 35;
...@@ -34,7 +33,7 @@ type Inputs = { ...@@ -34,7 +33,7 @@ type Inputs = {
const TransactionForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisible }) => { const TransactionForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisible }) => {
const [ pending, setPending ] = useState(false); const [ pending, setPending ] = useState(false);
const { control, handleSubmit, formState: { errors, isDirty }, setError } = useForm<Inputs>({ const formApi = useForm<Inputs>({
mode: 'onTouched', mode: 'onTouched',
defaultValues: { defaultValues: {
transaction: data?.transaction_hash || '', transaction: data?.transaction_hash || '',
...@@ -45,7 +44,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVi ...@@ -45,7 +44,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVi
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const { mutate } = useMutation({ const { mutateAsync } = useMutation({
mutationFn: (formData: Inputs) => { mutationFn: (formData: Inputs) => {
const body = { const body = {
name: formData?.tag, name: formData?.tag,
...@@ -66,10 +65,10 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVi ...@@ -66,10 +65,10 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVi
setPending(false); setPending(false);
const errorMap = error.payload?.errors; const errorMap = error.payload?.errors;
if (errorMap?.tx_hash || errorMap?.name) { if (errorMap?.tx_hash || errorMap?.name) {
errorMap?.tx_hash && setError('transaction', { type: 'custom', message: getErrorMessage(errorMap, 'tx_hash') }); errorMap?.tx_hash && formApi.setError('transaction', { type: 'custom', message: getErrorMessage(errorMap, 'tx_hash') });
errorMap?.name && setError('tag', { type: 'custom', message: getErrorMessage(errorMap, 'name') }); errorMap?.name && formApi.setError('tag', { type: 'custom', message: getErrorMessage(errorMap, 'name') });
} else if (errorMap?.identity_id) { } else if (errorMap?.identity_id) {
setError('transaction', { type: 'custom', message: getErrorMessage(errorMap, 'identity_id') }); formApi.setError('transaction', { type: 'custom', message: getErrorMessage(errorMap, 'identity_id') });
} else { } else {
setAlertVisible(true); setAlertVisible(true);
} }
...@@ -82,54 +81,47 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVi ...@@ -82,54 +81,47 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVi
}, },
}); });
const onSubmit: SubmitHandler<Inputs> = formData => { const onSubmit: SubmitHandler<Inputs> = async(formData) => {
setPending(true); setPending(true);
mutate(formData); await mutateAsync(formData);
}; };
const renderTransactionInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'transaction'>}) => {
return <TransactionInput field={ field } error={ errors.transaction } bgColor="dialog_bg"/>;
}, [ errors ]);
const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => {
return <TagInput<Inputs, 'tag'> field={ field } error={ errors.tag } bgColor="dialog_bg"/>;
}, [ errors ]);
return ( return (
<form noValidate onSubmit={ handleSubmit(onSubmit) }> <FormProvider { ...formApi }>
<Box marginBottom={ 5 }> <form noValidate onSubmit={ formApi.handleSubmit(onSubmit) }>
<Controller <FormFieldText<Inputs>
name="transaction" name="transaction"
control={ control } placeholder="Transaction hash (0x...)"
isRequired
rules={{ rules={{
maxLength: TRANSACTION_HASH_LENGTH,
pattern: TRANSACTION_HASH_REGEXP, pattern: TRANSACTION_HASH_REGEXP,
required: true,
}} }}
render={ renderTransactionInput } bgColor="dialog_bg"
mb={ 5 }
/> />
</Box> <FormFieldText<Inputs>
<Box marginBottom={ 8 }>
<Controller
name="tag" name="tag"
control={ control } placeholder="Private tag (max 35 characters)"
isRequired
rules={{ rules={{
maxLength: TAG_MAX_LENGTH, maxLength: TAG_MAX_LENGTH,
required: true,
}} }}
render={ renderTagInput } bgColor="dialog_bg"
mb={ 8 }
/> />
</Box> <Box marginTop={ 8 }>
<Box marginTop={ 8 }> <Button
<Button size="lg"
size="lg" type="submit"
type="submit" isDisabled={ !formApi.formState.isDirty }
isDisabled={ !isDirty } isLoading={ pending }
isLoading={ pending } >
> { data ? 'Save changes' : 'Add tag' }
{ data ? 'Save changes' : 'Add tag' } </Button>
</Button> </Box>
</Box> </form>
</form> </FormProvider>
); );
}; };
......
...@@ -14,15 +14,13 @@ import useApiFetch from 'lib/api/useApiFetch'; ...@@ -14,15 +14,13 @@ import useApiFetch from 'lib/api/useApiFetch';
import getErrorObj from 'lib/errors/getErrorObj'; import getErrorObj from 'lib/errors/getErrorObj';
import getErrorObjPayload from 'lib/errors/getErrorObjPayload'; import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import FormFieldEmail from 'ui/shared/forms/fields/FormFieldEmail';
import FormFieldReCaptcha from 'ui/shared/forms/fields/FormFieldReCaptcha'; import FormFieldReCaptcha from 'ui/shared/forms/fields/FormFieldReCaptcha';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import FormFieldUrl from 'ui/shared/forms/fields/FormFieldUrl';
import Hint from 'ui/shared/Hint'; import Hint from 'ui/shared/Hint';
import PublicTagsSubmitFieldAddresses from './fields/PublicTagsSubmitFieldAddresses'; import PublicTagsSubmitFieldAddresses from './fields/PublicTagsSubmitFieldAddresses';
import PublicTagsSubmitFieldCompanyName from './fields/PublicTagsSubmitFieldCompanyName';
import PublicTagsSubmitFieldCompanyWebsite from './fields/PublicTagsSubmitFieldCompanyWebsite';
import PublicTagsSubmitFieldDescription from './fields/PublicTagsSubmitFieldDescription';
import PublicTagsSubmitFieldRequesterEmail from './fields/PublicTagsSubmitFieldRequesterEmail';
import PublicTagsSubmitFieldRequesterName from './fields/PublicTagsSubmitFieldRequesterName';
import PublicTagsSubmitFieldTags from './fields/PublicTagsSubmitFieldTags'; import PublicTagsSubmitFieldTags from './fields/PublicTagsSubmitFieldTags';
import { convertFormDataToRequestsBody, getFormDefaultValues } from './utils'; import { convertFormDataToRequestsBody, getFormDefaultValues } from './utils';
...@@ -82,6 +80,10 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => { ...@@ -82,6 +80,10 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => {
return null; return null;
} }
const fieldProps = {
size: { base: 'md', lg: 'lg' },
};
return ( return (
<GoogleReCaptchaProvider reCaptchaKey={ appConfig.services.reCaptchaV3.siteKey }> <GoogleReCaptchaProvider reCaptchaKey={ appConfig.services.reCaptchaV3.siteKey }>
<FormProvider { ...formApi }> <FormProvider { ...formApi }>
...@@ -97,21 +99,34 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => { ...@@ -97,21 +99,34 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => {
<GridItem colSpan={{ base: 1, lg: 3 }} as="h2" textStyle="h4"> <GridItem colSpan={{ base: 1, lg: 3 }} as="h2" textStyle="h4">
Company info Company info
</GridItem> </GridItem>
<PublicTagsSubmitFieldRequesterName/> <FormFieldText<FormFields> name="requesterName" isRequired placeholder="Your name" { ...fieldProps }/>
<PublicTagsSubmitFieldRequesterEmail/> <FormFieldEmail<FormFields> name="requesterEmail" isRequired { ...fieldProps }/>
{ !isMobile && <div/> } { !isMobile && <div/> }
<PublicTagsSubmitFieldCompanyName/> <FormFieldText<FormFields> name="companyName" placeholder="Company name" { ...fieldProps }/>
<PublicTagsSubmitFieldCompanyWebsite/> <FormFieldUrl<FormFields> name="companyWebsite" placeholder="Company website" { ...fieldProps }/>
{ !isMobile && <div/> } { !isMobile && <div/> }
<GridItem colSpan={{ base: 1, lg: 3 }} as="h2" textStyle="h4" mt={{ base: 3, lg: 5 }}> <GridItem colSpan={{ base: 1, lg: 3 }} as="h2" textStyle="h4" mt={{ base: 3, lg: 5 }}>
Public tags/labels Public tags/labels
<Hint label="Submit a public tag proposal for our moderation team to review" ml={ 1 } color="link"/> <Hint label="Submit a public tag proposal for our moderation team to review" ml={ 1 } color="link"/>
</GridItem> </GridItem>
<PublicTagsSubmitFieldAddresses/> <PublicTagsSubmitFieldAddresses/>
<PublicTagsSubmitFieldTags tagTypes={ config?.tagTypes }/> <PublicTagsSubmitFieldTags tagTypes={ config?.tagTypes }/>
<GridItem colSpan={{ base: 1, lg: 2 }}> <GridItem colSpan={{ base: 1, lg: 2 }}>
<PublicTagsSubmitFieldDescription/> <FormFieldText<FormFields>
name="description"
isRequired
placeholder={
isMobile ?
'Confirm the connection between addresses and tags.' :
'Provide a comment to confirm the connection between addresses and tags.'
}
maxH="160px"
rules={{ maxLength: 80 }}
asComponent="Textarea"
{ ...fieldProps }
/>
</GridItem> </GridItem>
<GridItem colSpan={{ base: 1, lg: 3 }}> <GridItem colSpan={{ base: 1, lg: 3 }}>
......
import { FormControl, GridItem, IconButton, Input } from '@chakra-ui/react'; import { GridItem, IconButton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form'; import { useFieldArray, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import { ADDRESS_REGEXP } from 'lib/validations/address'; import FormFieldAddress from 'ui/shared/forms/fields/FormFieldAddress';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
const LIMIT = 20; const LIMIT = 20;
const PublicTagsSubmitFieldAddresses = () => { const PublicTagsSubmitFieldAddresses = () => {
const { control, formState, register } = useFormContext<FormFields>(); const { control, formState } = useFormContext<FormFields>();
const { fields, insert, remove } = useFieldArray<FormFields, 'addresses'>({ const { fields, insert, remove } = useFieldArray<FormFields, 'addresses'>({
name: 'addresses', name: 'addresses',
control, control,
...@@ -36,20 +35,15 @@ const PublicTagsSubmitFieldAddresses = () => { ...@@ -36,20 +35,15 @@ const PublicTagsSubmitFieldAddresses = () => {
return ( return (
<> <>
{ fields.map((field, index) => { { fields.map((field, index) => {
const error = formState.errors?.addresses?.[ index ]?.hash;
return ( return (
<React.Fragment key={ field.id }> <React.Fragment key={ field.id }>
<GridItem colSpan={{ base: 1, lg: 2 }}> <GridItem colSpan={{ base: 1, lg: 2 }}>
<FormControl variant="floating" isRequired size={{ base: 'md', lg: 'lg' }}> <FormFieldAddress<FormFields>
<Input name={ `addresses.${ index }.hash` }
{ ...register(`addresses.${ index }.hash`, { required: true, pattern: ADDRESS_REGEXP }) } isRequired
isInvalid={ Boolean(error) } placeholder="Smart contract / Address (0x...)"
isDisabled={ formState.isSubmitting } size={{ base: 'md', lg: 'lg' }}
autoComplete="off" />
/>
<InputPlaceholder text="Smart contract / Address (0x...)" error={ error }/>
</FormControl>
</GridItem> </GridItem>
<GridItem display="flex" alignItems="center" columnGap={ 5 } justifyContent={{ base: 'flex-end', lg: 'flex-start' }}> <GridItem display="flex" alignItems="center" columnGap={ 5 } justifyContent={{ base: 'flex-end', lg: 'flex-start' }}>
{ fields.length < LIMIT && index === fields.length - 1 && ( { fields.length < LIMIT && index === fields.length - 1 && (
......
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import { useController, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
const PublicTagsSubmitFieldCompanyName = () => {
const { control } = useFormContext<FormFields>();
const { field, fieldState, formState } = useController<FormFields, 'companyName'>({ control, name: 'companyName' });
const isDisabled = formState.isSubmitting;
return (
<FormControl variant="floating" isDisabled={ isDisabled } size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...field }
isInvalid={ Boolean(fieldState.error) }
isDisabled={ isDisabled }
autoComplete="off"
/>
<InputPlaceholder text="Company name" error={ fieldState.error }/>
</FormControl>
);
};
export default React.memo(PublicTagsSubmitFieldCompanyName);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import { useController, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types';
import { validator as urlValidator } from 'lib/validations/url';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
const PublicTagsSubmitFieldCompanyWebsite = () => {
const { control } = useFormContext<FormFields>();
const { field, fieldState, formState } = useController<FormFields, 'companyWebsite'>({ control, name: 'companyWebsite', rules: { validate: urlValidator } });
const isDisabled = formState.isSubmitting;
return (
<FormControl variant="floating" isDisabled={ isDisabled } size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...field }
isInvalid={ Boolean(fieldState.error) }
isDisabled={ isDisabled }
autoComplete="off"
/>
<InputPlaceholder text="Company website" error={ fieldState.error }/>
</FormControl>
);
};
export default React.memo(PublicTagsSubmitFieldCompanyWebsite);
import { FormControl, Textarea } from '@chakra-ui/react';
import React from 'react';
import { useController, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types';
import useIsMobile from 'lib/hooks/useIsMobile';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
const MAX_LENGTH = 80;
const PublicTagsSubmitFieldDescription = () => {
const isMobile = useIsMobile();
const { control } = useFormContext<FormFields>();
const { field, fieldState, formState } = useController<FormFields, 'description'>({
control,
name: 'description',
rules: { maxLength: MAX_LENGTH, required: true },
});
const isDisabled = formState.isSubmitting;
return (
<FormControl variant="floating" isDisabled={ isDisabled } isRequired size={{ base: 'md', lg: 'lg' }}>
<Textarea
{ ...field }
isInvalid={ Boolean(fieldState.error) }
isDisabled={ isDisabled }
autoComplete="off"
maxH="160px"
maxLength={ MAX_LENGTH }
/>
<InputPlaceholder
text={ isMobile ? 'Confirm the connection between addresses and tags.' : 'Provide a comment to confirm the connection between addresses and tags.' }
error={ fieldState.error }
/>
</FormControl>
);
};
export default React.memo(PublicTagsSubmitFieldDescription);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import { useController, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types';
import { EMAIL_REGEXP } from 'lib/validations/email';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
const PublicTagsSubmitFieldRequesterEmail = () => {
const { control } = useFormContext<FormFields>();
const { field, fieldState, formState } = useController<FormFields, 'requesterEmail'>({
control,
name: 'requesterEmail',
rules: { required: true, pattern: EMAIL_REGEXP },
});
const isDisabled = formState.isSubmitting;
return (
<FormControl variant="floating" isDisabled={ isDisabled } isRequired size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...field }
required
isInvalid={ Boolean(fieldState.error) }
isDisabled={ isDisabled }
autoComplete="off"
/>
<InputPlaceholder text="Email" error={ fieldState.error }/>
</FormControl>
);
};
export default React.memo(PublicTagsSubmitFieldRequesterEmail);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import { useController, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
const PublicTagsSubmitFieldRequesterName = () => {
const { control } = useFormContext<FormFields>();
const { field, fieldState, formState } = useController<FormFields, 'requesterName'>({ control, name: 'requesterName', rules: { required: true } });
const isDisabled = formState.isSubmitting;
return (
<FormControl variant="floating" isDisabled={ isDisabled } isRequired size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...field }
required
isInvalid={ Boolean(fieldState.error) }
isDisabled={ isDisabled }
autoComplete="off"
/>
<InputPlaceholder text="Your name" error={ fieldState.error }/>
</FormControl>
);
};
export default React.memo(PublicTagsSubmitFieldRequesterName);
import { chakra, Flex, FormControl, Grid, GridItem, IconButton, Input, Textarea, useColorModeValue } from '@chakra-ui/react'; import { chakra, Flex, Grid, GridItem, IconButton, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { type FieldError, type FieldErrorsImpl, type Merge, type UseFormRegister } from 'react-hook-form'; import { type FieldError, type FieldErrorsImpl, type Merge } from 'react-hook-form';
import type { FormFields, FormFieldTag } from '../types'; import type { FormFields, FormFieldTag } from '../types';
import type { PublicTagType } from 'types/api/addressMetadata'; import type { PublicTagType } from 'types/api/addressMetadata';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import { validator as colorValidator } from 'lib/validations/color';
import { validator as urlValidator } from 'lib/validations/url';
import EntityTag from 'ui/shared/EntityTags/EntityTag'; import EntityTag from 'ui/shared/EntityTags/EntityTag';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import FormFieldUrl from 'ui/shared/forms/fields/FormFieldUrl';
import { validator as colorValidator } from 'ui/shared/forms/validators/color';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import PublicTagsSubmitFieldTagColor from './PublicTagsSubmitFieldTagColor'; import PublicTagsSubmitFieldTagColor from './PublicTagsSubmitFieldTagColor';
import PublicTagsSubmitFieldTagType from './PublicTagsSubmitFieldTagType'; import PublicTagsSubmitFieldTagType from './PublicTagsSubmitFieldTagType';
...@@ -19,14 +19,13 @@ interface Props { ...@@ -19,14 +19,13 @@ interface Props {
index: number; index: number;
field: FormFieldTag; field: FormFieldTag;
tagTypes: Array<PublicTagType> | undefined; tagTypes: Array<PublicTagType> | undefined;
register: UseFormRegister<FormFields>;
errors: Merge<FieldError, FieldErrorsImpl<FormFieldTag>> | undefined; errors: Merge<FieldError, FieldErrorsImpl<FormFieldTag>> | undefined;
isDisabled: boolean; isDisabled: boolean;
onAddClick?: (index: number) => void; onAddClick?: (index: number) => void;
onRemoveClick?: (index: number) => void; onRemoveClick?: (index: number) => void;
} }
const PublicTagsSubmitFieldTag = ({ index, isDisabled, register, errors, onAddClick, onRemoveClick, tagTypes, field }: Props) => { const PublicTagsSubmitFieldTag = ({ index, isDisabled, errors, onAddClick, onRemoveClick, tagTypes, field }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const bgColorDefault = useColorModeValue('blackAlpha.50', 'whiteAlpha.100'); const bgColorDefault = useColorModeValue('blackAlpha.50', 'whiteAlpha.100');
const bgColorError = useColorModeValue('red.50', 'red.900'); const bgColorError = useColorModeValue('red.50', 'red.900');
...@@ -39,6 +38,10 @@ const PublicTagsSubmitFieldTag = ({ index, isDisabled, register, errors, onAddCl ...@@ -39,6 +38,10 @@ const PublicTagsSubmitFieldTag = ({ index, isDisabled, register, errors, onAddCl
onRemoveClick?.(index); onRemoveClick?.(index);
}, [ index, onRemoveClick ]); }, [ index, onRemoveClick ]);
const fieldProps = {
size: { base: 'md', lg: 'lg' },
};
return ( return (
<> <>
<GridItem colSpan={{ base: 1, lg: 2 }} p="10px" borderRadius="base" bgColor={ errors ? bgColorError : bgColorDefault }> <GridItem colSpan={{ base: 1, lg: 2 }} p="10px" borderRadius="base" bgColor={ errors ? bgColorError : bgColorDefault }>
...@@ -48,62 +51,45 @@ const PublicTagsSubmitFieldTag = ({ index, isDisabled, register, errors, onAddCl ...@@ -48,62 +51,45 @@ const PublicTagsSubmitFieldTag = ({ index, isDisabled, register, errors, onAddCl
templateColumns={{ base: '1fr', lg: 'repeat(4, 1fr)' }} templateColumns={{ base: '1fr', lg: 'repeat(4, 1fr)' }}
> >
<GridItem colSpan={{ base: 1, lg: 2 }}> <GridItem colSpan={{ base: 1, lg: 2 }}>
<FormControl variant="floating" isRequired size={{ base: 'md', lg: 'lg' }}> <FormFieldText<FormFields>
<Input name={ `tags.${ index }.name` }
{ ...register(`tags.${ index }.name`, { required: true, maxLength: 35 }) } placeholder="Tag (max 35 characters)"
isInvalid={ Boolean(errors?.name) } isRequired
isDisabled={ isDisabled } rules={{ maxLength: 35 }}
autoComplete="off" { ...fieldProps }
/> />
<InputPlaceholder text="Tag (max 35 characters)" error={ errors?.name }/>
</FormControl>
</GridItem> </GridItem>
<GridItem colSpan={{ base: 1, lg: 2 }}> <GridItem colSpan={{ base: 1, lg: 2 }}>
<PublicTagsSubmitFieldTagType index={ index } tagTypes={ tagTypes } isDisabled={ isDisabled }/> <PublicTagsSubmitFieldTagType index={ index } tagTypes={ tagTypes }/>
</GridItem> </GridItem>
<GridItem colSpan={{ base: 1, lg: 2 }}> <GridItem colSpan={{ base: 1, lg: 2 }}>
<FormControl variant="floating" size={{ base: 'md', lg: 'lg' }}> <FormFieldUrl<FormFields>
<Input name={ `tags.${ index }.url` }
{ ...register(`tags.${ index }.url`, { validate: urlValidator }) } placeholder="Label URL"
isInvalid={ Boolean(errors?.url) } { ...fieldProps }
isDisabled={ isDisabled } />
autoComplete="off"
/>
<InputPlaceholder text="Label URL" error={ errors?.url }/>
</FormControl>
</GridItem> </GridItem>
<PublicTagsSubmitFieldTagColor <PublicTagsSubmitFieldTagColor
fieldType="bgColor" fieldType="bgColor"
fieldName={ `tags.${ index }.bgColor` } fieldName={ `tags.${ index }.bgColor` }
placeholder="Background (Hex)" placeholder="Background (Hex)"
index={ index }
register={ register }
error={ errors?.bgColor } error={ errors?.bgColor }
isDisabled={ isDisabled }
/> />
<PublicTagsSubmitFieldTagColor <PublicTagsSubmitFieldTagColor
fieldType="textColor" fieldType="textColor"
fieldName={ `tags.${ index }.textColor` } fieldName={ `tags.${ index }.textColor` }
placeholder="Text (Hex)" placeholder="Text (Hex)"
index={ index }
register={ register }
error={ errors?.textColor } error={ errors?.textColor }
isDisabled={ isDisabled }
/> />
<GridItem colSpan={{ base: 1, lg: 4 }}> <GridItem colSpan={{ base: 1, lg: 4 }}>
<FormControl variant="floating" size={{ base: 'md', lg: 'lg' }}> <FormFieldText<FormFields>
<Textarea name={ `tags.${ index }.tooltipDescription` }
{ ...register(`tags.${ index }.tooltipDescription`, { maxLength: 80 }) } placeholder="Label description (max 80 characters)"
isInvalid={ Boolean(errors?.tooltipDescription) } maxH="160px"
isDisabled={ isDisabled } rules={{ maxLength: 80 }}
autoComplete="off" asComponent="Textarea"
maxH="160px" { ...fieldProps }
/> />
<InputPlaceholder
text="Label description (max 80 characters)"
error={ errors?.tooltipDescription }
/>
</FormControl>
</GridItem> </GridItem>
</Grid> </Grid>
</GridItem> </GridItem>
......
import { Circle, FormControl, Input, InputGroup, InputRightElement, useColorModeValue } from '@chakra-ui/react'; import { Circle, FormControl, Input, InputGroup, InputRightElement, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { useFormContext, type FieldError, type UseFormRegister } from 'react-hook-form'; import { useFormContext, type FieldError } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import { validator as colorValidator } from 'lib/validations/color'; import FormInputPlaceholder from 'ui/shared/forms/inputs/FormInputPlaceholder';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import { validator as colorValidator } from 'ui/shared/forms/validators/color';
type ColorFieldTypes = 'bgColor' | 'textColor'; type ColorFieldTypes = 'bgColor' | 'textColor';
interface Props<Type extends ColorFieldTypes> { interface Props<Type extends ColorFieldTypes> {
fieldType: Type; fieldType: Type;
fieldName: `tags.${ number }.${ Type }`; fieldName: `tags.${ number }.${ Type }`;
index: number;
isDisabled: boolean;
register: UseFormRegister<FormFields>;
error: FieldError | undefined; error: FieldError | undefined;
placeholder: string; placeholder: string;
} }
const PublicTagsSubmitFieldTagColor = <Type extends ColorFieldTypes>({ isDisabled, error, fieldName, placeholder, fieldType }: Props<Type>) => { const PublicTagsSubmitFieldTagColor = <Type extends ColorFieldTypes>({ error, fieldName, placeholder, fieldType }: Props<Type>) => {
const { register } = useFormContext<FormFields>(); const { register, formState } = useFormContext<FormFields>();
const isDisabled = formState.isSubmitting;
const circleBgColorDefault = { const circleBgColorDefault = {
bgColor: useColorModeValue('gray.100', 'gray.700'), bgColor: useColorModeValue('gray.100', 'gray.700'),
...@@ -57,7 +56,7 @@ const PublicTagsSubmitFieldTagColor = <Type extends ColorFieldTypes>({ isDisable ...@@ -57,7 +56,7 @@ const PublicTagsSubmitFieldTagColor = <Type extends ColorFieldTypes>({ isDisable
autoComplete="off" autoComplete="off"
maxLength={ 7 } maxLength={ 7 }
/> />
<InputPlaceholder text={ placeholder } error={ error }/> <FormInputPlaceholder text={ placeholder } error={ error }/>
<InputRightElement w="30px" h="auto" right={ 4 } top="50%" transform="translateY(-50%)" zIndex={ 10 }> <InputRightElement w="30px" h="auto" right={ 4 } top="50%" transform="translateY(-50%)" zIndex={ 10 }>
<Circle <Circle
size="30px" size="30px"
......
import { chakra, Flex, FormControl } from '@chakra-ui/react'; import { chakra, Flex } from '@chakra-ui/react';
import type { GroupBase, SelectComponentsConfig, SingleValueProps } from 'chakra-react-select'; import type { GroupBase, SelectComponentsConfig, SingleValueProps } from 'chakra-react-select';
import { chakraComponents } from 'chakra-react-select'; import { chakraComponents } from 'chakra-react-select';
import _capitalize from 'lodash/capitalize'; import _capitalize from 'lodash/capitalize';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import type { PublicTagType } from 'types/api/addressMetadata'; import type { PublicTagType } from 'types/api/addressMetadata';
import type { Option } from 'ui/shared/FancySelect/types'; import type { Option } from 'ui/shared/forms/inputs/select/types';
import useIsMobile from 'lib/hooks/useIsMobile'; import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
interface Props { interface Props {
index: number; index: number;
tagTypes: Array<PublicTagType> | undefined; tagTypes: Array<PublicTagType> | undefined;
isDisabled: boolean;
} }
const PublicTagsSubmitFieldTagType = ({ index, tagTypes, isDisabled }: Props) => { const PublicTagsSubmitFieldTagType = ({ index, tagTypes }: Props) => {
const isMobile = useIsMobile(); const { watch } = useFormContext<FormFields>();
const { control, watch } = useFormContext<FormFields>();
const typeOptions = React.useMemo(() => tagTypes?.map((type) => ({ const typeOptions = React.useMemo(() => tagTypes?.map((type) => ({
value: type.type, value: type.type,
label: _capitalize(type.type), label: _capitalize(type.type),
})), [ tagTypes ]); })) ?? [], [ tagTypes ]);
const fieldValue = watch(`tags.${ index }.type`).value; const fieldValue = watch(`tags.${ index }.type`).value;
...@@ -63,30 +59,15 @@ const PublicTagsSubmitFieldTagType = ({ index, tagTypes, isDisabled }: Props) => ...@@ -63,30 +59,15 @@ const PublicTagsSubmitFieldTagType = ({ index, tagTypes, isDisabled }: Props) =>
return { SingleValue }; return { SingleValue };
}, [ fieldValue ]); }, [ fieldValue ]);
const renderControl = React.useCallback(({ field }: { field: ControllerRenderProps<FormFields, `tags.${ number }.type`> }) => {
return (
<FormControl variant="floating" id={ field.name } isRequired size={{ base: 'md', lg: 'lg' }}>
<FancySelect
{ ...field }
options={ typeOptions }
size={ isMobile ? 'md' : 'lg' }
placeholder="Tag type"
isDisabled={ isDisabled }
isRequired
isAsync={ false }
isSearchable={ false }
components={ selectComponents }
/>
</FormControl>
);
}, [ isDisabled, isMobile, selectComponents, typeOptions ]);
return ( return (
<Controller <FormFieldFancySelect<FormFields, `tags.${ number }.type`>
name={ `tags.${ index }.type` } name={ `tags.${ index }.type` }
control={ control } placeholder="Tag type"
render={ renderControl } options={ typeOptions }
rules={{ required: true }} isRequired
isAsync={ false }
isSearchable={ false }
components={ selectComponents }
/> />
); );
}; };
......
...@@ -13,7 +13,7 @@ interface Props { ...@@ -13,7 +13,7 @@ interface Props {
} }
const PublicTagsSubmitFieldTags = ({ tagTypes }: Props) => { const PublicTagsSubmitFieldTags = ({ tagTypes }: Props) => {
const { control, formState, register, watch } = useFormContext<FormFields>(); const { control, formState, watch } = useFormContext<FormFields>();
const { fields, insert, remove } = useFieldArray<FormFields, 'tags'>({ const { fields, insert, remove } = useFieldArray<FormFields, 'tags'>({
name: 'tags', name: 'tags',
control, control,
...@@ -47,7 +47,6 @@ const PublicTagsSubmitFieldTags = ({ tagTypes }: Props) => { ...@@ -47,7 +47,6 @@ const PublicTagsSubmitFieldTags = ({ tagTypes }: Props) => {
field={ watch(`tags.${ index }`) } field={ watch(`tags.${ index }`) }
index={ index } index={ index }
tagTypes={ tagTypes } tagTypes={ tagTypes }
register={ register }
errors={ errors } errors={ errors }
isDisabled={ isDisabled } isDisabled={ isDisabled }
onAddClick={ fields.length < LIMIT && index === fields.length - 1 ? handleAddFieldClick : undefined } onAddClick={ fields.length < LIMIT && index === fields.length - 1 ? handleAddFieldClick : undefined }
......
import type { AddressMetadataTagType } from 'types/api/addressMetadata'; import type { AddressMetadataTagType } from 'types/api/addressMetadata';
import type { Option } from 'ui/shared/forms/inputs/select/types';
export interface FormFields { export interface FormFields {
requesterName: string; requesterName: string;
...@@ -13,10 +14,7 @@ export interface FormFields { ...@@ -13,10 +14,7 @@ export interface FormFields {
export interface FormFieldTag { export interface FormFieldTag {
name: string; name: string;
type: { type: Option<AddressMetadataTagType>;
label: string;
value: AddressMetadataTagType;
};
url: string | undefined; url: string | undefined;
bgColor: string | undefined; bgColor: string | undefined;
textColor: string | undefined; textColor: string | undefined;
......
...@@ -10,7 +10,6 @@ import dayjs from 'lib/date/dayjs'; ...@@ -10,7 +10,6 @@ import dayjs from 'lib/date/dayjs';
import highlightText from 'lib/highlightText'; import highlightText from 'lib/highlightText';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import { saveToRecentKeywords } from 'lib/recentSearchKeywords'; import { saveToRecentKeywords } from 'lib/recentSearchKeywords';
import { ADDRESS_REGEXP } from 'lib/validations/address';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel'; import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
import * as AddressEntity from 'ui/shared/entities/address/AddressEntity'; import * as AddressEntity from 'ui/shared/entities/address/AddressEntity';
import * as BlobEntity from 'ui/shared/entities/blob/BlobEntity'; import * as BlobEntity from 'ui/shared/entities/blob/BlobEntity';
...@@ -19,6 +18,7 @@ import * as EnsEntity from 'ui/shared/entities/ens/EnsEntity'; ...@@ -19,6 +18,7 @@ import * as EnsEntity from 'ui/shared/entities/ens/EnsEntity';
import * as TokenEntity from 'ui/shared/entities/token/TokenEntity'; import * as TokenEntity from 'ui/shared/entities/token/TokenEntity';
import * as TxEntity from 'ui/shared/entities/tx/TxEntity'; import * as TxEntity from 'ui/shared/entities/tx/TxEntity';
import * as UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity'; import * as UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity';
import { ADDRESS_REGEXP } from 'ui/shared/forms/validators/address';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/links/LinkExternal'; import LinkExternal from 'ui/shared/links/LinkExternal';
......
...@@ -10,7 +10,6 @@ import dayjs from 'lib/date/dayjs'; ...@@ -10,7 +10,6 @@ import dayjs from 'lib/date/dayjs';
import highlightText from 'lib/highlightText'; import highlightText from 'lib/highlightText';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import { saveToRecentKeywords } from 'lib/recentSearchKeywords'; import { saveToRecentKeywords } from 'lib/recentSearchKeywords';
import { ADDRESS_REGEXP } from 'lib/validations/address';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel'; import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
import * as AddressEntity from 'ui/shared/entities/address/AddressEntity'; import * as AddressEntity from 'ui/shared/entities/address/AddressEntity';
import * as BlobEntity from 'ui/shared/entities/blob/BlobEntity'; import * as BlobEntity from 'ui/shared/entities/blob/BlobEntity';
...@@ -19,6 +18,7 @@ import * as EnsEntity from 'ui/shared/entities/ens/EnsEntity'; ...@@ -19,6 +18,7 @@ import * as EnsEntity from 'ui/shared/entities/ens/EnsEntity';
import * as TokenEntity from 'ui/shared/entities/token/TokenEntity'; import * as TokenEntity from 'ui/shared/entities/token/TokenEntity';
import * as TxEntity from 'ui/shared/entities/tx/TxEntity'; import * as TxEntity from 'ui/shared/entities/tx/TxEntity';
import * as UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity'; import * as UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity';
import { ADDRESS_REGEXP } from 'ui/shared/forms/validators/address';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/links/LinkExternal'; import LinkExternal from 'ui/shared/links/LinkExternal';
......
import type { InputProps } from '@chakra-ui/react';
import {
Input,
FormControl,
} from '@chakra-ui/react';
import React from 'react';
import type { ControllerRenderProps, FieldError, FieldValues, Path } from 'react-hook-form';
import { ADDRESS_LENGTH } from 'lib/validations/address';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Props<TInputs extends FieldValues, TInputName extends Path<TInputs>> = {
field: ControllerRenderProps<TInputs, TInputName>;
size?: InputProps['size'];
placeholder?: string;
bgColor?: string;
error?: FieldError;
}
export default function AddressInput<Inputs extends FieldValues, Name extends Path<Inputs>>(
{
error,
field,
size,
placeholder = 'Address (0x...)',
bgColor,
}: Props<Inputs, Name>) {
return (
<FormControl variant="floating" id="address" isRequired size={ size } bgColor={ bgColor }>
<Input
{ ...field }
isInvalid={ Boolean(error) }
maxLength={ ADDRESS_LENGTH }
bgColor={ bgColor }
/>
<InputPlaceholder text={ placeholder } error={ error }/>
</FormControl>
);
}
import {
Checkbox,
} from '@chakra-ui/react';
import React from 'react';
import type { ControllerRenderProps, FieldValues, Path } from 'react-hook-form';
type Props<TInputs extends FieldValues, TInputName extends Path<TInputs>> = {
field: ControllerRenderProps<TInputs, TInputName>;
text: string;
onChange?: () => void;
isDisabled?: boolean;
}
export default function CheckboxInput<Inputs extends FieldValues, Name extends Path<Inputs>>(
{
field,
text,
onChange,
isDisabled,
}: Props<Inputs, Name>) {
const handleChange: typeof field.onChange = React.useCallback((...args) => {
field.onChange(...args);
onChange?.();
}, [ field, onChange ]);
return (
<Checkbox
isChecked={ field.value }
onChange={ handleChange }
ref={ field.ref }
colorScheme="blue"
size="lg"
isDisabled={ isDisabled }
>
{ text }
</Checkbox>
);
}
export interface Option {
label: string;
value: string;
}
...@@ -37,7 +37,7 @@ const AdaptiveTabsList = (props: Props) => { ...@@ -37,7 +37,7 @@ const AdaptiveTabsList = (props: Props) => {
return [ ...props.tabs, menuButton ]; return [ ...props.tabs, menuButton ];
}, [ props.tabs ]); }, [ props.tabs ]);
const { tabsCut, tabsRefs, listRef, rightSlotRef } = useAdaptiveTabs(tabsList, isMobile); const { tabsCut, tabsRefs, listRef, rightSlotRef, leftSlotRef } = useAdaptiveTabs(tabsList, isMobile);
const isSticky = useIsSticky(listRef, 5, props.stickyEnabled); const isSticky = useIsSticky(listRef, 5, props.stickyEnabled);
useScrollToActiveTab({ activeTabIndex: props.activeTabIndex, listRef, tabsRefs, isMobile, isLoading: props.isLoading }); useScrollToActiveTab({ activeTabIndex: props.activeTabIndex, listRef, tabsRefs, isMobile, isLoading: props.isLoading });
...@@ -80,6 +80,7 @@ const AdaptiveTabsList = (props: Props) => { ...@@ -80,6 +80,7 @@ const AdaptiveTabsList = (props: Props) => {
props.tabListProps) props.tabListProps)
} }
> >
{ props.leftSlot && <Box ref={ leftSlotRef } { ...props.leftSlotProps }> { props.leftSlot } </Box> }
{ tabsList.slice(0, props.isLoading ? 5 : Infinity).map((tab, index) => { { tabsList.slice(0, props.isLoading ? 5 : Infinity).map((tab, index) => {
if (!tab.id) { if (!tab.id) {
if (props.isLoading) { if (props.isLoading) {
......
...@@ -14,13 +14,27 @@ interface Props extends ThemingProps<'Tabs'> { ...@@ -14,13 +14,27 @@ interface Props extends ThemingProps<'Tabs'> {
tabListProps?: ChakraProps | (({ isSticky, activeTabIndex }: { isSticky: boolean; activeTabIndex: number }) => ChakraProps); tabListProps?: ChakraProps | (({ isSticky, activeTabIndex }: { isSticky: boolean; activeTabIndex: number }) => ChakraProps);
rightSlot?: React.ReactNode; rightSlot?: React.ReactNode;
rightSlotProps?: ChakraProps; rightSlotProps?: ChakraProps;
leftSlot?: React.ReactNode;
leftSlotProps?: ChakraProps;
stickyEnabled?: boolean; stickyEnabled?: boolean;
className?: string; className?: string;
onTabChange?: (index: number) => void; onTabChange?: (index: number) => void;
isLoading?: boolean; isLoading?: boolean;
} }
const RoutedTabs = ({ tabs, tabListProps, rightSlot, rightSlotProps, stickyEnabled, className, onTabChange, isLoading, ...themeProps }: Props) => { const RoutedTabs = ({
tabs,
tabListProps,
rightSlot,
rightSlotProps,
leftSlot,
leftSlotProps,
stickyEnabled,
className,
onTabChange,
isLoading,
...themeProps
}: Props) => {
const router = useRouter(); const router = useRouter();
const tabIndex = useTabIndexFromQuery(tabs); const tabIndex = useTabIndexFromQuery(tabs);
const tabsRef = useRef<HTMLDivElement>(null); const tabsRef = useRef<HTMLDivElement>(null);
...@@ -59,6 +73,8 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, rightSlotProps, stickyEnabl ...@@ -59,6 +73,8 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, rightSlotProps, stickyEnabl
<TabsWithScroll <TabsWithScroll
tabs={ tabs } tabs={ tabs }
tabListProps={ tabListProps } tabListProps={ tabListProps }
leftSlot={ leftSlot }
leftSlotProps={ leftSlotProps }
rightSlot={ rightSlot } rightSlot={ rightSlot }
rightSlotProps={ rightSlotProps } rightSlotProps={ rightSlotProps }
stickyEnabled={ stickyEnabled } stickyEnabled={ stickyEnabled }
......
...@@ -43,6 +43,8 @@ const TabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRef, act ...@@ -43,6 +43,8 @@ const TabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRef, act
<Popover isLazy placement="bottom-end" key="more" isOpen={ isOpen } onClose={ onClose } onOpen={ onOpen } closeDelay={ 0 }> <Popover isLazy placement="bottom-end" key="more" isOpen={ isOpen } onClose={ onClose } onOpen={ onOpen } closeDelay={ 0 }>
<PopoverTrigger> <PopoverTrigger>
<Button <Button
as="div"
role="button"
variant="ghost" variant="ghost"
isActive={ isOpen || isActive } isActive={ isOpen || isActive }
ref={ buttonRef } ref={ buttonRef }
......
...@@ -22,6 +22,8 @@ export interface Props extends ThemingProps<'Tabs'> { ...@@ -22,6 +22,8 @@ export interface Props extends ThemingProps<'Tabs'> {
tabListProps?: ChakraProps | (({ isSticky, activeTabIndex }: { isSticky: boolean; activeTabIndex: number }) => ChakraProps); tabListProps?: ChakraProps | (({ isSticky, activeTabIndex }: { isSticky: boolean; activeTabIndex: number }) => ChakraProps);
rightSlot?: React.ReactNode; rightSlot?: React.ReactNode;
rightSlotProps?: ChakraProps; rightSlotProps?: ChakraProps;
leftSlot?: React.ReactNode;
leftSlotProps?: ChakraProps;
stickyEnabled?: boolean; stickyEnabled?: boolean;
onTabChange?: (index: number) => void; onTabChange?: (index: number) => void;
defaultTabIndex?: number; defaultTabIndex?: number;
...@@ -35,6 +37,8 @@ const TabsWithScroll = ({ ...@@ -35,6 +37,8 @@ const TabsWithScroll = ({
tabListProps, tabListProps,
rightSlot, rightSlot,
rightSlotProps, rightSlotProps,
leftSlot,
leftSlotProps,
stickyEnabled, stickyEnabled,
onTabChange, onTabChange,
defaultTabIndex, defaultTabIndex,
...@@ -102,6 +106,8 @@ const TabsWithScroll = ({ ...@@ -102,6 +106,8 @@ const TabsWithScroll = ({
key={ isLoading + '_' + screenWidth + '_' + tabsList.map((tab) => tab.id).join(':') } key={ isLoading + '_' + screenWidth + '_' + tabsList.map((tab) => tab.id).join(':') }
tabs={ tabs } tabs={ tabs }
tabListProps={ tabListProps } tabListProps={ tabListProps }
leftSlot={ leftSlot }
leftSlotProps={ leftSlotProps }
rightSlot={ rightSlot } rightSlot={ rightSlot }
rightSlotProps={ rightSlotProps } rightSlotProps={ rightSlotProps }
stickyEnabled={ stickyEnabled } stickyEnabled={ stickyEnabled }
......
...@@ -9,10 +9,12 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab | MenuButton>, dis ...@@ -9,10 +9,12 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab | MenuButton>, dis
const [ tabsRefs, setTabsRefs ] = React.useState<Array<React.RefObject<HTMLButtonElement>>>([]); const [ tabsRefs, setTabsRefs ] = React.useState<Array<React.RefObject<HTMLButtonElement>>>([]);
const listRef = React.useRef<HTMLDivElement>(null); const listRef = React.useRef<HTMLDivElement>(null);
const rightSlotRef = React.useRef<HTMLDivElement>(null); const rightSlotRef = React.useRef<HTMLDivElement>(null);
const leftSlotRef = React.useRef<HTMLDivElement>(null);
const calculateCut = React.useCallback(() => { const calculateCut = React.useCallback(() => {
const listWidth = listRef.current?.getBoundingClientRect().width; const listWidth = listRef.current?.getBoundingClientRect().width;
const rightSlotWidth = rightSlotRef.current?.getBoundingClientRect().width || 0; const rightSlotWidth = rightSlotRef.current?.getBoundingClientRect().width || 0;
const leftSlotWidth = leftSlotRef.current?.getBoundingClientRect().width || 0;
const tabWidths = tabsRefs.map((tab) => tab.current?.getBoundingClientRect().width); const tabWidths = tabsRefs.map((tab) => tab.current?.getBoundingClientRect().width);
const menuWidth = tabWidths[tabWidths.length - 1]; const menuWidth = tabWidths[tabWidths.length - 1];
...@@ -33,11 +35,11 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab | MenuButton>, dis ...@@ -33,11 +35,11 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab | MenuButton>, dis
if (index === array.length - 1) { if (index === array.length - 1) {
// last element // last element
if (result.accWidth + item < listWidth - rightSlotWidth) { if (result.accWidth + item < listWidth - rightSlotWidth - leftSlotWidth) {
return { visibleNum: result.visibleNum + 1, accWidth: result.accWidth + item }; return { visibleNum: result.visibleNum + 1, accWidth: result.accWidth + item };
} }
} else { } else {
if (result.accWidth + item + menuWidth < listWidth - rightSlotWidth) { if (result.accWidth + item + menuWidth < listWidth - rightSlotWidth - leftSlotWidth) {
return { visibleNum: result.visibleNum + 1, accWidth: result.accWidth + item }; return { visibleNum: result.visibleNum + 1, accWidth: result.accWidth + item };
} }
} }
...@@ -67,6 +69,7 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab | MenuButton>, dis ...@@ -67,6 +69,7 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab | MenuButton>, dis
tabsRefs, tabsRefs,
listRef, listRef,
rightSlotRef, rightSlotRef,
leftSlotRef,
}; };
}, [ tabsCut, tabsRefs ]); }, [ tabsCut, tabsRefs ]);
} }
import {
Input,
FormControl,
} from '@chakra-ui/react';
import React from 'react';
import type { ControllerRenderProps, FieldError, FieldValues, Path } from 'react-hook-form';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
const TAG_MAX_LENGTH = 35;
type Props<TInputs extends FieldValues, TInputName extends Path<TInputs>> = {
field: ControllerRenderProps<TInputs, TInputName>;
error?: FieldError;
bgColor?: string;
}
function TagInput<Inputs extends FieldValues, Name extends Path<Inputs>>({ field, error, bgColor }: Props<Inputs, Name>) {
return (
<FormControl variant="floating" id="tag" isRequired bgColor={ bgColor }>
<Input
{ ...field }
isInvalid={ Boolean(error) }
maxLength={ TAG_MAX_LENGTH }
bgColor={ bgColor }
/>
<InputPlaceholder text="Private tag (max 35 characters)" error={ error }/>
</FormControl>
);
}
export default TagInput;
import {
Input,
FormControl,
} from '@chakra-ui/react';
import React from 'react';
import type { ControllerRenderProps, FieldError, FieldValues } from 'react-hook-form';
import { TRANSACTION_HASH_LENGTH } from 'lib/validations/transaction';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Props<Field> = {
field: Field;
error?: FieldError;
bgColor?: string;
}
function TransactionInput<Field extends Partial<ControllerRenderProps<FieldValues, 'transaction'>>>({ field, error, bgColor }: Props<Field>) {
return (
<FormControl variant="floating" id="transaction" isRequired bgColor={ bgColor }>
<Input
{ ...field }
isInvalid={ Boolean(error) }
maxLength={ TRANSACTION_HASH_LENGTH }
bgColor={ bgColor }
/>
<InputPlaceholder text="Transaction hash (0x...)" error={ error }/>
</FormControl>
);
}
export default TransactionInput;
import type { ColorMode } from '@chakra-ui/react';
import { Image, Skeleton, chakra, DarkMode } from '@chakra-ui/react';
import React from 'react';
interface Props {
src: string | undefined;
onLoad?: () => void;
onError?: () => void;
isInvalid: boolean;
className?: string;
fallback: React.ReactElement;
colorMode?: ColorMode;
}
const ImageUrlPreview = ({
src,
isInvalid,
onError,
onLoad,
className,
fallback: fallbackProp,
colorMode,
}: Props) => {
const skeleton = <Skeleton className={ className } w="100%" h="100%"/>;
const fallback = (() => {
if (src && !isInvalid) {
return colorMode === 'dark' ? <DarkMode>{ skeleton }</DarkMode> : skeleton;
}
return fallbackProp;
})();
return (
<Image
className={ className }
src={ src }
alt="Image preview"
w="auto"
h="100%"
fallback={ fallback }
onError={ onError }
onLoad={ onLoad }
/>
);
};
export default chakra(React.memo(ImageUrlPreview));
import type { ChakraProps } from '@chakra-ui/react';
import React from 'react';
import type { FieldValues, Path } from 'react-hook-form';
import type { FormFieldPropsBase } from './types';
import type { PartialBy } from 'types/utils';
import { addressValidator } from '../validators/address';
import FormFieldText from './FormFieldText';
const FormFieldAddress = <FormFields extends FieldValues>(
props: PartialBy<FormFieldPropsBase<FormFields>, 'placeholder'>,
) => {
const rules = React.useMemo(
() => ({
...props.rules,
validate: {
...props.rules?.validate,
address: addressValidator,
},
}),
[ props.rules ],
);
return (
<FormFieldText
{ ...props }
placeholder={ props.placeholder || 'Address (0x...)' }
rules={ rules }
/>
);
};
export type WrappedComponent = <
FormFields extends FieldValues,
Name extends Path<FormFields> = Path<FormFields>,
>(props: PartialBy<FormFieldPropsBase<FormFields, Name>, 'placeholder'> & ChakraProps) => JSX.Element;
export default React.memo(FormFieldAddress) as WrappedComponent;
import type { ChakraProps } from '@chakra-ui/react';
import { chakra, Checkbox } from '@chakra-ui/react';
import React from 'react';
import { useController, useFormContext, type FieldValues, type Path } from 'react-hook-form';
import type { FormFieldPropsBase } from './types';
interface Props<
FormFields extends FieldValues,
Name extends Path<FormFields> = Path<FormFields>,
> extends Omit<FormFieldPropsBase<FormFields, Name>, 'size' | 'bgColor' | 'placeholder'> {
label: string;
}
const FormFieldCheckbox = <
FormFields extends FieldValues,
Name extends Path<FormFields> = Path<FormFields>,
>({
name,
label,
rules,
onChange,
isReadOnly,
className,
}: Props<FormFields, Name>) => {
const { control } = useFormContext<FormFields>();
const { field, formState } = useController<FormFields, typeof name>({
control,
name,
rules,
});
const isDisabled = formState.isSubmitting;
const handleChange: typeof field.onChange = React.useCallback((...args) => {
field.onChange(...args);
onChange?.();
}, [ field, onChange ]);
return (
<Checkbox
ref={ field.ref }
isChecked={ field.value }
className={ className }
onChange={ handleChange }
colorScheme="blue"
size="lg"
isDisabled={ isDisabled }
isReadOnly={ isReadOnly }
>
{ label }
</Checkbox>
);
};
const WrappedFormFieldCheckbox = chakra(FormFieldCheckbox);
export type WrappedComponent = <
FormFields extends FieldValues,
Name extends Path<FormFields> = Path<FormFields>,
>(props: Props<FormFields, Name> & ChakraProps) => JSX.Element;
export default React.memo(WrappedFormFieldCheckbox) as WrappedComponent;
import type { ChakraProps } from '@chakra-ui/react';
import React from 'react';
import type { FieldValues, Path } from 'react-hook-form';
import type { FormFieldPropsBase } from './types';
import type { PartialBy } from 'types/utils';
import { EMAIL_REGEXP } from '../validators/email';
import FormFieldText from './FormFieldText';
const FormFieldEmail = <FormFields extends FieldValues>(
props: PartialBy<FormFieldPropsBase<FormFields>, 'placeholder'>,
) => {
const rules = React.useMemo(
() => ({
...props.rules,
pattern: EMAIL_REGEXP,
}),
[ props.rules ],
);
return (
<FormFieldText
{ ...props }
placeholder={ props.placeholder || 'Email' }
rules={ rules }
/>
);
};
export type WrappedComponent = <
FormFields extends FieldValues,
Name extends Path<FormFields> = Path<FormFields>,
>(props: PartialBy<FormFieldPropsBase<FormFields, Name>, 'placeholder'> & ChakraProps) => JSX.Element;
export default React.memo(FormFieldEmail) as WrappedComponent;
import React from 'react';
import type { Path, FieldValues } from 'react-hook-form';
import { useController, useFormContext } from 'react-hook-form';
import type { FormFieldPropsBase } from './types';
// import type { Option } from 'ui/shared/forms/inputs/select/types';
import useIsMobile from 'lib/hooks/useIsMobile';
import type { Props as FancySelectProps } from 'ui/shared/forms/inputs/select/FancySelect';
import FancySelect from 'ui/shared/forms/inputs/select/FancySelect';
// FIXME: Try to get this to work to add more constraints to the props type
// this type only works for plain objects, not for nested objects or arrays (e.g. ui/publicTags/submit/types.ts:FormFields)
// type SelectField<O> = { [K in keyof O]: NonNullable<O[K]> extends Option ? K : never }[keyof O];
type Props<
FormFields extends FieldValues,
Name extends Path<FormFields>,
> = Omit<FormFieldPropsBase<FormFields, Name>, 'bgColor' | 'size'> & Partial<FancySelectProps> & {
size?: 'md' | 'lg';
}
const FormFieldFancySelect = <
FormFields extends FieldValues,
Name extends Path<FormFields>,
>(props: Props<FormFields, Name>) => {
const isMobile = useIsMobile();
const defaultSize = isMobile ? 'md' : 'lg';
const { control } = useFormContext<FormFields>();
const { field, fieldState, formState } = useController<FormFields, typeof props.name>({
control,
name: props.name,
rules: { ...props.rules, required: props.isRequired },
});
const isDisabled = formState.isSubmitting;
return (
<FancySelect
{ ...field }
{ ...props }
size={ props.size || defaultSize }
error={ fieldState.error }
isDisabled={ isDisabled }
/>
);
};
export default React.memo(FormFieldFancySelect) as typeof FormFieldFancySelect;
import type { ChakraProps } from '@chakra-ui/react';
import { FormControl, Input, InputGroup, InputRightElement, Textarea, chakra, shouldForwardProp } from '@chakra-ui/react';
import React from 'react';
import type { FieldValues, Path } from 'react-hook-form';
import { useController, useFormContext } from 'react-hook-form';
import type { FormFieldPropsBase } from './types';
import FormInputPlaceholder from '../inputs/FormInputPlaceholder';
interface Props<
FormFields extends FieldValues,
Name extends Path<FormFields> = Path<FormFields>,
> extends FormFieldPropsBase<FormFields, Name> {
asComponent?: 'Input' | 'Textarea';
}
const FormFieldText = <
FormFields extends FieldValues,
Name extends Path<FormFields> = Path<FormFields>,
>({
name,
placeholder,
isReadOnly,
isRequired,
rules,
onBlur,
type = 'text',
rightElement,
asComponent,
max,
className,
size = 'md',
bgColor,
minH,
maxH,
}: Props<FormFields, Name>) => {
const { control } = useFormContext<FormFields>();
const { field, fieldState, formState } = useController<FormFields, typeof name>({
control,
name,
rules: { ...rules, required: isRequired },
});
const isDisabled = formState.isSubmitting;
const handleBlur = React.useCallback(() => {
field.onBlur();
onBlur?.();
}, [ field, onBlur ]);
const Component = asComponent === 'Textarea' ? Textarea : Input;
const input = (
<Component
{ ...field }
onBlur={ handleBlur }
isInvalid={ Boolean(fieldState.error) }
isDisabled={ isDisabled }
isReadOnly={ isReadOnly }
autoComplete="off"
type={ type }
placeholder=" "
max={ max }
size={ size }
bgColor={ bgColor }
minH={ minH }
maxH={ maxH }
/>
);
const inputPlaceholder = size !== 'xs' && <FormInputPlaceholder text={ placeholder } error={ fieldState.error }/>;
return (
<FormControl
className={ className }
variant="floating"
isDisabled={ isDisabled }
isRequired={ isRequired }
size={ size }
bgColor={ bgColor }
>
{ rightElement ? (
<InputGroup>
{ input }
{ inputPlaceholder }
<InputRightElement h="100%"> { rightElement({ field }) } </InputRightElement>
</InputGroup>
) : (
<>
{ input }
{ inputPlaceholder }
</>
) }
</FormControl>
);
};
const WrappedFormFieldText = chakra(FormFieldText, {
shouldForwardProp: (prop) => {
const isChakraProp = !shouldForwardProp(prop);
if (isChakraProp && ![ 'bgColor', 'size', 'minH', 'maxH' ].includes(prop)) {
return false;
}
return true;
},
});
export type WrappedComponent = <
FormFields extends FieldValues,
Name extends Path<FormFields> = Path<FormFields>,
>(props: Props<FormFields, Name> & ChakraProps) => JSX.Element;
export default React.memo(WrappedFormFieldText) as WrappedComponent;
import React from 'react';
import type { FieldValues } from 'react-hook-form';
import type { FormFieldPropsBase } from './types';
import { urlValidator } from '../validators/url';
import FormFieldText, { type WrappedComponent } from './FormFieldText';
const FormFieldUrl = <FormFields extends FieldValues>(
props: FormFieldPropsBase<FormFields>,
) => {
const rules = React.useMemo(
() => ({
...props.rules,
validate: {
...props.rules?.validate,
url: urlValidator,
},
}),
[ props.rules ],
);
return <FormFieldText { ...props } rules={ rules }/>;
};
export default React.memo(FormFieldUrl) as WrappedComponent;
import type { FormControlProps } from '@chakra-ui/react';
import type { ControllerRenderProps, FieldValues, Path, RegisterOptions } from 'react-hook-form';
export interface FormFieldPropsBase<
FormFields extends FieldValues,
Name extends Path<FormFields> = Path<FormFields>,
> {
name: Name;
placeholder: string;
isReadOnly?: boolean;
isRequired?: boolean;
rules?: Omit<RegisterOptions<FormFields, Name>, 'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled'>;
onBlur?: () => void;
onChange?: () => void;
type?: HTMLInputElement['type'];
rightElement?: ({ field }: { field: ControllerRenderProps<FormFields, Name> }) => React.ReactNode;
max?: HTMLInputElement['max'];
// styles
size?: FormControlProps['size'];
bgColor?: FormControlProps['bgColor'];
maxH?: FormControlProps['maxH'];
minH?: FormControlProps['minH'];
className?: string;
}
...@@ -9,7 +9,7 @@ interface Props { ...@@ -9,7 +9,7 @@ interface Props {
isFancy?: boolean; isFancy?: boolean;
} }
const InputPlaceholder = ({ text, icon, error, isFancy }: Props) => { const FormInputPlaceholder = ({ text, icon, error, isFancy }: Props) => {
let errorMessage = error?.message; let errorMessage = error?.message;
if (!errorMessage && error?.type === 'pattern') { if (!errorMessage && error?.type === 'pattern') {
...@@ -21,13 +21,17 @@ const InputPlaceholder = ({ text, icon, error, isFancy }: Props) => { ...@@ -21,13 +21,17 @@ const InputPlaceholder = ({ text, icon, error, isFancy }: Props) => {
alignItems="center" alignItems="center"
{ ...(isFancy ? { 'data-fancy': true } : {}) } { ...(isFancy ? { 'data-fancy': true } : {}) }
variant="floating" variant="floating"
bgColor="deeppink"
> >
{ icon } { icon }
<chakra.span>{ text }</chakra.span> <chakra.span>{ text }</chakra.span>
{ errorMessage && <chakra.span order={ 3 } whiteSpace="pre"> - { errorMessage }</chakra.span> } { errorMessage && (
<chakra.span order={ 3 } whiteSpace="pre">
{ ' ' }
- { errorMessage }
</chakra.span>
) }
</FormLabel> </FormLabel>
); );
}; };
export default React.memo(InputPlaceholder); export default React.memo(FormInputPlaceholder);
...@@ -2,7 +2,7 @@ import { chakra, Center, useColorModeValue } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { chakra, Center, useColorModeValue } from '@chakra-ui/react';
import type { DragEvent } from 'react'; import type { DragEvent } from 'react';
import React from 'react'; import React from 'react';
import { getAllFileEntries, convertFileEntryToFile } from './utils/files'; import { getAllFileEntries, convertFileEntryToFile } from './utils';
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
......
...@@ -6,8 +6,8 @@ import type { FieldError, FieldErrorsImpl, Merge } from 'react-hook-form'; ...@@ -6,8 +6,8 @@ import type { FieldError, FieldErrorsImpl, Merge } from 'react-hook-form';
import type { Option } from './types'; import type { Option } from './types';
import { getChakraStyles } from 'ui/shared/FancySelect/utils'; import FormInputPlaceholder from 'ui/shared/forms/inputs/FormInputPlaceholder';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import { getChakraStyles } from 'ui/shared/forms/inputs/select/utils';
interface CommonProps { interface CommonProps {
error?: Merge<FieldError, FieldErrorsImpl<Option>> | undefined; error?: Merge<FieldError, FieldErrorsImpl<Option>> | undefined;
...@@ -24,7 +24,7 @@ interface AsyncSelectProps extends AsyncProps<Option, boolean, GroupBase<Option> ...@@ -24,7 +24,7 @@ interface AsyncSelectProps extends AsyncProps<Option, boolean, GroupBase<Option>
onChange: (newValue: SingleValue<Option> | MultiValue<Option>) => void; onChange: (newValue: SingleValue<Option> | MultiValue<Option>) => void;
} }
type Props = RegularSelectProps | AsyncSelectProps; export type Props = RegularSelectProps | AsyncSelectProps;
const FancySelect = (props: Props, ref: React.LegacyRef<HTMLDivElement>) => { const FancySelect = (props: Props, ref: React.LegacyRef<HTMLDivElement>) => {
const menuZIndex = useToken('zIndices', 'dropdown'); const menuZIndex = useToken('zIndices', 'dropdown');
...@@ -58,7 +58,7 @@ const FancySelect = (props: Props, ref: React.LegacyRef<HTMLDivElement>) => { ...@@ -58,7 +58,7 @@ const FancySelect = (props: Props, ref: React.LegacyRef<HTMLDivElement>) => {
isInvalid={ Boolean(props.error) } isInvalid={ Boolean(props.error) }
useBasicStyles useBasicStyles
/> />
<InputPlaceholder <FormInputPlaceholder
text={ typeof props.placeholder === 'string' ? props.placeholder : '' } text={ typeof props.placeholder === 'string' ? props.placeholder : '' }
icon={ props.placeholderIcon } icon={ props.placeholderIcon }
error={ props.error } error={ props.error }
......
export interface Option<T extends string = string> {
label: string;
value: T;
}
import React from 'react';
import type { FieldValues, Path } from 'react-hook-form';
import { useFormContext, useWatch } from 'react-hook-form';
import { urlValidator } from '../validators/url';
interface Params<
FormFields extends FieldValues,
Name extends Path<FormFields>
> {
name: Name;
isRequired?: boolean;
}
interface ReturnType {
input: {
rules: {
required?: boolean;
validate: {
preview: () => string | true;
};
};
isRequired?: boolean;
onBlur: () => void;
};
preview: {
src: string | undefined;
isInvalid: boolean;
onLoad: () => void;
onError: () => void;
};
}
export default function useFieldWithImagePreview<
FormFields extends FieldValues,
Name extends Path<FormFields>
>({
name,
isRequired,
}: Params<FormFields, Name>): ReturnType {
const { trigger, formState, control } = useFormContext<FormFields>();
const imageLoadError = React.useRef(false);
const fieldValue = useWatch({ name, control, exact: true });
const fieldError = formState.errors[name];
const [ value, setValue ] = React.useState<string | undefined>(fieldValue);
const validator = React.useCallback(() => {
return imageLoadError.current ? 'Unable to load image' : true;
}, []);
const onLoad = React.useCallback(() => {
imageLoadError.current = false;
trigger(name);
}, [ name, trigger ]);
const onError = React.useCallback(() => {
imageLoadError.current = true;
trigger(name);
}, [ name, trigger ]);
const onBlur = React.useCallback(() => {
if (!isRequired && !fieldValue) {
imageLoadError.current = false;
trigger(name);
setValue(undefined);
return;
}
const isValidUrl = urlValidator(fieldValue);
isValidUrl === true && setValue(fieldValue);
}, [ fieldValue, isRequired, name, trigger ]);
return React.useMemo(() => {
return {
input: {
isRequired,
rules: {
required: isRequired,
validate: {
preview: validator,
},
},
onBlur,
},
preview: {
src: fieldError?.type === 'url' ? undefined : value,
isInvalid: fieldError?.type === 'preview',
onLoad,
onError,
},
};
}, [ fieldError?.type, isRequired, onBlur, onError, onLoad, validator, value ]);
}
// maybe it depends on the network??
export const ADDRESS_REGEXP = /^0x[a-fA-F\d]{40}$/; export const ADDRESS_REGEXP = /^0x[a-fA-F\d]{40}$/;
export const ADDRESS_LENGTH = 42; export const ADDRESS_LENGTH = 42;
export function addressValidator(value: string | undefined) {
if (!value) {
return true;
}
return ADDRESS_REGEXP.test(value) ? true : 'Incorrect address format';
}
export function urlValidator(value: string | undefined) {
if (!value) {
return true;
}
try {
new URL(value);
return true;
} catch (error) {
return 'Incorrect URL';
}
}
export const DOMAIN_REGEXP =
/(?:[a-z\d](?:[a-z\d-]{0,61}[a-z\d])?\.)+[a-z\d][a-z\d-]{0,61}[a-z\d]/gi;
export function domainValidator(value: string | undefined) {
if (!value) {
return true;
}
const domain = (() => {
try {
const url = new URL(`https://${ value }`);
return url.hostname;
} catch (error) {
return;
}
})();
return domain === value.toLowerCase() || 'Incorrect domain';
}
import { ButtonGroup, Button, Flex, useRadio, useRadioGroup, useColorModeValue } from '@chakra-ui/react'; import { ButtonGroup, Button, Flex, useRadio, useRadioGroup } from '@chakra-ui/react';
import type { UseRadioProps } from '@chakra-ui/react'; import type { UseRadioProps } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
...@@ -20,35 +20,17 @@ type RadioButtonProps = UseRadioProps & RadioItemProps; ...@@ -20,35 +20,17 @@ type RadioButtonProps = UseRadioProps & RadioItemProps;
const RadioButton = (props: RadioButtonProps) => { const RadioButton = (props: RadioButtonProps) => {
const { getInputProps, getRadioProps } = useRadio(props); const { getInputProps, getRadioProps } = useRadio(props);
const buttonColor = useColorModeValue('blue.50', 'gray.800');
const checkedTextColor = useColorModeValue('blue.700', 'gray.50');
const input = getInputProps(); const input = getInputProps();
const checkbox = getRadioProps(); const checkbox = getRadioProps();
const styleProps = {
flex: 1,
variant: 'outline',
fontWeight: 500,
cursor: props.isChecked ? 'initial' : 'pointer',
borderColor: buttonColor,
backgroundColor: props.isChecked ? buttonColor : 'none',
_hover: {
borderColor: buttonColor,
...(props.isChecked ? {} : { color: 'link_hovered' }),
},
_active: {
backgroundColor: 'none',
},
...(props.isChecked ? { color: checkedTextColor } : {}),
};
if (props.onlyIcon) { if (props.onlyIcon) {
return ( return (
<Button <Button
as="label" as="label"
aria-label={ props.title } aria-label={ props.title }
{ ...styleProps } variant="radio_group"
data-selected={ props.isChecked }
> >
<input { ...input }/> <input { ...input }/>
<Flex <Flex
...@@ -64,7 +46,8 @@ const RadioButton = (props: RadioButtonProps) => { ...@@ -64,7 +46,8 @@ const RadioButton = (props: RadioButtonProps) => {
<Button <Button
as="label" as="label"
leftIcon={ props.icon ? <IconSvg name={ props.icon } boxSize={ 5 } mr={ -1 }/> : undefined } leftIcon={ props.icon ? <IconSvg name={ props.icon } boxSize={ 5 } mr={ -1 }/> : undefined }
{ ...styleProps } variant="radio_group"
data-selected={ props.isChecked }
> >
<input { ...input }/> <input { ...input }/>
<Flex <Flex
......
...@@ -98,7 +98,7 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig }: Props) => { ...@@ -98,7 +98,7 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig }: Props) => {
const currentStep = steps[steps.length - 1]; const currentStep = steps[steps.length - 1];
switch (currentStep.type) { switch (currentStep.type) {
case 'select_method': case 'select_method':
return 'Select a way to connect'; return 'Select a way to login';
case 'connect_wallet': case 'connect_wallet':
return currentStep.isAuth ? 'Add wallet' : 'Continue with wallet'; return currentStep.isAuth ? 'Add wallet' : 'Continue with wallet';
case 'email': case 'email':
......
...@@ -11,8 +11,7 @@ import getErrorMessage from 'lib/errors/getErrorMessage'; ...@@ -11,8 +11,7 @@ import getErrorMessage from 'lib/errors/getErrorMessage';
import getErrorObjPayload from 'lib/errors/getErrorObjPayload'; import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
import * as mixpanel from 'lib/mixpanel'; import * as mixpanel from 'lib/mixpanel';
import FormFieldEmail from 'ui/shared/forms/fields/FormFieldEmail';
import AuthModalFieldEmail from '../fields/AuthModalFieldEmail';
interface Props { interface Props {
onSubmit: (screen: Screen) => void; onSubmit: (screen: Screen) => void;
...@@ -78,7 +77,13 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => { ...@@ -78,7 +77,13 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => {
onSubmit={ formApi.handleSubmit(onFormSubmit) } onSubmit={ formApi.handleSubmit(onFormSubmit) }
> >
<Text>Account email, used for transaction notifications from your watchlist.</Text> <Text>Account email, used for transaction notifications from your watchlist.</Text>
<AuthModalFieldEmail mt={ 6 }/> <FormFieldEmail<EmailFormFields>
name="email"
isRequired
placeholder="Email"
bgColor="dialog_bg"
mt={ 6 }
/>
<Button <Button
mt={ 6 } mt={ 6 }
type="submit" type="submit"
......
...@@ -29,7 +29,7 @@ const AuthModalScreenSelectMethod = ({ onSelectMethod }: Props) => { ...@@ -29,7 +29,7 @@ const AuthModalScreenSelectMethod = ({ onSelectMethod }: Props) => {
return ( return (
<VStack spacing={ 3 } mt={ 4 } align="stretch"> <VStack spacing={ 3 } mt={ 4 } align="stretch">
<Button variant="outline" onClick={ handleConnectWalletClick }>Connect Web3 wallet</Button> <Button variant="outline" onClick={ handleConnectWalletClick }>Continue with Web3 wallet</Button>
<Button variant="outline" onClick={ handleEmailClick }>Continue with email</Button> <Button variant="outline" onClick={ handleEmailClick }>Continue with email</Button>
</VStack> </VStack>
); );
......
...@@ -5,9 +5,9 @@ import type { SearchResultAddressOrContract } from 'types/api/search'; ...@@ -5,9 +5,9 @@ import type { SearchResultAddressOrContract } from 'types/api/search';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import highlightText from 'lib/highlightText'; import highlightText from 'lib/highlightText';
import { ADDRESS_REGEXP } from 'lib/validations/address';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel'; import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
import * as AddressEntity from 'ui/shared/entities/address/AddressEntity'; import * as AddressEntity from 'ui/shared/entities/address/AddressEntity';
import { ADDRESS_REGEXP } from 'ui/shared/forms/validators/address';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props { interface Props {
......
import { Button, Grid, GridItem } from '@chakra-ui/react'; import { Button, Grid, GridItem, Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import type { Fields } from './types'; import type { Fields } from './types';
import type { TokenInfoApplication } from 'types/api/account'; import type { TokenInfoApplication } from 'types/api/account';
...@@ -15,22 +15,15 @@ import useUpdateEffect from 'lib/hooks/useUpdateEffect'; ...@@ -15,22 +15,15 @@ import useUpdateEffect from 'lib/hooks/useUpdateEffect';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import FormFieldAddress from 'ui/shared/forms/fields/FormFieldAddress';
import FormFieldEmail from 'ui/shared/forms/fields/FormFieldEmail';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import FormFieldUrl from 'ui/shared/forms/fields/FormFieldUrl';
import TokenInfoFieldAddress from './fields/TokenInfoFieldAddress';
import TokenInfoFieldComment from './fields/TokenInfoFieldComment';
import TokenInfoFieldDocs from './fields/TokenInfoFieldDocs';
import TokenInfoFieldIconUrl from './fields/TokenInfoFieldIconUrl'; import TokenInfoFieldIconUrl from './fields/TokenInfoFieldIconUrl';
import TokenInfoFieldPriceTicker from './fields/TokenInfoFieldPriceTicker';
import TokenInfoFieldProjectDescription from './fields/TokenInfoFieldProjectDescription';
import TokenInfoFieldProjectEmail from './fields/TokenInfoFieldProjectEmail';
import TokenInfoFieldProjectName from './fields/TokenInfoFieldProjectName';
import TokenInfoFieldProjectSector from './fields/TokenInfoFieldProjectSector'; import TokenInfoFieldProjectSector from './fields/TokenInfoFieldProjectSector';
import TokenInfoFieldProjectWebsite from './fields/TokenInfoFieldProjectWebsite';
import TokenInfoFieldRequesterEmail from './fields/TokenInfoFieldRequesterEmail';
import TokenInfoFieldRequesterName from './fields/TokenInfoFieldRequesterName';
import TokenInfoFieldSocialLink from './fields/TokenInfoFieldSocialLink'; import TokenInfoFieldSocialLink from './fields/TokenInfoFieldSocialLink';
import TokenInfoFieldSupport from './fields/TokenInfoFieldSupport'; import TokenInfoFieldSupport from './fields/TokenInfoFieldSupport';
import TokenInfoFieldTokenName from './fields/TokenInfoFieldTokenName';
import TokenInfoFormSectionHeader from './TokenInfoFormSectionHeader'; import TokenInfoFormSectionHeader from './TokenInfoFormSectionHeader';
import TokenInfoFormStatusText from './TokenInfoFormStatusText'; import TokenInfoFormStatusText from './TokenInfoFormStatusText';
import { getFormDefaultValues, prepareRequestBody } from './utils'; import { getFormDefaultValues, prepareRequestBody } from './utils';
...@@ -58,7 +51,7 @@ const TokenInfoForm = ({ address, tokenName, application, onSubmit }: Props) => ...@@ -58,7 +51,7 @@ const TokenInfoForm = ({ address, tokenName, application, onSubmit }: Props) =>
mode: 'onBlur', mode: 'onBlur',
defaultValues: getFormDefaultValues(address, tokenName, application), defaultValues: getFormDefaultValues(address, tokenName, application),
}); });
const { handleSubmit, formState, control, trigger } = formApi; const { handleSubmit, formState } = formApi;
React.useEffect(() => { React.useEffect(() => {
if (!application?.id && !openEventSent.current) { if (!application?.id && !openEventSent.current) {
...@@ -116,66 +109,90 @@ const TokenInfoForm = ({ address, tokenName, application, onSubmit }: Props) => ...@@ -116,66 +109,90 @@ const TokenInfoForm = ({ address, tokenName, application, onSubmit }: Props) =>
return <ContentLoader/>; return <ContentLoader/>;
} }
const fieldProps = { control, isReadOnly: application?.status === 'IN_PROCESS' }; const fieldProps = {
size: { base: 'md', lg: 'lg' },
isReadOnly: application?.status === 'IN_PROCESS',
};
return ( return (
<form noValidate onSubmit={ handleSubmit(onFormSubmit) } autoComplete="off" ref={ containerRef }> <FormProvider { ...formApi }>
<TokenInfoFormStatusText application={ application }/> <form noValidate onSubmit={ handleSubmit(onFormSubmit) } autoComplete="off" ref={ containerRef }>
<Grid mt={ 8 } gridTemplateColumns={{ base: '1fr', lg: '1fr 1fr' }} columnGap={ 5 } rowGap={ 5 }> <TokenInfoFormStatusText application={ application }/>
<Grid mt={ 8 } gridTemplateColumns={{ base: '1fr', lg: '1fr 1fr' }} columnGap={ 5 } rowGap={ 5 }>
<TokenInfoFieldTokenName { ...fieldProps }/>
<TokenInfoFieldAddress { ...fieldProps }/> <FormFieldText<Fields> name="token_name" isRequired placeholder="Token name" { ...fieldProps } isReadOnly/>
<TokenInfoFieldRequesterName { ...fieldProps }/> <FormFieldAddress<Fields> name="address" isRequired placeholder="Token contract address" { ...fieldProps } isReadOnly/>
<TokenInfoFieldRequesterEmail { ...fieldProps }/> <FormFieldText<Fields> name="requester_name" isRequired placeholder="Requester name" { ...fieldProps }/>
<FormFieldEmail<Fields> name="requester_email" isRequired placeholder="Requester email" { ...fieldProps }/>
<TokenInfoFormSectionHeader>Project info</TokenInfoFormSectionHeader>
<TokenInfoFieldProjectName { ...fieldProps }/> <TokenInfoFormSectionHeader>Project info</TokenInfoFormSectionHeader>
<TokenInfoFieldProjectSector { ...fieldProps } config={ configQuery.data.projectSectors }/> <FormFieldText<Fields> name="project_name" placeholder="Project name" { ...fieldProps }/>
<TokenInfoFieldProjectEmail { ...fieldProps }/> <TokenInfoFieldProjectSector { ...fieldProps } config={ configQuery.data.projectSectors }/>
<TokenInfoFieldProjectWebsite { ...fieldProps }/> <FormFieldEmail<Fields> name="project_email" isRequired placeholder="Official project email address" { ...fieldProps }/>
<TokenInfoFieldDocs { ...fieldProps }/> <FormFieldUrl<Fields> name="project_website" isRequired placeholder="Official project website" { ...fieldProps }/>
<TokenInfoFieldSupport { ...fieldProps }/> <FormFieldUrl<Fields> name="docs" placeholder="Docs" { ...fieldProps }/>
<GridItem colSpan={{ base: 1, lg: 2 }}> <TokenInfoFieldSupport { ...fieldProps }/>
<TokenInfoFieldIconUrl { ...fieldProps } trigger={ trigger }/> <GridItem colSpan={{ base: 1, lg: 2 }}>
</GridItem> <TokenInfoFieldIconUrl { ...fieldProps }/>
<GridItem colSpan={{ base: 1, lg: 2 }}> </GridItem>
<TokenInfoFieldProjectDescription { ...fieldProps }/> <GridItem colSpan={{ base: 1, lg: 2 }}>
</GridItem> <FormFieldText<Fields>
name="project_description"
<TokenInfoFormSectionHeader>Links</TokenInfoFormSectionHeader> isRequired
<TokenInfoFieldSocialLink { ...fieldProps } name="github"/> placeholder="Project description"
<TokenInfoFieldSocialLink { ...fieldProps } name="twitter"/> maxH="160px"
<TokenInfoFieldSocialLink { ...fieldProps } name="telegram"/> rules={{ maxLength: 300 }}
<TokenInfoFieldSocialLink { ...fieldProps } name="opensea"/> asComponent="Textarea"
<TokenInfoFieldSocialLink { ...fieldProps } name="linkedin"/> { ...fieldProps }
<TokenInfoFieldSocialLink { ...fieldProps } name="facebook"/> />
<TokenInfoFieldSocialLink { ...fieldProps } name="discord"/> <Text variant="secondary" fontSize="sm" mt={ 1 }>
<TokenInfoFieldSocialLink { ...fieldProps } name="medium"/> Introduce or summarize the project’s operation/goals in a maximum of 300 characters.
<TokenInfoFieldSocialLink { ...fieldProps } name="slack"/> The description should be written in a neutral point of view and must exclude unsubstantiated claims unless proven otherwise.
<TokenInfoFieldSocialLink { ...fieldProps } name="reddit"/> </Text>
</GridItem>
<TokenInfoFormSectionHeader>Price data</TokenInfoFormSectionHeader>
<TokenInfoFieldPriceTicker { ...fieldProps } name="ticker_coin_market_cap" label="CoinMarketCap URL"/> <TokenInfoFormSectionHeader>Links</TokenInfoFormSectionHeader>
<TokenInfoFieldPriceTicker { ...fieldProps } name="ticker_coin_gecko" label="CoinGecko URL"/> <TokenInfoFieldSocialLink { ...fieldProps } name="github"/>
<GridItem colSpan={{ base: 1, lg: 2 }}> <TokenInfoFieldSocialLink { ...fieldProps } name="twitter"/>
<TokenInfoFieldPriceTicker { ...fieldProps } name="ticker_defi_llama" label="DefiLlama URL "/> <TokenInfoFieldSocialLink { ...fieldProps } name="telegram"/>
</GridItem> <TokenInfoFieldSocialLink { ...fieldProps } name="opensea"/>
<TokenInfoFieldSocialLink { ...fieldProps } name="linkedin"/>
<GridItem colSpan={{ base: 1, lg: 2 }}> <TokenInfoFieldSocialLink { ...fieldProps } name="facebook"/>
<TokenInfoFieldComment { ...fieldProps }/> <TokenInfoFieldSocialLink { ...fieldProps } name="discord"/>
</GridItem> <TokenInfoFieldSocialLink { ...fieldProps } name="medium"/>
</Grid> <TokenInfoFieldSocialLink { ...fieldProps } name="slack"/>
<Button <TokenInfoFieldSocialLink { ...fieldProps } name="reddit"/>
type="submit"
size="lg" <TokenInfoFormSectionHeader>Price data</TokenInfoFormSectionHeader>
mt={ 8 } <FormFieldUrl<Fields> name="ticker_coin_market_cap" placeholder="CoinMarketCap URL" { ...fieldProps }/>
isLoading={ formState.isSubmitting } <FormFieldUrl<Fields> name="ticker_coin_gecko" placeholder="CoinGecko URL" { ...fieldProps }/>
loadingText="Send request" <GridItem colSpan={{ base: 1, lg: 2 }}>
isDisabled={ application?.status === 'IN_PROCESS' } <FormFieldUrl<Fields> name="ticker_defi_llama" placeholder="DefiLlama URL" { ...fieldProps }/>
> </GridItem>
Send request
</Button> <GridItem colSpan={{ base: 1, lg: 2 }}>
</form> <FormFieldText<Fields>
name="comment"
placeholder="Comment"
maxH="160px"
rules={{ maxLength: 300 }}
asComponent="Textarea"
{ ...fieldProps }
/>
</GridItem>
</Grid>
<Button
type="submit"
size="lg"
mt={ 8 }
isLoading={ formState.isSubmitting }
loadingText="Send request"
isDisabled={ application?.status === 'IN_PROCESS' }
>
Send request
</Button>
</form>
</FormProvider>
); );
}; };
......
import { Center, Image, Skeleton, useColorModeValue } from '@chakra-ui/react'; import { Center, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import TokenLogoPlaceholder from 'ui/shared/TokenLogoPlaceholder';
interface Props { interface Props {
url: string | undefined; url: string | undefined;
onLoad?: () => void;
onError?: () => void;
isInvalid: boolean; isInvalid: boolean;
children: React.ReactElement;
} }
const TokenInfoIconPreview = ({ url, onError, onLoad, isInvalid }: Props) => { const TokenInfoIconPreview = ({ url, isInvalid, children }: Props) => {
const borderColor = useColorModeValue('gray.100', 'gray.700'); const borderColor = useColorModeValue('gray.100', 'gray.700');
const borderColorFilled = useColorModeValue('gray.300', 'gray.600'); const borderColorFilled = useColorModeValue('gray.300', 'gray.600');
const borderColorError = useColorModeValue('red.400', 'red.300'); const borderColorActive = isInvalid ? 'error' : borderColorFilled;
const borderColorActive = isInvalid ? borderColorError : borderColorFilled;
return ( return (
<Center <Center
...@@ -24,16 +20,7 @@ const TokenInfoIconPreview = ({ url, onError, onLoad, isInvalid }: Props) => { ...@@ -24,16 +20,7 @@ const TokenInfoIconPreview = ({ url, onError, onLoad, isInvalid }: Props) => {
borderColor={ url ? borderColorActive : borderColor } borderColor={ url ? borderColorActive : borderColor }
borderRadius="base" borderRadius="base"
> >
<Image { children }
borderRadius="base"
src={ url }
alt="Token logo preview"
boxSize={{ base: 10, lg: 12 }}
objectFit="cover"
fallback={ url && !isInvalid ? <Skeleton boxSize={{ base: 10, lg: 12 }}/> : <TokenLogoPlaceholder boxSize={{ base: 10, lg: 12 }}/> }
onError={ onError }
onLoad={ onLoad }
/>
</Center> </Center>
); );
}; };
......
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerRenderProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { Fields } from '../types';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props {
control: Control<Fields>;
}
const TokenInfoFieldAddress = ({ control }: Props) => {
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'address'>}) => {
return (
<FormControl variant="floating" id={ field.name } isRequired size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...field }
required
isReadOnly
/>
<InputPlaceholder text="Token contract address"/>
</FormControl>
);
}, []);
return (
<Controller
name="address"
control={ control }
render={ renderControl }
/>
);
};
export default React.memo(TokenInfoFieldAddress);
import { FormControl, Textarea } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { Fields } from '../types';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props {
control: Control<Fields>;
isReadOnly?: boolean;
}
const TokenInfoFieldComment = ({ control, isReadOnly }: Props) => {
const renderControl: ControllerProps<Fields, 'comment'>['render'] = React.useCallback(({ field, fieldState, formState }) => {
return (
<FormControl variant="floating" id={ field.name } size={{ base: 'md', lg: 'lg' }}>
<Textarea
{ ...field }
isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off"
maxH="160px"
maxLength={ 300 }
/>
<InputPlaceholder text="Comment" error={ fieldState.error }/>
</FormControl>
);
}, [ isReadOnly ]);
return (
<Controller
name="comment"
control={ control }
render={ renderControl }
rules={{ maxLength: 300 }}
/>
);
};
export default React.memo(TokenInfoFieldComment);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { Fields } from '../types';
import { validator } from 'lib/validations/url';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props {
control: Control<Fields>;
isReadOnly?: boolean;
}
const TokenInfoFieldDocs = ({ control, isReadOnly }: Props) => {
const renderControl: ControllerProps<Fields, 'docs'>['render'] = React.useCallback(({ field, fieldState, formState }) => {
return (
<FormControl variant="floating" id={ field.name } size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...field }
isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text="Docs" error={ fieldState.error }/>
</FormControl>
);
}, [ isReadOnly ]);
return (
<Controller
name="docs"
control={ control }
render={ renderControl }
rules={{ validate: validator }}
/>
);
};
export default React.memo(TokenInfoFieldDocs);
import { FormControl, Flex, Input } from '@chakra-ui/react'; import type { FormControlProps } from '@chakra-ui/react';
import { Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { Control, UseFormTrigger } from 'react-hook-form';
import { useController } from 'react-hook-form';
import type { Fields } from '../types'; import type { Fields } from '../types';
import { times } from 'lib/html-entities'; import { times } from 'lib/html-entities';
import { validator as validateUrl } from 'lib/validations/url'; import ImageUrlPreview from 'ui/shared/forms/components/ImageUrlPreview';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import FormFieldUrl from 'ui/shared/forms/fields/FormFieldUrl';
import useFieldWithImagePreview from 'ui/shared/forms/utils/useFieldWithImagePreview';
import TokenLogoPlaceholder from 'ui/shared/TokenLogoPlaceholder';
import TokenInfoIconPreview from '../TokenInfoIconPreview'; import TokenInfoIconPreview from '../TokenInfoIconPreview';
interface Props { interface Props {
control: Control<Fields>;
isReadOnly?: boolean; isReadOnly?: boolean;
trigger: UseFormTrigger<Fields>; size?: FormControlProps['size'];
} }
const TokenInfoFieldIconUrl = ({ control, isReadOnly, trigger }: Props) => { const TokenInfoFieldIconUrl = ({ isReadOnly, size }: Props) => {
const validatePreview = React.useCallback(() => { const previewUtils = useFieldWithImagePreview({ name: 'icon_url', isRequired: true });
return imageLoadError.current ? 'Unable to load image' : true;
}, [ ]);
const { field, formState, fieldState } = useController({
name: 'icon_url',
control,
rules: {
required: true,
validate: { url: validateUrl, preview: validatePreview },
},
});
const [ valueForPreview, setValueForPreview ] = React.useState<string>(field.value);
const imageLoadError = React.useRef(false);
const handleImageLoadSuccess = React.useCallback(() => {
imageLoadError.current = false;
trigger('icon_url');
}, [ trigger ]);
const handleImageLoadError = React.useCallback(() => {
imageLoadError.current = true;
trigger('icon_url');
}, [ trigger ]);
const handleBlur = React.useCallback(() => {
field.onBlur();
const isValidUrl = validateUrl(field.value);
isValidUrl === true && setValueForPreview(field.value);
}, [ field ]);
return ( return (
<Flex columnGap={ 5 }> <Flex columnGap={ 5 }>
<FormControl variant="floating" id={ field.name } size={{ base: 'md', lg: 'lg' }} isRequired> <FormFieldUrl<Fields>
<Input name="icon_url"
{ ...field } placeholder={ `Link to icon URL, link to download a SVG or 48${ times }48 PNG icon logo` }
onBlur={ handleBlur } isReadOnly={ isReadOnly }
isInvalid={ Boolean(fieldState.error) } size={ size }
isDisabled={ formState.isSubmitting } { ...previewUtils.input }
isReadOnly={ isReadOnly }
autoComplete="off"
required
/>
<InputPlaceholder text={ `Link to icon URL, link to download a SVG or 48${ times }48 PNG icon logo` } error={ fieldState.error }/>
</FormControl>
<TokenInfoIconPreview
url={ fieldState.error?.type === 'url' ? undefined : valueForPreview }
onLoad={ handleImageLoadSuccess }
onError={ !isReadOnly ? handleImageLoadError : undefined }
isInvalid={ fieldState.error?.type === 'preview' }
/> />
<TokenInfoIconPreview url={ previewUtils.preview.src } isInvalid={ previewUtils.preview.isInvalid }>
<ImageUrlPreview
{ ...previewUtils.preview }
fallback={ <TokenLogoPlaceholder boxSize={{ base: 10, lg: 12 }}/> }
boxSize={{ base: 10, lg: 12 }}
borderRadius="base"
/>
</TokenInfoIconPreview>
</Flex> </Flex>
); );
}; };
......
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { Fields, TickerUrlFields } from '../types';
import { validator } from 'lib/validations/url';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props {
control: Control<Fields>;
isReadOnly?: boolean;
name: keyof TickerUrlFields;
label: string;
}
const TokenInfoFieldPriceTicker = ({ control, isReadOnly, name, label }: Props) => {
const renderControl: ControllerProps<Fields, typeof name>['render'] = React.useCallback(({ field, fieldState, formState }) => {
return (
<FormControl variant="floating" id={ field.name } size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...field }
isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text={ label } error={ fieldState.error }/>
</FormControl>
);
}, [ isReadOnly, label ]);
return (
<Controller
name={ name }
control={ control }
render={ renderControl }
rules={{ validate: validator }}
/>
);
};
export default React.memo(TokenInfoFieldPriceTicker);
import { FormControl, Text, Textarea } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { Fields } from '../types';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props {
control: Control<Fields>;
isReadOnly?: boolean;
}
const TokenInfoFieldProjectDescription = ({ control, isReadOnly }: Props) => {
const renderControl: ControllerProps<Fields, 'project_description'>['render'] = React.useCallback(({ field, fieldState, formState }) => {
return (
<FormControl variant="floating" id={ field.name } size={{ base: 'md', lg: 'lg' }} isRequired>
<Textarea
{ ...field }
required
isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off"
maxH="160px"
maxLength={ 300 }
/>
<InputPlaceholder text="Project description" error={ fieldState.error }/>
<Text variant="secondary" fontSize="sm" mt={ 1 }>
Introduce or summarize the project’s operation/goals in a maximum of 300 characters.
The description should be written in a neutral point of view and must exclude unsubstantiated claims unless proven otherwise.
</Text>
</FormControl>
);
}, [ isReadOnly ]);
return (
<Controller
name="project_description"
control={ control }
render={ renderControl }
rules={{ required: true, maxLength: 300 }}
/>
);
};
export default React.memo(TokenInfoFieldProjectDescription);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { Fields } from '../types';
import { EMAIL_REGEXP } from 'lib/validations/email';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props {
control: Control<Fields>;
isReadOnly?: boolean;
}
const TokenInfoFieldProjectEmail = ({ control, isReadOnly }: Props) => {
const renderControl: ControllerProps<Fields, 'project_email'>['render'] = React.useCallback(({ field, fieldState, formState }) => {
return (
<FormControl variant="floating" id={ field.name } isRequired size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...field }
required
isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text="Official project email address" error={ fieldState.error }/>
</FormControl>
);
}, [ isReadOnly ]);
return (
<Controller
name="project_email"
control={ control }
render={ renderControl }
rules={{ required: true, pattern: EMAIL_REGEXP }}
/>
);
};
export default React.memo(TokenInfoFieldProjectEmail);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { Fields } from '../types';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props {
control: Control<Fields>;
isReadOnly?: boolean;
}
const TokenInfoFieldProjectName = ({ control, isReadOnly }: Props) => {
const renderControl: ControllerProps<Fields, 'project_name'>['render'] = React.useCallback(({ field, fieldState, formState }) => {
return (
<FormControl variant="floating" id={ field.name } size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...field }
isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text="Project name" error={ fieldState.error }/>
</FormControl>
);
}, [ isReadOnly ]);
return (
<Controller
name="project_name"
control={ control }
render={ renderControl }
/>
);
};
export default React.memo(TokenInfoFieldProjectName);
import React from 'react'; import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { Fields } from '../types'; import type { Fields } from '../types';
import type { TokenInfoApplicationConfig } from 'types/api/account'; import type { TokenInfoApplicationConfig } from 'types/api/account';
import useIsMobile from 'lib/hooks/useIsMobile'; import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
interface Props { interface Props {
control: Control<Fields>;
isReadOnly?: boolean; isReadOnly?: boolean;
config: TokenInfoApplicationConfig['projectSectors']; config: TokenInfoApplicationConfig['projectSectors'];
} }
const TokenInfoFieldProjectSector = ({ control, isReadOnly, config }: Props) => { const TokenInfoFieldProjectSector = ({ isReadOnly, config }: Props) => {
const isMobile = useIsMobile();
const options = React.useMemo(() => { const options = React.useMemo(() => {
return config.map((option) => ({ label: option, value: option })); return config.map((option) => ({ label: option, value: option }));
}, [ config ]); }, [ config ]);
const renderControl: ControllerProps<Fields, 'project_sector'>['render'] = React.useCallback(({ field, fieldState, formState }) => {
return (
<FancySelect
{ ...field }
options={ options }
size={ isMobile ? 'md' : 'lg' }
placeholder="Project industry"
isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
error={ fieldState.error }
/>
);
}, [ isReadOnly, options, isMobile ]);
return ( return (
<Controller <FormFieldFancySelect<Fields, 'project_sector'>
name="project_sector" name="project_sector"
control={ control } placeholder="Project industry"
render={ renderControl } options={ options }
isReadOnly={ isReadOnly }
/> />
); );
}; };
......
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { Fields } from '../types';
import { validator } from 'lib/validations/url';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props {
control: Control<Fields>;
isReadOnly?: boolean;
}
const TokenInfoFieldProjectWebsite = ({ control, isReadOnly }: Props) => {
const renderControl: ControllerProps<Fields, 'project_website'>['render'] = React.useCallback(({ field, fieldState, formState }) => {
return (
<FormControl variant="floating" id={ field.name } size={{ base: 'md', lg: 'lg' }} isRequired>
<Input
{ ...field }
isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off"
required
/>
<InputPlaceholder text="Official project website" error={ fieldState.error }/>
</FormControl>
);
}, [ isReadOnly ]);
return (
<Controller
name="project_website"
control={ control }
render={ renderControl }
rules={{ required: true, validate: validator }}
/>
);
};
export default React.memo(TokenInfoFieldProjectWebsite);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { Fields } from '../types';
import { EMAIL_REGEXP } from 'lib/validations/email';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props {
control: Control<Fields>;
isReadOnly?: boolean;
}
const TokenInfoFieldRequesterEmail = ({ control, isReadOnly }: Props) => {
const renderControl: ControllerProps<Fields, 'requester_email'>['render'] = React.useCallback(({ field, fieldState, formState }) => {
return (
<FormControl variant="floating" id={ field.name } isRequired size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...field }
required
isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text="Requester email" error={ fieldState.error }/>
</FormControl>
);
}, [ isReadOnly ]);
return (
<Controller
name="requester_email"
control={ control }
render={ renderControl }
rules={{ required: true, pattern: EMAIL_REGEXP }}
/>
);
};
export default React.memo(TokenInfoFieldRequesterEmail);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { Fields } from '../types';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props {
control: Control<Fields>;
isReadOnly?: boolean;
}
const TokenInfoFieldRequesterName = ({ control, isReadOnly }: Props) => {
const renderControl: ControllerProps<Fields, 'requester_name'>['render'] = React.useCallback(({ field, fieldState, formState }) => {
return (
<FormControl variant="floating" id={ field.name } isRequired size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...field }
required
isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text="Requester name" error={ fieldState.error }/>
</FormControl>
);
}, [ isReadOnly ]);
return (
<Controller
name="requester_name"
control={ control }
render={ renderControl }
rules={{ required: true }}
/>
);
};
export default React.memo(TokenInfoFieldRequesterName);
import { FormControl, Input, InputRightElement, InputGroup } from '@chakra-ui/react'; import type { FormControlProps } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form'; import type { ControllerRenderProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { Fields, SocialLinkFields } from '../types'; import type { Fields, SocialLinkFields } from '../types';
import { validator } from 'lib/validations/url'; import FormFieldUrl from 'ui/shared/forms/fields/FormFieldUrl';
import type { IconName } from 'ui/shared/IconSvg'; import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Item { interface Item {
icon: IconName; icon: IconName;
...@@ -29,38 +27,24 @@ const SETTINGS: Record<keyof SocialLinkFields, Item> = { ...@@ -29,38 +27,24 @@ const SETTINGS: Record<keyof SocialLinkFields, Item> = {
}; };
interface Props { interface Props {
control: Control<Fields>;
isReadOnly?: boolean; isReadOnly?: boolean;
size?: FormControlProps['size'];
name: keyof SocialLinkFields; name: keyof SocialLinkFields;
} }
const TokenInfoFieldSocialLink = ({ control, isReadOnly, name }: Props) => { const TokenInfoFieldSocialLink = ({ isReadOnly, size, name }: Props) => {
const renderControl: ControllerProps<Fields, typeof name>['render'] = React.useCallback(({ field, fieldState, formState }) => {
return ( const rightElement = React.useCallback(({ field }: { field: ControllerRenderProps<Fields, keyof SocialLinkFields> }) => {
<FormControl variant="floating" id={ field.name } size={{ base: 'md', lg: 'lg' }} sx={{ '.chakra-input__group input': { pr: '60px' } }}> return <IconSvg name={ SETTINGS[name].icon } boxSize={ 6 } color={ field.value ? SETTINGS[name].color : '#718096' }/>;
<InputGroup> }, [ name ]);
<Input
{ ...field }
isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text={ SETTINGS[name].label } error={ fieldState.error }/>
<InputRightElement h="100%">
<IconSvg name={ SETTINGS[name].icon } boxSize={ 6 } color={ field.value ? SETTINGS[name].color : '#718096' }/>
</InputRightElement>
</InputGroup>
</FormControl>
);
}, [ isReadOnly, name ]);
return ( return (
<Controller <FormFieldUrl<Fields, keyof SocialLinkFields>
name={ name } name={ name }
control={ control } placeholder={ SETTINGS[name].label }
render={ renderControl } rightElement={ rightElement }
rules={{ validate: validator }} isReadOnly={ isReadOnly }
size={ size }
/> />
); );
}; };
......
import { FormControl, Input } from '@chakra-ui/react'; import type { InputProps } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { Fields } from '../types'; import type { Fields } from '../types';
import { validator as emailValidator } from 'lib/validations/email'; import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import { validator as urlValidator } from 'lib/validations/url'; import { validator as emailValidator } from 'ui/shared/forms/validators/email';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import { urlValidator } from 'ui/shared/forms/validators/url';
interface Props { interface Props {
control: Control<Fields>;
isReadOnly?: boolean; isReadOnly?: boolean;
size?: InputProps['size'];
} }
const TokenInfoFieldSupport = ({ control, isReadOnly }: Props) => { const TokenInfoFieldSupport = (props: Props) => {
const renderControl: ControllerProps<Fields, 'support'>['render'] = React.useCallback(({ field, fieldState, formState }) => {
return (
<FormControl variant="floating" id={ field.name } size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...field }
isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text="Support URL or email" error={ fieldState.error }/>
</FormControl>
);
}, [ isReadOnly ]);
const validate = React.useCallback((newValue: string | undefined) => { const validate = React.useCallback((newValue: string | undefined) => {
if (typeof newValue !== 'string') {
return true;
}
const urlValidationResult = urlValidator(newValue); const urlValidationResult = urlValidator(newValue);
const emailValidationResult = emailValidator(newValue || ''); const emailValidationResult = emailValidator(newValue);
if (urlValidationResult === true || emailValidationResult === true) { if (urlValidationResult === true || emailValidationResult === true) {
return true; return true;
...@@ -43,11 +29,11 @@ const TokenInfoFieldSupport = ({ control, isReadOnly }: Props) => { ...@@ -43,11 +29,11 @@ const TokenInfoFieldSupport = ({ control, isReadOnly }: Props) => {
}, []); }, []);
return ( return (
<Controller <FormFieldText<Fields, 'support'>
name="support" name="support"
control={ control } placeholder="Support URL or email"
render={ renderControl }
rules={{ validate }} rules={{ validate }}
{ ...props }
/> />
); );
}; };
......
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerRenderProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { Fields } from '../types';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props {
control: Control<Fields>;
}
const TokenInfoFieldTokenName = ({ control }: Props) => {
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'token_name'>}) => {
return (
<FormControl variant="floating" id={ field.name } isRequired size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...field }
required
isReadOnly
/>
<InputPlaceholder text="Token name"/>
</FormControl>
);
}, []);
return (
<Controller
name="token_name"
control={ control }
render={ renderControl }
/>
);
};
export default React.memo(TokenInfoFieldTokenName);
import type { Option } from 'ui/shared/FancySelect/types'; import type { Option } from 'ui/shared/forms/inputs/select/types';
export interface Fields extends SocialLinkFields, TickerUrlFields { export interface Fields extends SocialLinkFields, TickerUrlFields {
address: string; address: string;
......
...@@ -6,19 +6,18 @@ import { ...@@ -6,19 +6,18 @@ import {
useDisclosure, useDisclosure,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useState } from 'react';
import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form'; import { useForm, FormProvider } from 'react-hook-form';
import type { WatchlistAddress, WatchlistErrors } from 'types/api/account'; import type { WatchlistAddress, WatchlistErrors } from 'types/api/account';
import type { ResourceErrorAccount } from 'lib/api/resources'; import type { ResourceErrorAccount } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import { ADDRESS_REGEXP } from 'lib/validations/address'; import FormFieldAddress from 'ui/shared/forms/fields/FormFieldAddress';
import AddressInput from 'ui/shared/AddressInput'; import FormFieldCheckbox from 'ui/shared/forms/fields/FormFieldCheckbox';
import CheckboxInput from 'ui/shared/CheckboxInput'; import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import TagInput from 'ui/shared/TagInput';
import AuthModal from 'ui/snippets/auth/AuthModal'; import AuthModal from 'ui/snippets/auth/AuthModal';
import useProfileQuery from 'ui/snippets/auth/useProfileQuery'; import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
...@@ -36,7 +35,7 @@ type Props = { ...@@ -36,7 +35,7 @@ type Props = {
isAdd: boolean; isAdd: boolean;
} }
type Inputs = { export type Inputs = {
address: string; address: string;
tag: string; tag: string;
notification: boolean; notification: boolean;
...@@ -60,16 +59,6 @@ type Inputs = { ...@@ -60,16 +59,6 @@ type Inputs = {
}; };
} }
type Checkboxes = 'notification' |
'notification_settings.native.outcoming' |
'notification_settings.native.incoming' |
'notification_settings.ERC-20.outcoming' |
'notification_settings.ERC-20.incoming' |
'notification_settings.ERC-721.outcoming' |
'notification_settings.ERC-721.incoming' |
'notification_settings.ERC-404.outcoming' |
'notification_settings.ERC-404.incoming';
const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd }) => { const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd }) => {
const [ pending, setPending ] = useState(false); const [ pending, setPending ] = useState(false);
...@@ -84,7 +73,7 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd ...@@ -84,7 +73,7 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd
notificationsDefault = data.notification_settings; notificationsDefault = data.notification_settings;
} }
const { control, handleSubmit, formState: { errors, isDirty }, setError } = useForm<Inputs>({ const formApi = useForm<Inputs>({
defaultValues: { defaultValues: {
address: data?.address_hash || '', address: data?.address_hash || '',
tag: data?.name || '', tag: data?.name || '',
...@@ -118,7 +107,7 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd ...@@ -118,7 +107,7 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd
} }
} }
const { mutate } = useMutation({ const { mutateAsync } = useMutation({
mutationFn: updateWatchlist, mutationFn: updateWatchlist,
onSuccess: async() => { onSuccess: async() => {
await onSuccess(); await onSuccess();
...@@ -128,109 +117,85 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd ...@@ -128,109 +117,85 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd
setPending(false); setPending(false);
const errorMap = error.payload?.errors; const errorMap = error.payload?.errors;
if (errorMap?.address_hash || errorMap?.name) { if (errorMap?.address_hash || errorMap?.name) {
errorMap?.address_hash && setError('address', { type: 'custom', message: getErrorMessage(errorMap, 'address_hash') }); errorMap?.address_hash && formApi.setError('address', { type: 'custom', message: getErrorMessage(errorMap, 'address_hash') });
errorMap?.name && setError('tag', { type: 'custom', message: getErrorMessage(errorMap, 'name') }); errorMap?.name && formApi.setError('tag', { type: 'custom', message: getErrorMessage(errorMap, 'name') });
} else if (errorMap?.watchlist_id) { } else if (errorMap?.watchlist_id) {
setError('address', { type: 'custom', message: getErrorMessage(errorMap, 'watchlist_id') }); formApi.setError('address', { type: 'custom', message: getErrorMessage(errorMap, 'watchlist_id') });
} else { } else {
setAlertVisible(true); setAlertVisible(true);
} }
}, },
}); });
const onSubmit: SubmitHandler<Inputs> = (formData) => { const onSubmit: SubmitHandler<Inputs> = async(formData) => {
setAlertVisible(false); setAlertVisible(false);
setPending(true); setPending(true);
mutate(formData); await mutateAsync(formData);
}; };
const renderAddressInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'address'>}) => {
return (
<AddressInput<Inputs, 'address'>
field={ field }
bgColor="dialog_bg"
error={ errors.address }
/>
);
}, [ errors ]);
const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => {
return <TagInput<Inputs, 'tag'> field={ field } error={ errors.tag } bgColor="dialog_bg"/>;
}, [ errors ]);
// eslint-disable-next-line react/display-name
const renderCheckbox = useCallback((text: string) => ({ field }: {field: ControllerRenderProps<Inputs, Checkboxes>}) => (
<CheckboxInput<Inputs, Checkboxes> text={ text } field={ field }/>
), []);
return ( return (
<form noValidate onSubmit={ handleSubmit(onSubmit) }> <FormProvider { ...formApi }>
<Box marginBottom={ 5 }> <form noValidate onSubmit={ formApi.handleSubmit(onSubmit) }>
<Controller <FormFieldAddress<Inputs>
name="address" name="address"
control={ control } isRequired
rules={{ bgColor="dialog_bg"
pattern: ADDRESS_REGEXP, mb={ 5 }
required: true,
}}
render={ renderAddressInput }
/> />
</Box> <FormFieldText<Inputs>
<Box marginBottom={ 8 }>
<Controller
name="tag" name="tag"
control={ control } placeholder="Private tag (max 35 characters)"
isRequired
rules={{ rules={{
maxLength: TAG_MAX_LENGTH, maxLength: TAG_MAX_LENGTH,
required: true,
}} }}
render={ renderTagInput } bgColor="dialog_bg"
mb={ 8 }
/> />
</Box> { userWithoutEmail ? (
{ userWithoutEmail ? ( <>
<> <Alert
<Alert status="info"
status="info" colorScheme="gray"
colorScheme="gray" display="flex"
display="flex" flexDirection={{ base: 'column', md: 'row' }}
flexDirection={{ base: 'column', md: 'row' }} alignItems={{ base: 'flex-start', lg: 'center' }}
alignItems={{ base: 'flex-start', lg: 'center' }} columnGap={ 2 }
columnGap={ 2 } rowGap={ 2 }
rowGap={ 2 } w="fit-content"
w="fit-content" >
>
To receive notifications you need to add an email to your profile. To receive notifications you need to add an email to your profile.
<Button variant="outline" size="sm" onClick={ authModal.onOpen }>Add email</Button> <Button variant="outline" size="sm" onClick={ authModal.onOpen }>Add email</Button>
</Alert> </Alert>
{ authModal.isOpen && <AuthModal initialScreen={{ type: 'email', isAuth: true }} onClose={ authModal.onClose }/> } { authModal.isOpen && <AuthModal initialScreen={{ type: 'email', isAuth: true }} onClose={ authModal.onClose }/> }
</> </>
) : ( ) : (
<> <>
<Text variant="secondary" fontSize="sm" marginBottom={ 5 }> <Text variant="secondary" fontSize="sm" marginBottom={ 5 }>
Please select what types of notifications you will receive Please select what types of notifications you will receive
</Text> </Text>
<Box marginBottom={ 8 }> <Box marginBottom={ 8 }>
<AddressFormNotifications control={ control }/> <AddressFormNotifications/>
</Box> </Box>
<Text variant="secondary" fontSize="sm" marginBottom={{ base: '10px', lg: 5 }}>Notification methods</Text> <Text variant="secondary" fontSize="sm" marginBottom={{ base: '10px', lg: 5 }}>Notification methods</Text>
<Controller <FormFieldCheckbox<Inputs, 'notification'>
name={ 'notification' as Checkboxes } name="notification"
control={ control } label="Email notifications"
render={ renderCheckbox('Email notifications') } />
/> </>
</> ) }
) } <Box marginTop={ 8 }>
<Box marginTop={ 8 }> <Button
<Button size="lg"
size="lg" type="submit"
type="submit" isLoading={ pending }
isLoading={ pending } isDisabled={ !formApi.formState.isDirty }
isDisabled={ !isDirty } >
> { !isAdd ? 'Save changes' : 'Add address' }
{ !isAdd ? 'Save changes' : 'Add address' } </Button>
</Button> </Box>
</Box> </form>
</form> </FormProvider>
); );
}; };
......
import { Grid, GridItem } from '@chakra-ui/react'; import { Grid, GridItem } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import React from 'react';
import { Controller } from 'react-hook-form';
import type { Path, ControllerRenderProps, FieldValues, Control } from 'react-hook-form';
import config from 'configs/app'; import config from 'configs/app';
import CheckboxInput from 'ui/shared/CheckboxInput'; import FormFieldCheckbox from 'ui/shared/forms/fields/FormFieldCheckbox';
import type { Inputs as FormFields } from './AddressForm';
const tokenStandardName = config.chain.tokenStandard; const tokenStandardName = config.chain.tokenStandard;
...@@ -15,21 +15,12 @@ const NOTIFICATIONS_NAMES = [ ...@@ -15,21 +15,12 @@ const NOTIFICATIONS_NAMES = [
`${ tokenStandardName }-721, ${ tokenStandardName }-1155 (NFT)`, `${ tokenStandardName }-721, ${ tokenStandardName }-1155 (NFT)`,
`${ tokenStandardName }-404` ]; `${ tokenStandardName }-404` ];
type Props<Inputs extends FieldValues> = { export default function AddressFormNotifications() {
control: Control<Inputs>;
}
export default function AddressFormNotifications<Inputs extends FieldValues, Checkboxes extends Path<Inputs>>({ control }: Props<Inputs>) {
// eslint-disable-next-line react/display-name
const renderCheckbox = useCallback((text: string) => ({ field }: {field: ControllerRenderProps<Inputs, Checkboxes>}) => (
<CheckboxInput<Inputs, Checkboxes> text={ text } field={ field }/>
), []);
return ( return (
<Grid templateColumns={{ base: 'repeat(2, max-content)', lg: 'repeat(3, max-content)' }} gap={{ base: '10px 24px', lg: '20px 24px' }}> <Grid templateColumns={{ base: 'repeat(2, max-content)', lg: 'repeat(3, max-content)' }} gap={{ base: '10px 24px', lg: '20px 24px' }}>
{ NOTIFICATIONS.map((notification: string, index: number) => { { NOTIFICATIONS.map((notification, index: number) => {
const incomingFieldName = `notification_settings.${ notification }.incoming` as Checkboxes; const incomingFieldName = `notification_settings.${ notification }.incoming` as const;
const outgoingFieldName = `notification_settings.${ notification }.outcoming` as Checkboxes; const outgoingFieldName = `notification_settings.${ notification }.outcoming` as const;
return ( return (
<React.Fragment key={ notification }> <React.Fragment key={ notification }>
<GridItem <GridItem
...@@ -42,19 +33,15 @@ export default function AddressFormNotifications<Inputs extends FieldValues, Che ...@@ -42,19 +33,15 @@ export default function AddressFormNotifications<Inputs extends FieldValues, Che
{ NOTIFICATIONS_NAMES[index] } { NOTIFICATIONS_NAMES[index] }
</GridItem> </GridItem>
<GridItem> <GridItem>
<Controller <FormFieldCheckbox<FormFields, typeof incomingFieldName>
name={ incomingFieldName } name={ incomingFieldName }
control={ control } label="Incoming"
render={ renderCheckbox('Incoming') }
/> />
</GridItem> </GridItem>
<GridItem> <GridItem>
<Controller <FormFieldCheckbox<FormFields, typeof outgoingFieldName>
name={ outgoingFieldName } name={ outgoingFieldName }
control={ control } label="Outgoing"
render={ renderCheckbox('Outgoing') }
/> />
</GridItem> </GridItem>
</React.Fragment> </React.Fragment>
......
...@@ -9137,19 +9137,6 @@ electron-to-chromium@^1.5.28: ...@@ -9137,19 +9137,6 @@ electron-to-chromium@^1.5.28:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.28.tgz#aee074e202c6ee8a0030a9c2ef0b3fe9f967d576" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.28.tgz#aee074e202c6ee8a0030a9c2ef0b3fe9f967d576"
integrity sha512-VufdJl+rzaKZoYVUijN13QcXVF5dWPZANeFTLNy+OSpHdDL5ynXTF35+60RSBbaQYB1ae723lQXHCrf4pyLsMw== integrity sha512-VufdJl+rzaKZoYVUijN13QcXVF5dWPZANeFTLNy+OSpHdDL5ynXTF35+60RSBbaQYB1ae723lQXHCrf4pyLsMw==
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"
hash.js "^1.0.0"
hmac-drbg "^1.0.1"
inherits "^2.0.4"
minimalistic-assert "^1.0.1"
minimalistic-crypto-utils "^1.0.1"
elliptic@^6.5.7: elliptic@^6.5.7:
version "6.5.7" version "6.5.7"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.7.tgz#8ec4da2cb2939926a1b9a73619d768207e647c8b" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.7.tgz#8ec4da2cb2939926a1b9a73619d768207e647c8b"
...@@ -11947,12 +11934,7 @@ json5@^1.0.2: ...@@ -11947,12 +11934,7 @@ json5@^1.0.2:
dependencies: dependencies:
minimist "^1.2.0" minimist "^1.2.0"
json5@^2.2.1: json5@^2.2.1, json5@^2.2.2, json5@^2.2.3:
version "2.2.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c"
integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==
json5@^2.2.2, json5@^2.2.3:
version "2.2.3" version "2.2.3"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
...@@ -13900,10 +13882,10 @@ react-google-recaptcha-v3@1.10.1: ...@@ -13900,10 +13882,10 @@ react-google-recaptcha-v3@1.10.1:
dependencies: dependencies:
hoist-non-react-statics "^3.3.2" hoist-non-react-statics "^3.3.2"
react-hook-form@^7.33.1: react-hook-form@7.52.1:
version "7.37.0" version "7.52.1"
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.37.0.tgz#4d1738f092d3d8a3ade34ee892d97350b1032b19" resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.52.1.tgz#ec2c96437b977f8b89ae2d541a70736c66284852"
integrity sha512-6NFTxsnw+EXSpNNvLr5nFMjPdYKRryQcelTHg7zwBB6vAzfPIcZq4AExP4heVlwdzntepQgwiOQW4z7Mr99Lsg== integrity sha512-uNKIhaoICJ5KQALYZ4TOaOLElyM+xipord+Ha3crEFhTntdLvWZqVY49Wqd/0GiVCA/f9NjemLeiNPjG7Hpurg==
react-identicons@^1.2.5: react-identicons@^1.2.5:
version "1.2.5" version "1.2.5"
...@@ -14581,11 +14563,11 @@ scslre@^0.1.6: ...@@ -14581,11 +14563,11 @@ scslre@^0.1.6:
regexpp "^3.2.0" regexpp "^3.2.0"
secp256k1@^5.0.0: secp256k1@^5.0.0:
version "5.0.0" version "5.0.1"
resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-5.0.0.tgz#be6f0c8c7722e2481e9773336d351de8cddd12f7" resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-5.0.1.tgz#dc2c86187d48ff2da756f0f7e96417ee03c414b1"
integrity sha512-TKWX8xvoGHrxVdqbYeZM9w+izTF4b9z3NhSaDkdn81btvuh+ivbIMGT/zQvDtTFWhRlThpoz6LEYTr7n8A5GcA== integrity "sha1-3CyGGH1I/y2nVvD36WQX7gPEFLE= sha512-lDFs9AAIaWP9UCdtWrotXWWF9t8PWgQDcxqgAnpM9rMqxb3Oaq2J0thzPVSxBwdJgyQtkU/sYtFtbM1RSt/iYA=="
dependencies: dependencies:
elliptic "^6.5.4" elliptic "^6.5.7"
node-addon-api "^5.0.0" node-addon-api "^5.0.0"
node-gyp-build "^4.2.0" node-gyp-build "^4.2.0"
...@@ -14926,7 +14908,16 @@ string-length@^4.0.1: ...@@ -14926,7 +14908,16 @@ string-length@^4.0.1:
char-regex "^1.0.2" char-regex "^1.0.2"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: "string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
...@@ -15054,7 +15045,14 @@ string_decoder@~1.1.1: ...@@ -15054,7 +15045,14 @@ string_decoder@~1.1.1:
dependencies: dependencies:
safe-buffer "~5.1.0" safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: "strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
...@@ -16191,7 +16189,7 @@ word-wrap@^1.2.5, word-wrap@~1.2.3: ...@@ -16191,7 +16189,7 @@ word-wrap@^1.2.5, word-wrap@~1.2.3:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
...@@ -16209,6 +16207,15 @@ wrap-ansi@^6.2.0: ...@@ -16209,6 +16207,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0" string-width "^4.1.0"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0: wrap-ansi@^8.1.0:
version "8.1.0" version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment