Commit dfc33b4b authored by Max Alekseenko's avatar Max Alekseenko

Merge main into rewards

parents 3ead00d4 d5863de0
......@@ -81,6 +81,12 @@ jobs:
with:
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
# There is an issue with building web3modal deps
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 { MultichainProviderConfig } from 'types/client/multichainProviderConfig';
import type { MultichainProviderConfig, MultichainProviderConfigParsed } from 'types/client/multichainProviderConfig';
import { getEnvValue, parseEnvJson } from '../utils';
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 config: Feature<{name: string; logoUrl?: string; urlTemplate: string; dappId?: string }> = (() => {
const config: Feature<{ providers: Array<MultichainProviderConfigParsed> }> = (() => {
if (value) {
return Object.freeze({
title,
isEnabled: true,
name: value.name,
logoUrl: value.logo,
urlTemplate: value.url_template,
dappId: marketplace.isEnabled ? value.dapp_id : undefined,
providers: value.map((provider) => ({
name: provider.name,
logoUrl: provider.logo,
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
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_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_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps']
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
......
......@@ -39,7 +39,7 @@ NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKj
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
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_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps']
......
......@@ -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_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
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_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps','/account/rewards']
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
......
......@@ -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_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
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_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps']
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
......
......@@ -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_METADATA_SERVICE_API_HOST=https://metadata-test.k8s-dev.blockscout.com
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_NAVIGATION_HIGHLIGHTED_ROUTES=['/blocks','/apps']
NEXT_PUBLIC_NAVIGATION_LAYOUT=horizontal
......
......@@ -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_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
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_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps']
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
......
......@@ -27,7 +27,7 @@ NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_LOGOUT_URL=https://zksync.us.auth0.com/v2/logout
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
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_NAME=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
......
......@@ -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_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
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_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
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)'}
......@@ -39,9 +39,14 @@ export_envs_from_preset
./download_assets.sh ./public/assets/configs
# Check run-time ENVs values
./validate_envs.sh
if [ $? -ne 0 ]; then
exit 1
if [ "$SKIP_ENVS_VALIDATION" != "true" ]; then
./validate_envs.sh
if [ $? -ne 0 ]; then
exit 1
fi
else
echo "😱 Skipping ENVs validation."
echo
fi
# Generate favicons bundle
......
......@@ -90,7 +90,7 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
braces@^3.0.2:
braces@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
......@@ -441,11 +441,11 @@ merge2@^1.3.0, merge2@^1.4.1:
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
micromatch@^4.0.4:
version "4.0.5"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
version "4.0.8"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
dependencies:
braces "^3.0.2"
braces "^3.0.3"
picomatch "^2.3.1"
minimatch@^3.1.1:
......@@ -582,9 +582,9 @@ requirejs-config-file@^4.0.0:
stringify-object "^3.2.1"
requirejs@^2.3.6:
version "2.3.6"
resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.6.tgz#e5093d9601c2829251258c0b9445d4d19fa9e7c9"
integrity sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg==
version "2.3.7"
resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.7.tgz#0b22032e51a967900e0ae9f32762c23a87036bd0"
integrity "sha1-CyIDLlGpZ5AOCunzJ2LCOocDa9A= sha512-DouTG8T1WanGok6Qjg2SXuCMzszOo0eHeH9hDZ5Y4x8Je+9JB38HdTLT4/VA8OaUhBa0JPVHJ0pyBkM1z+pDsw=="
resolve-dependency-path@^3.0.2:
version "3.0.2"
......
......@@ -530,6 +530,13 @@ const deFiDropdownItemSchema: yup.ObjectSchema<DeFiDropdownItem> = yup
return Boolean(value.dappId) || Boolean(value.url);
}) 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
.object()
.noUnknown(true, (params) => {
......@@ -768,18 +775,10 @@ const schema = yup
NEXT_PUBLIC_HAS_USER_OPS: yup.boolean(),
NEXT_PUBLIC_METASUITES_ENABLED: yup.boolean(),
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG: yup
.mixed()
.test('shape', 'Invalid schema were provided for NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG, it should have name and url template', (data) => {
const isUndefined = data === undefined;
const valueSchema = yup.object<MultichainProviderConfig>().transform(replaceQuotes).json().shape({
name: yup.string().required(),
url_template: yup.string().required(),
logo: yup.string(),
dapp_id: yup.string(),
});
return isUndefined || valueSchema.isValidSync(data);
}),
.array()
.transform(replaceQuotes)
.json()
.of(multichainProviderConfigSchema),
NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG: yup
.mixed()
.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
NEXT_PUBLIC_WEB3_WALLETS=['coinbase','metamask','token_pocket']
NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE=stability
NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'uniswap'},{'text':'Payment link','icon':'payment_link','url':'https://example.com'}]
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG=[{'name': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}]
NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Need gas?', 'dapp_id': 'smol-refuel', 'url_template': 'https://smolrefuel.com/?outboundChain={chainId}&partner=blockscout&utm_source=blockscout&utm_medium=address&disableBridges=true', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png'}
NEXT_PUBLIC_SAVE_ON_GAS_ENABLED=true
NEXT_PUBLIC_REWARDS_SERVICE_API_HOST=https://example.com
......@@ -21,6 +21,11 @@
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
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":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
......@@ -44,7 +49,20 @@
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
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"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6"
integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==
......@@ -52,28 +70,12 @@
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"
"@types/eslint-scope@^3.7.3":
version "3.7.4"
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16"
integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==
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@^1.0.5":
version "1.0.6"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
"@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":
"@types/json-schema@^7.0.8":
version "7.0.12"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
......@@ -83,10 +85,10 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.2.tgz#129cc9ae69f93824f92fac653eebfb4812ab4af9"
integrity sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==
"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5":
version "1.11.6"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24"
integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==
"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1":
version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb"
integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==
dependencies:
"@webassemblyjs/helper-numbers" "1.11.6"
"@webassemblyjs/helper-wasm-bytecode" "1.11.6"
......@@ -101,10 +103,10 @@
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768"
integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==
"@webassemblyjs/helper-buffer@1.11.6":
version "1.11.6"
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093"
integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==
"@webassemblyjs/helper-buffer@1.12.1":
version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6"
integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==
"@webassemblyjs/helper-numbers@1.11.6":
version "1.11.6"
......@@ -120,15 +122,15 @@
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9"
integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==
"@webassemblyjs/helper-wasm-section@1.11.6":
version "1.11.6"
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577"
integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==
"@webassemblyjs/helper-wasm-section@1.12.1":
version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf"
integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==
dependencies:
"@webassemblyjs/ast" "1.11.6"
"@webassemblyjs/helper-buffer" "1.11.6"
"@webassemblyjs/ast" "1.12.1"
"@webassemblyjs/helper-buffer" "1.12.1"
"@webassemblyjs/helper-wasm-bytecode" "1.11.6"
"@webassemblyjs/wasm-gen" "1.11.6"
"@webassemblyjs/wasm-gen" "1.12.1"
"@webassemblyjs/ieee754@1.11.6":
version "1.11.6"
......@@ -149,59 +151,59 @@
resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a"
integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==
"@webassemblyjs/wasm-edit@^1.11.5":
version "1.11.6"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab"
integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==
"@webassemblyjs/wasm-edit@^1.12.1":
version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b"
integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==
dependencies:
"@webassemblyjs/ast" "1.11.6"
"@webassemblyjs/helper-buffer" "1.11.6"
"@webassemblyjs/ast" "1.12.1"
"@webassemblyjs/helper-buffer" "1.12.1"
"@webassemblyjs/helper-wasm-bytecode" "1.11.6"
"@webassemblyjs/helper-wasm-section" "1.11.6"
"@webassemblyjs/wasm-gen" "1.11.6"
"@webassemblyjs/wasm-opt" "1.11.6"
"@webassemblyjs/wasm-parser" "1.11.6"
"@webassemblyjs/wast-printer" "1.11.6"
"@webassemblyjs/wasm-gen@1.11.6":
version "1.11.6"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268"
integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==
dependencies:
"@webassemblyjs/ast" "1.11.6"
"@webassemblyjs/helper-wasm-section" "1.12.1"
"@webassemblyjs/wasm-gen" "1.12.1"
"@webassemblyjs/wasm-opt" "1.12.1"
"@webassemblyjs/wasm-parser" "1.12.1"
"@webassemblyjs/wast-printer" "1.12.1"
"@webassemblyjs/wasm-gen@1.12.1":
version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547"
integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==
dependencies:
"@webassemblyjs/ast" "1.12.1"
"@webassemblyjs/helper-wasm-bytecode" "1.11.6"
"@webassemblyjs/ieee754" "1.11.6"
"@webassemblyjs/leb128" "1.11.6"
"@webassemblyjs/utf8" "1.11.6"
"@webassemblyjs/wasm-opt@1.11.6":
version "1.11.6"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2"
integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==
"@webassemblyjs/wasm-opt@1.12.1":
version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5"
integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==
dependencies:
"@webassemblyjs/ast" "1.11.6"
"@webassemblyjs/helper-buffer" "1.11.6"
"@webassemblyjs/wasm-gen" "1.11.6"
"@webassemblyjs/wasm-parser" "1.11.6"
"@webassemblyjs/ast" "1.12.1"
"@webassemblyjs/helper-buffer" "1.12.1"
"@webassemblyjs/wasm-gen" "1.12.1"
"@webassemblyjs/wasm-parser" "1.12.1"
"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5":
version "1.11.6"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1"
integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==
"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1":
version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937"
integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==
dependencies:
"@webassemblyjs/ast" "1.11.6"
"@webassemblyjs/ast" "1.12.1"
"@webassemblyjs/helper-api-error" "1.11.6"
"@webassemblyjs/helper-wasm-bytecode" "1.11.6"
"@webassemblyjs/ieee754" "1.11.6"
"@webassemblyjs/leb128" "1.11.6"
"@webassemblyjs/utf8" "1.11.6"
"@webassemblyjs/wast-printer@1.11.6":
version "1.11.6"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20"
integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==
"@webassemblyjs/wast-printer@1.12.1":
version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac"
integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==
dependencies:
"@webassemblyjs/ast" "1.11.6"
"@webassemblyjs/ast" "1.12.1"
"@xtuc/long" "4.2.2"
"@webpack-cli/configtest@^2.1.1":
......@@ -229,10 +231,10 @@
resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
acorn-import-assertions@^1.9.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac"
integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==
acorn-import-attributes@^1.9.5:
version "1.9.5"
resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef"
integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==
acorn@^8.7.1, acorn@^8.8.2:
version "8.10.0"
......@@ -261,32 +263,32 @@ ansi-styles@^4.1.0:
dependencies:
color-convert "^2.0.1"
braces@^3.0.2:
braces@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies:
fill-range "^7.1.1"
browserslist@^4.14.5:
version "4.21.9"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.9.tgz#e11bdd3c313d7e2a9e87e8b4b0c7872b13897635"
integrity sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==
browserslist@^4.21.10:
version "4.24.2"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580"
integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==
dependencies:
caniuse-lite "^1.0.30001503"
electron-to-chromium "^1.4.431"
node-releases "^2.0.12"
update-browserslist-db "^1.0.11"
caniuse-lite "^1.0.30001669"
electron-to-chromium "^1.5.41"
node-releases "^2.0.18"
update-browserslist-db "^1.1.1"
buffer-from@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
caniuse-lite@^1.0.30001503:
version "1.0.30001517"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz#90fabae294215c3495807eb24fc809e11dc2f0a8"
integrity sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==
caniuse-lite@^1.0.30001669:
version "1.0.30001673"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001673.tgz#5aa291557af1c71340e809987367410aab7a5a9e"
integrity sha512-WTrjUCSMp3LYX0nE12ECkV0a+e6LC85E0Auz75555/qr78Oc8YWhEPNfDd6SHdtlCMSzqtuXY0uyEMNRcsKpKw==
chalk@^4.1.0:
version "4.1.2"
......@@ -366,12 +368,12 @@ dotenv@^16.0.0:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==
electron-to-chromium@^1.4.431:
version "1.4.467"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.467.tgz#b0660bf644baff7eedea33b8c742fb53ec60e3c2"
integrity sha512-2qI70O+rR4poYeF2grcuS/bCps5KJh6y1jtZMDDEteyKJQrzLOEhFyXCLcHW6DTBjKjWkk26JhWoAi+Ux9A0fg==
electron-to-chromium@^1.5.41:
version "1.5.47"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.47.tgz#ef0751bc19b28be8ee44cd8405309de3bf3b20c7"
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"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35"
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:
graceful-fs "^4.2.4"
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:
version "7.10.0"
resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.10.0.tgz#55146e3909cc5fe63c22da63fb15b05aeac35b13"
......@@ -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"
integrity sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==
escalade@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
escalade@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
eslint-scope@5.1.1:
version "5.1.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"
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"
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==
......@@ -577,11 +587,11 @@ merge-stream@^2.0.0:
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
micromatch@^4.0.0:
version "4.0.5"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
version "4.0.8"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
dependencies:
braces "^3.0.2"
braces "^3.0.3"
picomatch "^2.3.1"
mime-db@1.52.0:
......@@ -606,10 +616,10 @@ neo-async@^2.6.2:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
node-releases@^2.0.12:
version "2.0.13"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d"
integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==
node-releases@^2.0.18:
version "2.0.18"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f"
integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==
p-limit@^2.2.0:
version "2.3.0"
......@@ -645,10 +655,10 @@ path-parse@^1.0.7:
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
picocolors@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^2.3.1:
version "2.3.1"
......@@ -796,21 +806,21 @@ tapable@^2.1.1, tapable@^2.2.0:
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
terser-webpack-plugin@^5.3.7:
version "5.3.9"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1"
integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==
terser-webpack-plugin@^5.3.10:
version "5.3.10"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199"
integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==
dependencies:
"@jridgewell/trace-mapping" "^0.3.17"
"@jridgewell/trace-mapping" "^0.3.20"
jest-worker "^27.4.5"
schema-utils "^3.1.1"
serialize-javascript "^6.0.1"
terser "^5.16.8"
terser "^5.26.0"
terser@^5.16.8:
version "5.19.2"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.19.2.tgz#bdb8017a9a4a8de4663a7983f45c506534f9234e"
integrity sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==
terser@^5.26.0:
version "5.36.0"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.36.0.tgz#8b0dbed459ac40ff7b4c9fd5a3a2029de105180e"
integrity sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==
dependencies:
"@jridgewell/source-map" "^0.3.3"
acorn "^8.8.2"
......@@ -867,13 +877,13 @@ type-fest@^2.19.0:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
update-browserslist-db@^1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940"
integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==
update-browserslist-db@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5"
integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==
dependencies:
escalade "^3.1.1"
picocolors "^1.0.0"
escalade "^3.2.0"
picocolors "^1.1.0"
uri-js@^4.2.2:
version "4.4.1"
......@@ -882,10 +892,10 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
watchpack@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
watchpack@^2.4.1:
version "2.4.2"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da"
integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==
dependencies:
glob-to-regexp "^0.4.1"
graceful-fs "^4.1.2"
......@@ -923,33 +933,32 @@ webpack-sources@^3.2.3:
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
webpack@^5.88.2:
version "5.88.2"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.88.2.tgz#f62b4b842f1c6ff580f3fcb2ed4f0b579f4c210e"
integrity sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==
dependencies:
"@types/eslint-scope" "^3.7.3"
"@types/estree" "^1.0.0"
"@webassemblyjs/ast" "^1.11.5"
"@webassemblyjs/wasm-edit" "^1.11.5"
"@webassemblyjs/wasm-parser" "^1.11.5"
version "5.95.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.95.0.tgz#8fd8c454fa60dad186fbe36c400a55848307b4c0"
integrity sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==
dependencies:
"@types/estree" "^1.0.5"
"@webassemblyjs/ast" "^1.12.1"
"@webassemblyjs/wasm-edit" "^1.12.1"
"@webassemblyjs/wasm-parser" "^1.12.1"
acorn "^8.7.1"
acorn-import-assertions "^1.9.0"
browserslist "^4.14.5"
acorn-import-attributes "^1.9.5"
browserslist "^4.21.10"
chrome-trace-event "^1.0.2"
enhanced-resolve "^5.15.0"
enhanced-resolve "^5.17.1"
es-module-lexer "^1.2.1"
eslint-scope "5.1.1"
events "^3.2.0"
glob-to-regexp "^0.4.1"
graceful-fs "^4.2.9"
graceful-fs "^4.2.11"
json-parse-even-better-errors "^2.3.1"
loader-runner "^4.2.0"
mime-types "^2.1.27"
neo-async "^2.6.2"
schema-utils "^3.2.0"
tapable "^2.1.1"
terser-webpack-plugin "^5.3.7"
watchpack "^2.4.0"
terser-webpack-plugin "^5.3.10"
watchpack "^2.4.1"
webpack-sources "^3.2.3"
which@^2.0.1:
......
......@@ -39,7 +39,15 @@
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
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"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811"
integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==
......@@ -68,28 +76,12 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@types/eslint-scope@^3.7.3":
version "3.7.4"
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16"
integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==
dependencies:
"@types/eslint" "*"
"@types/estree" "*"
"@types/estree@^1.0.5":
version "1.0.6"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
integrity "sha1-Yo7/7q4gZKG055946B2Ht+X8e1A= sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="
"@types/eslint@*":
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":
"@types/json-schema@^7.0.8":
version "7.0.12"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
......@@ -99,10 +91,10 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.8.tgz#b5dda19adaa473a9bf0ab5cbd8f30ec7d43f5c85"
integrity sha512-0mHckf6D2DiIAzh8fM8f3HQCvMKDpK94YQ0DSVkfWTG9BZleYIWudw9cJxX8oCk9bM+vAkDyujDV6dmKHbvQpg==
"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5":
version "1.11.6"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24"
integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==
"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1":
version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb"
integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==
dependencies:
"@webassemblyjs/helper-numbers" "1.11.6"
"@webassemblyjs/helper-wasm-bytecode" "1.11.6"
......@@ -117,10 +109,10 @@
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768"
integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==
"@webassemblyjs/helper-buffer@1.11.6":
version "1.11.6"
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093"
integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==
"@webassemblyjs/helper-buffer@1.12.1":
version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6"
integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==
"@webassemblyjs/helper-numbers@1.11.6":
version "1.11.6"
......@@ -136,15 +128,15 @@
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9"
integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==
"@webassemblyjs/helper-wasm-section@1.11.6":
version "1.11.6"
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577"
integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==
"@webassemblyjs/helper-wasm-section@1.12.1":
version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf"
integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==
dependencies:
"@webassemblyjs/ast" "1.11.6"
"@webassemblyjs/helper-buffer" "1.11.6"
"@webassemblyjs/ast" "1.12.1"
"@webassemblyjs/helper-buffer" "1.12.1"
"@webassemblyjs/helper-wasm-bytecode" "1.11.6"
"@webassemblyjs/wasm-gen" "1.11.6"
"@webassemblyjs/wasm-gen" "1.12.1"
"@webassemblyjs/ieee754@1.11.6":
version "1.11.6"
......@@ -165,59 +157,59 @@
resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a"
integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==
"@webassemblyjs/wasm-edit@^1.11.5":
version "1.11.6"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab"
integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==
"@webassemblyjs/wasm-edit@^1.12.1":
version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b"
integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==
dependencies:
"@webassemblyjs/ast" "1.11.6"
"@webassemblyjs/helper-buffer" "1.11.6"
"@webassemblyjs/ast" "1.12.1"
"@webassemblyjs/helper-buffer" "1.12.1"
"@webassemblyjs/helper-wasm-bytecode" "1.11.6"
"@webassemblyjs/helper-wasm-section" "1.11.6"
"@webassemblyjs/wasm-gen" "1.11.6"
"@webassemblyjs/wasm-opt" "1.11.6"
"@webassemblyjs/wasm-parser" "1.11.6"
"@webassemblyjs/wast-printer" "1.11.6"
"@webassemblyjs/wasm-gen@1.11.6":
version "1.11.6"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268"
integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==
dependencies:
"@webassemblyjs/ast" "1.11.6"
"@webassemblyjs/helper-wasm-section" "1.12.1"
"@webassemblyjs/wasm-gen" "1.12.1"
"@webassemblyjs/wasm-opt" "1.12.1"
"@webassemblyjs/wasm-parser" "1.12.1"
"@webassemblyjs/wast-printer" "1.12.1"
"@webassemblyjs/wasm-gen@1.12.1":
version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547"
integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==
dependencies:
"@webassemblyjs/ast" "1.12.1"
"@webassemblyjs/helper-wasm-bytecode" "1.11.6"
"@webassemblyjs/ieee754" "1.11.6"
"@webassemblyjs/leb128" "1.11.6"
"@webassemblyjs/utf8" "1.11.6"
"@webassemblyjs/wasm-opt@1.11.6":
version "1.11.6"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2"
integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==
"@webassemblyjs/wasm-opt@1.12.1":
version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5"
integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==
dependencies:
"@webassemblyjs/ast" "1.11.6"
"@webassemblyjs/helper-buffer" "1.11.6"
"@webassemblyjs/wasm-gen" "1.11.6"
"@webassemblyjs/wasm-parser" "1.11.6"
"@webassemblyjs/ast" "1.12.1"
"@webassemblyjs/helper-buffer" "1.12.1"
"@webassemblyjs/wasm-gen" "1.12.1"
"@webassemblyjs/wasm-parser" "1.12.1"
"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5":
version "1.11.6"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1"
integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==
"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1":
version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937"
integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==
dependencies:
"@webassemblyjs/ast" "1.11.6"
"@webassemblyjs/ast" "1.12.1"
"@webassemblyjs/helper-api-error" "1.11.6"
"@webassemblyjs/helper-wasm-bytecode" "1.11.6"
"@webassemblyjs/ieee754" "1.11.6"
"@webassemblyjs/leb128" "1.11.6"
"@webassemblyjs/utf8" "1.11.6"
"@webassemblyjs/wast-printer@1.11.6":
version "1.11.6"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20"
integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==
"@webassemblyjs/wast-printer@1.12.1":
version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac"
integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==
dependencies:
"@webassemblyjs/ast" "1.11.6"
"@webassemblyjs/ast" "1.12.1"
"@xtuc/long" "4.2.2"
"@webpack-cli/configtest@^2.1.1":
......@@ -245,10 +237,10 @@
resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
acorn-import-assertions@^1.9.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac"
integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==
acorn-import-attributes@^1.9.5:
version "1.9.5"
resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef"
integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==
acorn@^8.7.1, acorn@^8.8.2:
version "8.10.0"
......@@ -288,32 +280,32 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
braces@^3.0.2, braces@~3.0.2:
braces@^3.0.3, braces@~3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies:
fill-range "^7.1.1"
browserslist@^4.14.5:
version "4.21.10"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0"
integrity sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==
browserslist@^4.21.10:
version "4.24.2"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580"
integrity "sha1-9YRbyRBp29Ve6J+vmCLh2IXRZYA= sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg=="
dependencies:
caniuse-lite "^1.0.30001517"
electron-to-chromium "^1.4.477"
node-releases "^2.0.13"
update-browserslist-db "^1.0.11"
caniuse-lite "^1.0.30001669"
electron-to-chromium "^1.5.41"
node-releases "^2.0.18"
update-browserslist-db "^1.1.1"
buffer-from@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
caniuse-lite@^1.0.30001517:
version "1.0.30001519"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001519.tgz#3e7b8b8a7077e78b0eb054d69e6edf5c7df35601"
integrity sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg==
caniuse-lite@^1.0.30001669:
version "1.0.30001673"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001673.tgz#5aa291557af1c71340e809987367410aab7a5a9e"
integrity "sha1-WqKRVXrxxxNA6AmYc2dBCqt6Wp4= sha512-WTrjUCSMp3LYX0nE12ECkV0a+e6LC85E0Auz75555/qr78Oc8YWhEPNfDd6SHdtlCMSzqtuXY0uyEMNRcsKpKw=="
chokidar@^3.5.3:
version "3.5.3"
......@@ -400,15 +392,15 @@ dotenv@^16.0.0:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==
electron-to-chromium@^1.4.477:
version "1.4.487"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.487.tgz#e2ef8b15f2791bf68fa6f38f2656f1a551d360ae"
integrity sha512-XbCRs/34l31np/p33m+5tdBrdXu9jJkZxSbNxj5I0H1KtV2ZMSB+i/HYqDiRzHaFx2T5EdytjoBRe8QRJE2vQg==
electron-to-chromium@^1.5.41:
version "1.5.47"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.47.tgz#ef0751bc19b28be8ee44cd8405309de3bf3b20c7"
integrity "sha1-7wdRvBmyi+juRM2EBTCd4787IMc= sha512-zS5Yer0MOYw4rtK2iq43cJagHZ8sXN0jDHDKzB+86gSBSAI4v07S97mcq+Gs2vclAxSh1j7vOAHxSVgduiiuVQ=="
enhanced-resolve@^5.15.0:
version "5.15.0"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35"
integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==
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"
......@@ -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"
integrity sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==
escalade@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
escalade@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
eslint-scope@5.1.1:
version "5.1.1"
......@@ -540,7 +532,7 @@ globby@^11.0.4:
merge2 "^1.4.1"
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"
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==
......@@ -670,11 +662,11 @@ merge2@^1.3.0, merge2@^1.4.1:
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
micromatch@^4.0.4:
version "4.0.5"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
version "4.0.8"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
dependencies:
braces "^3.0.2"
braces "^3.0.3"
picomatch "^2.3.1"
mime-db@1.52.0:
......@@ -704,10 +696,10 @@ neo-async@^2.6.2:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
node-releases@^2.0.13:
version "2.0.13"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d"
integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==
node-releases@^2.0.18:
version "2.0.18"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f"
integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.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"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
picocolors@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity "sha1-PTIa8+q5ObCDyPkpodEs2oHCa2s= sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
version "2.3.1"
......@@ -921,21 +913,21 @@ tapable@^2.1.1, tapable@^2.2.0:
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
terser-webpack-plugin@^5.3.7:
version "5.3.9"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1"
integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==
terser-webpack-plugin@^5.3.10:
version "5.3.10"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199"
integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==
dependencies:
"@jridgewell/trace-mapping" "^0.3.17"
"@jridgewell/trace-mapping" "^0.3.20"
jest-worker "^27.4.5"
schema-utils "^3.1.1"
serialize-javascript "^6.0.1"
terser "^5.16.8"
terser "^5.26.0"
terser@^5.16.8:
version "5.19.2"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.19.2.tgz#bdb8017a9a4a8de4663a7983f45c506534f9234e"
integrity sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==
terser@^5.26.0:
version "5.36.0"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.36.0.tgz#8b0dbed459ac40ff7b4c9fd5a3a2029de105180e"
integrity "sha1-iw2+1FmsQP97TJ/Vo6ICneEFGA4= sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w=="
dependencies:
"@jridgewell/source-map" "^0.3.3"
acorn "^8.8.2"
......@@ -971,13 +963,13 @@ typescript@5.1:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274"
integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==
update-browserslist-db@^1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940"
integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==
update-browserslist-db@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5"
integrity "sha1-gIRvuh156CVH+2YfjRQeCUV1X+U= sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A=="
dependencies:
escalade "^3.1.1"
picocolors "^1.0.0"
escalade "^3.2.0"
picocolors "^1.1.0"
uri-js@^4.2.2:
version "4.4.1"
......@@ -986,10 +978,10 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
watchpack@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
watchpack@^2.4.1:
version "2.4.2"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da"
integrity "sha1-L+6u1nQS58MxhOWnnKc4+9OFZNo= sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw=="
dependencies:
glob-to-regexp "^0.4.1"
graceful-fs "^4.1.2"
......@@ -1027,33 +1019,32 @@ webpack-sources@^3.2.3:
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
webpack@^5.88.2:
version "5.88.2"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.88.2.tgz#f62b4b842f1c6ff580f3fcb2ed4f0b579f4c210e"
integrity sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==
dependencies:
"@types/eslint-scope" "^3.7.3"
"@types/estree" "^1.0.0"
"@webassemblyjs/ast" "^1.11.5"
"@webassemblyjs/wasm-edit" "^1.11.5"
"@webassemblyjs/wasm-parser" "^1.11.5"
version "5.95.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.95.0.tgz#8fd8c454fa60dad186fbe36c400a55848307b4c0"
integrity "sha1-j9jEVPpg2tGG++NsQApVhIMHtMA= sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q=="
dependencies:
"@types/estree" "^1.0.5"
"@webassemblyjs/ast" "^1.12.1"
"@webassemblyjs/wasm-edit" "^1.12.1"
"@webassemblyjs/wasm-parser" "^1.12.1"
acorn "^8.7.1"
acorn-import-assertions "^1.9.0"
browserslist "^4.14.5"
acorn-import-attributes "^1.9.5"
browserslist "^4.21.10"
chrome-trace-event "^1.0.2"
enhanced-resolve "^5.15.0"
enhanced-resolve "^5.17.1"
es-module-lexer "^1.2.1"
eslint-scope "5.1.1"
events "^3.2.0"
glob-to-regexp "^0.4.1"
graceful-fs "^4.2.9"
graceful-fs "^4.2.11"
json-parse-even-better-errors "^2.3.1"
loader-runner "^4.2.0"
mime-types "^2.1.27"
neo-async "^2.6.2"
schema-utils "^3.2.0"
tapable "^2.1.1"
terser-webpack-plugin "^5.3.7"
watchpack "^2.4.0"
terser-webpack-plugin "^5.3.10"
watchpack "^2.4.1"
webpack-sources "^3.2.3"
which@^2.0.1:
......
......@@ -220,6 +220,7 @@ Settings for meta tags, OG tags and SEO
##### Block fields list
| Id | Description |
| --- | --- |
| `base_fee` | Base fee |
| `burnt_fees` | Burnt fees |
| `total_reward` | Total block reward |
| `nonce` | Block nonce |
......@@ -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 |
| --- | --- | --- | --- | --- | --- | --- |
| 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;
......
......@@ -67,6 +67,8 @@ import type {
SmartContract,
SmartContractVerificationConfigRaw,
SmartContractSecurityAudits,
SmartContractMudSystemsResponse,
SmartContractMudSystemInfo,
} from 'types/api/contract';
import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts';
import type {
......@@ -849,6 +851,16 @@ export const RESOURCES = {
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_messages: {
path: '/api/v2/arbitrum/messages/:direction',
......@@ -1277,6 +1289,8 @@ Q extends 'address_mud_tables' ? AddressMudTables :
Q extends 'address_mud_tables_count' ? number :
Q extends 'address_mud_records' ? AddressMudRecords :
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 'withdrawals' ? WithdrawalsResponse :
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 { SolidityScanReport } from 'lib/solidityScan/schema';
import { ADDRESS_PARAMS } from './addressParams';
import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams';
export const CONTRACT_CODE_UNVERIFIED = {
creation_bytecode: '0x60806040526e',
......@@ -46,6 +46,7 @@ export const CONTRACT_CODE_VERIFIED = {
remappings: [],
},
compiler_version: 'v0.8.7+commit.e28d00a7',
constructor_args: '0000000000000000000000005c7bcd6e7de5423a257d81b442095a1a6ced35c5000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
creation_bytecode: '0x6080604052348',
deployed_bytecode: '0x60806040',
evm_version: 'london',
......@@ -98,3 +99,12 @@ export const SOLIDITY_SCAN_REPORT: SolidityScanReport = {
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 } });
{ variant: 'subtle', colorScheme: 'gray', states: [ 'default', 'hovered' ], withDarkMode: true },
{ variant: 'hero', states: [ 'default', 'hovered' ], withDarkMode: true },
{ variant: 'header', states: [ 'default', 'hovered', 'selected' ], withDarkMode: true },
{ variant: 'radio_group', states: [ 'default', 'hovered', 'selected' ], withDarkMode: true },
].forEach(({ variant, colorScheme, withDarkMode, states }) => {
test.describe(`variant ${ variant }${ colorScheme ? ` with ${ colorScheme } color scheme` : '' }${ withDarkMode ? ' +@dark-mode' : '' }`, () => {
test('', async({ render }) => {
......
......@@ -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 outline = runIfFn(variantOutline, props);
......@@ -223,6 +259,7 @@ const variants = {
subtle: variantSubtle,
hero: variantHero,
header: variantHeader,
radio_group: variantRadioGroup,
};
const baseStyle = defineStyle({
......
......@@ -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 = {
sm: definePartsStyle({
tab: Button.sizes?.sm,
......@@ -53,6 +80,7 @@ const sizes = {
const variants = {
'soft-rounded': variantSoftRounded,
outline: variantOutline,
radio_group: variantRadioGroup,
};
const Tabs = defineMultiStyleConfig({
......
......@@ -143,3 +143,19 @@ export type SmartContractSecurityAuditSubmission = {
'audit_publish_date': 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 = {
name: string;
dapp_id?: 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<
{[K in keyof T]: T[K] extends X ? K : never}[keyof T],
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';
export const BLOCK_FIELDS_IDS = [
'base_fee',
'burnt_fees',
'total_reward',
'nonce',
......
......@@ -2,8 +2,8 @@ import { useRouter } from 'next/router';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import useContractTabs from 'lib/hooks/useContractTabs';
import getQueryParamString from 'lib/router/getQueryParamString';
import useContractTabs from 'ui/address/contract/useContractTabs';
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 * 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 * as socketServer from 'playwright/fixtures/socketServer';
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 = {
router: {
......@@ -28,30 +27,72 @@ test.beforeEach(async({ mockApiResponse, page }) => {
addressApiUrl = await mockApiResponse('address', addressMock.contract, { pathParams: { hash: addressMock.contract.hash } });
});
test('full view +@mobile +@dark-mode', async({ render, mockApiResponse, createSocket }) => {
await mockApiResponse('contract', contractMock.withChangedByteCode, { pathParams: { hash: addressMock.contract.hash } });
await mockApiResponse('contract', contractMock.withChangedByteCode, { pathParams: { hash: addressMock.contract.implementations?.[0].address as string } });
test.describe('full view', () => {
test.beforeEach(async({ 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(<ContractCode/>, { hooksConfig }, { withSocket: true });
await createSocket();
test('source code +@dark-mode', async({ render, 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 }) => {
await mockApiResponse('contract', contractMock.verified, { pathParams: { hash: addressMock.contract.hash } });
test('abi', async({ render, createSocket }) => {
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 });
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase());
socketServer.sendMessage(socket, channel, 'changed_bytecode', {});
test('bytecode', async({ render, createSocket }) => {
const hooksConfig = {
router: {
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 }) => {
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 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,
test('verified with multiple sources', async({ render, page, mockApiResponse }) => {
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' });
await expect(section).toHaveScreenshot();
......@@ -76,83 +117,55 @@ test('verified with multiple sources', async({ render, page, mockApiResponse })
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 }) => {
const hooksConfig = {
router: {
query: { hash: addressMock.contract.hash, tab: 'contract_bytecode' },
},
};
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' });
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 }) => {
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();
});
test('zkSync contract', async({ render, mockApiResponse, page, mockEnvs }) => {
await mockEnvs(ENVS_MAP.zkSyncRollup);
await mockApiResponse('contract', contractMock.zkSync, { pathParams: { hash: addressMock.contract.hash } });
await render(<ContractCode/>, { hooksConfig }, { withSocket: true });
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 300 } });
});
test.describe('with audits feature', () => {
test.beforeEach(async({ mockEnvs }) => {
await mockEnvs(ENVS_MAP.hasContractAuditReports);
});
test('no audits', async({ render, mockApiResponse }) => {
await mockApiResponse('contract', contractMock.verified, { pathParams: { hash: addressMock.contract.hash } });
await mockApiResponse('contract_security_audits', { items: [] }, { pathParams: { hash: addressMock.contract.hash } });
const component = await render(<ContractCode/>, { hooksConfig }, { withSocket: true });
await expect(component).toHaveScreenshot();
});
test('implementation info', async({ render, mockApiResponse }) => {
const hooksConfig = {
router: {
query: { hash: addressMock.contract.hash, tab: 'contract_compiler' },
},
};
const implementationName = addressMock.contract.implementations?.[0].name as string;
const implementationAddress = addressMock.contract.implementations?.[0].address as string;
const implementationContract = {
...contractMock.verified,
compiler_settings: {
evmVersion: 'london',
libraries: {},
metadata: {
bytecodeHash: 'ipfs',
useLiteralContent: false,
},
optimizer: {
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 }) => {
await mockApiResponse('contract', contractMock.verified, { pathParams: { hash: addressMock.contract.hash } });
await mockApiResponse('contract_security_audits', contractAudits, { pathParams: { hash: addressMock.contract.hash } });
const component = await render(<ContractCode/>, { hooksConfig }, { withSocket: true });
const component = await render(<ContractDetails/>, { hooksConfig }, { withSocket: true });
await component.getByRole('combobox').selectOption(implementationName);
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 type { AddressImplementation } from 'types/api/addressParams';
import type { SmartContract } from 'types/api/contract';
import { route } from 'nextjs-routes';
import useApiQuery from 'lib/api/useApiQuery';
import * as stubs from 'stubs/contract';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import LinkInternal from 'ui/shared/links/LinkInternal';
import CodeEditor from 'ui/shared/monaco/CodeEditor';
......@@ -38,82 +35,34 @@ function getEditorData(contractInfo: SmartContract | undefined) {
];
}
interface SourceContractOption {
address: string;
label: string;
}
interface Props {
address: string;
implementations?: Array<AddressImplementation>;
data: SmartContract | undefined;
sourceAddress: string;
isLoading?: boolean;
}
export const ContractSourceCode = ({ address, implementations }: 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,
},
});
export const ContractSourceCode = ({ data, isLoading, sourceAddress }: Props) => {
const editorData = React.useMemo(() => {
return getEditorData(contractQuery.data);
}, [ contractQuery.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 ]);
return getEditorData(data);
}, [ data ]);
const heading = (
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>
<span>Contract source code</span>
{ contractQuery.data?.language &&
<Text whiteSpace="pre" as="span" variant="secondary" textTransform="capitalize"> ({ contractQuery.data.language })</Text> }
{ data?.language &&
<Text whiteSpace="pre" as="span" variant="secondary" textTransform="capitalize"> ({ data.language })</Text> }
</Skeleton>
);
const select = options.length > 1 ? (
<Select
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 }/> :
const externalLibraries = data?.external_libraries ?
<ContractExternalLibraries data={ data.external_libraries } isLoading={ isLoading }/> :
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">
<LinkInternal
href={ route({ pathname: '/visualize/sol2uml', query: { address: sourceContract.address } }) }
href={ route({ pathname: '/visualize/sol2uml', query: { address: sourceAddress } }) }
ml={{ base: '0', lg: 'auto' }}
isLoading={ isLoading }
>
......@@ -124,11 +73,11 @@ export const ContractSourceCode = ({ address, implementations }: Props) => {
</Tooltip>
) : 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
text={ contractQuery.data.source_code }
text={ data.source_code }
isLoading={ isLoading }
ml={{ base: 'auto', lg: diagramLink ? '0' : 'auto' }}
/>
......@@ -146,13 +95,13 @@ export const ContractSourceCode = ({ address, implementations }: Props) => {
return (
<CodeEditor
key={ sourceContract.address }
key={ sourceAddress }
data={ editorData }
remappings={ contractQuery.data?.compiler_settings?.remappings }
libraries={ contractQuery.data?.external_libraries ?? undefined }
language={ contractQuery.data?.language ?? undefined }
remappings={ data?.compiler_settings?.remappings }
libraries={ data?.external_libraries ?? undefined }
language={ data?.language ?? undefined }
mainFile={ editorData[0]?.file_path }
contractName={ contractQuery.data?.name ?? undefined }
contractName={ data?.name ?? undefined }
/>
);
})();
......@@ -161,7 +110,6 @@ export const ContractSourceCode = ({ address, implementations }: Props) => {
<section>
<Flex alignItems="center" mb={ 3 } columnGap={ 3 } rowGap={ 2 } flexWrap="wrap">
{ heading }
{ select }
{ externalLibraries }
{ diagramLink }
{ ides }
......
......@@ -2,19 +2,19 @@ import React from 'react';
import { test, expect } from 'playwright/lib';
import ContractCodeProxyPattern from './ContractCodeProxyPattern';
import ContractDetailsAlertProxyPattern from './ContractDetailsAlertProxyPattern';
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();
});
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();
});
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();
});
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';
import FormModal from 'ui/shared/FormModal';
import LinkExternal from 'ui/shared/links/LinkExternal';
import ContractSubmitAuditForm from './contractSubmitAuditForm/ContractSubmitAuditForm';
import ContractSubmitAuditForm from './ContractSubmitAuditForm';
type Props = {
addressHash?: string;
......
import { Button, VStack } from '@chakra-ui/react';
import React from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import { FormProvider, useForm } from 'react-hook-form';
import type { SmartContractSecurityAuditSubmission } from 'types/api/contract';
import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import dayjs from 'lib/date/dayjs';
import useToast from 'lib/hooks/useToast';
import AuditComment from './fields/AuditComment';
import AuditCompanyName from './fields/AuditCompanyName';
import AuditProjectName from './fields/AuditProjectName';
import AuditProjectUrl from './fields/AuditProjectUrl';
import AuditReportDate from './fields/AuditReportDate';
import AuditReportUrl from './fields/AuditReportUrl';
import AuditSubmitterEmail from './fields/AuditSubmitterEmail';
import AuditSubmitterIsOwner from './fields/AuditSubmitterIsOwner';
import AuditSubmitterName from './fields/AuditSubmitterName';
import FormFieldCheckbox from 'ui/shared/forms/fields/FormFieldCheckbox';
import FormFieldEmail from 'ui/shared/forms/fields/FormFieldEmail';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import FormFieldUrl from 'ui/shared/forms/fields/FormFieldUrl';
interface Props {
address?: string;
......@@ -46,10 +41,11 @@ const ContractSubmitAuditForm = ({ address, onSuccess }: Props) => {
const apiFetch = useApiFetch();
const toast = useToast();
const { handleSubmit, formState, control, setError } = useForm<Inputs>({
const formApi = useForm<Inputs>({
mode: 'onTouched',
defaultValues: { is_project_owner: false },
});
const { handleSubmit, formState, setError } = formApi;
const onFormSubmit: SubmitHandler<Inputs> = React.useCallback(async(data) => {
try {
......@@ -94,30 +90,46 @@ const ContractSubmitAuditForm = ({ address, onSuccess }: Props) => {
}, [ apiFetch, address, toast, setError, onSuccess ]);
return (
<form noValidate onSubmit={ handleSubmit(onFormSubmit) } autoComplete="off" ref={ containerRef }>
<VStack gap={ 5 }>
<AuditSubmitterName control={ control }/>
<AuditSubmitterEmail control={ control }/>
<AuditSubmitterIsOwner control={ control }/>
<AuditProjectName control={ control }/>
<AuditProjectUrl control={ control }/>
<AuditCompanyName control={ control }/>
<AuditReportUrl control={ control }/>
<AuditReportDate control={ control }/>
<AuditComment control={ control }/>
</VStack>
<Button
type="submit"
size="lg"
mt={ 8 }
isLoading={ formState.isSubmitting }
loadingText="Send request"
isDisabled={ !formState.isDirty }
>
<FormProvider { ...formApi }>
<form noValidate onSubmit={ handleSubmit(onFormSubmit) } autoComplete="off" ref={ containerRef }>
<VStack gap={ 5 } alignItems="flex-start">
<FormFieldText<Inputs> name="submitter_name" isRequired placeholder="Submitter name"/>
<FormFieldEmail<Inputs> name="submitter_email" isRequired placeholder="Submitter email"/>
<FormFieldCheckbox<Inputs, 'is_project_owner'>
name="is_project_owner"
label="I'm the contract owner"
/>
<FormFieldText<Inputs> name="project_name" isRequired placeholder="Project name"/>
<FormFieldUrl<Inputs> name="project_url" isRequired placeholder="Project URL"/>
<FormFieldText<Inputs> name="audit_company_name" isRequired placeholder="Audit company name"/>
<FormFieldUrl<Inputs> name="audit_report_url" isRequired placeholder="Audit report URL"/>
<FormFieldText<Inputs>
name="audit_publish_date"
type="date"
max={ dayjs().format('YYYY-MM-DD') }
isRequired
placeholder="Audit publish date"
/>
<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
</Button>
</form>
</Button>
</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 {
abi: Array<SmartContractMethod>;
addressHash: 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 [ id, setId ] = React.useState(0);
......@@ -61,6 +62,7 @@ const ContractAbi = ({ abi, addressHash, tab }: Props) => {
id={ id }
index={ index }
addressHash={ addressHash }
sourceAddress={ sourceAddress }
tab={ tab }
onSubmit={ handleFormSubmit }
/>
......
......@@ -19,11 +19,12 @@ interface Props {
index: number;
id: number;
addressHash: string;
sourceAddress?: string;
tab: string;
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 url = React.useMemo(() => {
......@@ -36,10 +37,11 @@ const ContractAbiItem = ({ data, index, id, addressHash, tab, onSubmit }: Props)
query: {
hash: addressHash ?? '',
tab,
...(sourceAddress ? { source_address: sourceAddress } : {}),
},
hash: data.method_id,
});
}, [ addressHash, data, tab ]);
}, [ addressHash, data, tab, sourceAddress ]);
const handleCopyLinkClick = React.useCallback((event: React.MouseEvent) => {
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 {
isLoading?: boolean;
isError?: boolean;
type: MethodType;
sourceAddress?: string;
}
const ContractMethods = ({ abi, isLoading, isError, type }: Props) => {
const ContractMethods = ({ abi, isLoading, isError, type, sourceAddress }: Props) => {
const router = useRouter();
......@@ -32,10 +33,11 @@ const ContractMethods = ({ abi, isLoading, isError, type }: Props) => {
}
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);
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 { useRouter } from 'next/router';
import React from 'react';
import type { MethodType } from './types';
import type { AddressImplementation } from 'types/api/addressParams';
import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import ContractSourceAddressSelector from '../ContractSourceAddressSelector';
import ContractConnectWallet from './ContractConnectWallet';
import ContractImplementationAddress from './ContractImplementationAddress';
import ContractMethods from './ContractMethods';
import { isReadMethod, isWriteMethod } from './utils';
import { enrichWithMethodId, isReadMethod, isWriteMethod } from './utils';
interface Props {
type: MethodType;
......@@ -18,8 +20,10 @@ interface 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', {
pathParams: { hash: selectedItem.address },
......@@ -29,29 +33,25 @@ const ContractMethodsProxy = ({ type, implementations, isLoading: isInitialLoadi
},
});
const handleItemSelect = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
const nextOption = implementations.find(({ address }) => address === event.target.value);
if (nextOption) {
setSelectedItem(nextOption);
}
}, [ implementations ]);
const abi = contractQuery.data?.abi?.filter(type === 'read' ? isReadMethod : isWriteMethod) || [];
const abi = contractQuery.data?.abi?.filter(type === 'read' ? isReadMethod : isWriteMethod).map(enrichWithMethodId) || [];
return (
<Box>
<ContractConnectWallet isLoading={ isInitialLoading }/>
<ContractImplementationAddress
implementations={ implementations }
<ContractSourceAddressSelector
items={ implementations }
selectedItem={ selectedItem }
onItemSelect={ handleItemSelect }
onItemSelect={ setSelectedItem }
isLoading={ isInitialLoading }
label="Implementation address"
mb={ 6 }
/>
<ContractMethods
key={ selectedItem.address }
abi={ abi }
isLoading={ isInitialLoading || contractQuery.isPending }
isError={ contractQuery.isError }
sourceAddress={ selectedItem.address }
type={ type }
/>
</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 { useController, useFormContext } from 'react-hook-form';
import { NumericFormat } from 'react-number-format';
import type { ContractAbiItemInput } from '../types';
import { HOUR, SECOND } from 'lib/consts';
import ClearButton from 'ui/shared/ClearButton';
import ContractMethodAddressButton from './ContractMethodAddressButton';
import ContractMethodFieldLabel from './ContractMethodFieldLabel';
import ContractMethodMultiplyButton from './ContractMethodMultiplyButton';
import useFormatFieldValue from './useFormatFieldValue';
import useValidateField from './useValidateField';
import { matchInt } from './utils';
const TIMESTAMP_BUTTON_REGEXP = /time|deadline|expiration|expiry/i;
interface Props {
data: ContractAbiItemInput;
hideLabel?: boolean;
......@@ -30,6 +34,7 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi
const isOptional = isOptionalProp || isNativeCoin;
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 format = useFormatFieldValue({ argType: data.type, argTypeMatchInt });
......@@ -56,9 +61,28 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi
const zeroes = Array(power).fill('0').join('');
const value = getValues(name);
const newValue = format(value ? value + zeroes : '1' + zeroes);
setValue(name, newValue);
setValue(name, newValue, { shouldValidate: true });
}, [ 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;
return (
......@@ -90,11 +114,40 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi
isInvalid={ Boolean(error) }
placeholder={ data.type }
autoComplete="off"
data-1p-ignore
bgColor={ inputBgColor }
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 }/> }
{ 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 }/> }
</InputRightElement>
</InputGroup>
......
......@@ -62,7 +62,7 @@ const data: SmartContractMethod = {
// LITERALS
{ internalType: 'bytes32', name: 'fulfillerConduitKey', type: 'bytes32' },
{ internalType: 'address', name: 'recipient', type: 'address' },
{ internalType: 'uint256', name: 'maximumFulfilled', type: 'uint256' },
{ internalType: 'uint256', name: 'startTime', type: 'uint256' },
{ internalType: 'int8[]', name: 'criteriaProof', type: 'int8[]' },
],
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 type { SubmitHandler } 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 config from 'configs/app';
import { SECOND } from 'lib/consts';
import * as mixpanel from 'lib/mixpanel/index';
import IconSvg from 'ui/shared/IconSvg';
......@@ -43,17 +44,42 @@ const ContractMethodForm = ({ data, attempt, onSubmit, onReset, isOpen }: Props)
shouldUnregister: true,
});
const calldataButtonTooltip = useDisclosure();
const handleButtonClick = React.useCallback((event: React.MouseEvent) => {
const callStrategy = event?.currentTarget.getAttribute('data-call-strategy');
setCallStrategy(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 onFormSubmit: SubmitHandler<ContractMethodFormFields> = React.useCallback(async(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);
setLoading(true);
......@@ -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 (
<Box>
<FormProvider { ...formApi }>
......@@ -214,6 +284,7 @@ const ContractMethodForm = ({ data, attempt, onSubmit, onReset, isOpen }: Props)
</Flex>
{ secondaryButton }
{ primaryButton }
{ copyCallDataButton }
{ result && !isLoading && (
<Button
variant="simple"
......@@ -223,7 +294,7 @@ const ContractMethodForm = ({ data, attempt, onSubmit, onReset, isOpen }: Props)
ml={ 1 }
>
<IconSvg name="repeat" boxSize={ 5 } mr={ 1 }/>
Reset
Reset
</Button>
) }
</chakra.form>
......
......@@ -9,6 +9,7 @@ import {
ListItem,
useDisclosure,
Input,
useColorModeValue,
} from '@chakra-ui/react';
import React from 'react';
......@@ -26,6 +27,8 @@ const ContractMethodMultiplyButton = ({ onClick, isDisabled }: Props) => {
const [ customValue, setCustomValue ] = React.useState<number>();
const { isOpen, onToggle, onClose } = useDisclosure();
const dividerColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const handleOptionClick = React.useCallback((event: React.MouseEvent) => {
const id = Number((event.currentTarget as HTMLDivElement).getAttribute('data-id'));
if (!Object.is(id, NaN)) {
......@@ -60,6 +63,8 @@ const ContractMethodMultiplyButton = ({ onClick, isDisabled }: Props) => {
display="inline"
onClick={ handleButtonClick }
isDisabled={ isDisabled }
borderBottomRightRadius={ 0 }
borderTopRightRadius={ 0 }
>
{ times }
<chakra.span>10</chakra.span>
......@@ -73,11 +78,14 @@ const ContractMethodMultiplyButton = ({ onClick, isDisabled }: Props) => {
colorScheme="gray"
size="xs"
cursor="pointer"
ml={ 1 }
p={ 0 }
onClick={ onToggle }
isActive={ isOpen }
isDisabled={ isDisabled }
borderBottomLeftRadius={ 0 }
borderTopLeftRadius={ 0 }
borderLeftWidth="1px"
borderLeftColor={ dividerColor }
>
<IconSvg
name="arrows/east-mini"
......
......@@ -2,8 +2,8 @@ import type { AbiFunction, AbiFallback, AbiReceive } from 'abitype';
export type ContractAbiItemInput = AbiFunction['inputs'][number] & { fieldType?: 'native_coin' };
export type MethodType = 'read' | 'write';
export type MethodCallStrategy = 'read' | 'write' | 'simulate';
export type MethodType = 'read' | 'write' | 'all';
export type MethodCallStrategy = 'read' | 'write' | 'simulate' | 'copy_calldata';
export type ResultViewMode = 'preview' | 'result';
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 { toFunctionSelector } from 'viem';
import type { SmartContractMethodCustomFields, SmartContractMethodRead, SmartContractMethodWrite } from './types';
import type { SmartContractMethod, SmartContractMethodRead, SmartContractMethodWrite } from './types';
export const getNativeCoinValue = (value: unknown) => {
if (typeof value !== 'string') {
......@@ -17,6 +17,9 @@ interface DividedAbi {
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 =>
method.type === 'function' && (
method.constant || method.stateMutability === 'view' || method.stateMutability === 'pure'
......@@ -26,13 +29,19 @@ export const isWriteMethod = (method: Abi[number]): method is SmartContractMetho
(method.type === 'function' || method.type === 'fallback' || method.type === 'receive') &&
!isReadMethod(method);
const enrichWithMethodId = (method: AbiFunction): SmartContractMethodCustomFields => {
export const enrichWithMethodId = (method: AbiFunction | AbiFallback | AbiReceive): SmartContractMethod => {
if (method.type !== 'function') {
return method;
}
try {
return {
...method,
method_id: toFunctionSelector(method).slice(2),
};
} catch (error) {
return {
...method,
is_invalid: true,
};
}
......@@ -42,22 +51,9 @@ export function divideAbiIntoMethodTypes(abi: Abi): DividedAbi {
return {
read: abi
.filter(isReadMethod)
.map((method) => ({
...method,
...enrichWithMethodId(method),
})),
.map(enrichWithMethodId) as Array<SmartContractMethodRead>,
write: abi
.filter(isWriteMethod)
.map((method) => {
if (method.type !== 'function') {
return method;
}
return {
...method,
...enrichWithMethodId(method),
};
}),
.map(enrichWithMethodId) as Array<SmartContractMethodWrite>,
};
}
import { useRouter } from 'next/router';
import useApiQuery from 'lib/api/useApiQuery';
import useContractTabs from 'lib/hooks/useContractTabs';
import getQueryParamString from 'lib/router/getQueryParamString';
const ContractCode = () => {
import useContractTabs from '../useContractTabs';
const ContractDetails = () => {
const router = useRouter();
const hash = getQueryParamString(router.query.hash);
const addressQuery = useApiQuery('address', { pathParams: { hash } });
......@@ -13,4 +14,4 @@ const ContractCode = () => {
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';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import * as stubs from 'stubs/contract';
import ContractCode from 'ui/address/contract/ContractCode';
import ContractDetails from 'ui/address/contract/ContractDetails';
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 ContractMethodsRegular from 'ui/address/contract/methods/ContractMethodsRegular';
import { divideAbiIntoMethodTypes } from 'ui/address/contract/methods/utils';
import ContentLoader from 'ui/shared/ContentLoader';
const CONTRACT_TAB_IDS = [
'contract_code',
'read_contract',
'read_contract_rpc',
'read_proxy',
'read_custom_methods',
'write_contract',
'write_contract_rpc',
'write_proxy',
'write_custom_methods',
] as const;
import type { CONTRACT_MAIN_TAB_IDS } from './utils';
import { CONTRACT_DETAILS_TAB_IDS, CONTRACT_TAB_IDS } from './utils';
interface ContractTab {
id: typeof CONTRACT_TAB_IDS[number];
id: typeof CONTRACT_MAIN_TAB_IDS[number];
title: string;
component: JSX.Element;
subTabs?: Array<string>;
}
interface ReturnType {
......@@ -37,7 +31,7 @@ interface ReturnType {
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 router = useRouter();
......@@ -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({
topic: `addresses:${ data?.hash?.toLowerCase() }`,
isDisabled: !isEnabled,
......@@ -89,10 +92,11 @@ export default function useContractTabs(data: Address | undefined, isPlaceholder
return React.useMemo(() => {
return {
tabs: [
{
data?.hash && {
id: 'contract_code' as const,
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 && {
id: 'read_contract' as const,
......@@ -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),
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 }) => {
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([
[
'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');
......@@ -36,9 +36,23 @@ test('with multichain button internal +@dark-mode', async({ render, mockEnvs, mo
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([
[ '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');
......@@ -51,7 +65,7 @@ test('with multichain button internal small screen', async({ render, mockEnvs, m
await mockEnvs([
[
'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');
......
import { Image, Skeleton, Text, Flex } from '@chakra-ui/react';
import _capitalize from 'lodash/capitalize';
import { Skeleton, Text, Flex } from '@chakra-ui/react';
import React from 'react';
import type { Address } from 'types/api/address';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import getCurrencyValue from 'lib/getCurrencyValue';
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 { getTokensTotalInfo } from '../utils/tokenUtils';
import useFetchTokens from '../utils/useFetchTokens';
const TEMPLATE_ADDRESS = '{address}';
import AddressMultichainButton from './AddressMultichainButton';
const multichainFeature = config.features.multichainButton;
......@@ -46,58 +40,30 @@ const AddressNetWorth = ({ addressData, isLoading, addressHash }: Props) => {
mixpanel.logEvent(mixpanel.EventTypes.BUTTON_CLICK, { Content: 'Multichain', Source: 'address' });
}, []);
let multichainItem = null;
let multichainItems = null;
if (multichainFeature.isEnabled && !addressData?.is_contract) {
const buttonContent = (
const { providers } = multichainFeature;
const hasSingleProvider = providers.length === 1;
multichainItems = (
<>
{ multichainFeature.logoUrl &&
<Image src={ multichainFeature.logoUrl } alt={ multichainFeature.name } boxSize={ 5 } mr={ 2 } borderRadius="4px" overflow="hidden"/>
}
{ _capitalize(multichainFeature.name) }</>
<TextSeparator mx={ 0 } color="gray.500"/>
<Flex alignItems="center" gap={ 2 }>
<Text>Multichain</Text>
{ 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 (
......@@ -105,7 +71,7 @@ const AddressNetWorth = ({ addressData, isLoading, addressHash }: Props) => {
<Text>
{ (isError || !addressData?.exchange_rate) ? 'N/A' : `${ prefix }$${ totalUsd.toFormat(2) }` }
</Text>
{ multichainItem }
{ multichainItems }
</Skeleton>
);
};
......
......@@ -77,7 +77,7 @@ const AddressSaveOnGas = ({ gasUsed, address }: Props) => {
<TextSeparator color="divider"/>
<Skeleton isLoaded={ !query.isPlaceholderData } display="flex" alignItems="center" columnGap={ 2 }>
<Image src="/static/gas_hawk_logo.svg" w="15px" h="20px" alt="GasHawk logo"/>
<LinkExternal href="https://www.gashawk.io" fontSize="sm">
<LinkExternal href="https://www.gashawk.io?utm_source=blockscout&utm_medium=address" fontSize="sm">
Save { percent.toLocaleString(undefined, { maximumFractionDigits: 0 }) }% with GasHawk
</LinkExternal>
</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 React from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import { FormProvider, useForm } from 'react-hook-form';
import type {
AddressVerificationResponseError,
......@@ -16,10 +16,10 @@ import { route } from 'nextjs-routes';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import FormFieldAddress from 'ui/shared/forms/fields/FormFieldAddress';
import LinkInternal from 'ui/shared/links/LinkInternal';
import AdminSupportText from 'ui/shared/texts/AdminSupportText';
import AddressVerificationFieldAddress from '../fields/AddressVerificationFieldAddress';
type Fields = RootFields & AddressVerificationFormFirstStepFields;
interface Props {
......@@ -34,7 +34,7 @@ const AddressVerificationStepAddress = ({ defaultAddress, onContinue }: Props) =
address: defaultAddress,
},
});
const { handleSubmit, formState, control, setError, clearErrors, watch } = formApi;
const { handleSubmit, formState, setError, clearErrors, watch } = formApi;
const apiFetch = useApiFetch();
const address = watch('address');
......@@ -100,17 +100,25 @@ const AddressVerificationStepAddress = ({ defaultAddress, onContinue }: Props) =
})();
return (
<form noValidate onSubmit={ onSubmit }>
<Box>Enter the contract address you are verifying ownership for.</Box>
{ rootError && <Alert status="warning" mt={ 3 }>{ rootError }</Alert> }
<AddressVerificationFieldAddress formState={ formState } control={ control }/>
<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 }>
<FormProvider { ...formApi }>
<form noValidate onSubmit={ onSubmit }>
<Box>Enter the contract address you are verifying ownership for.</Box>
{ rootError && <Alert status="warning" mt={ 3 }>{ rootError }</Alert> }
<FormFieldAddress<Fields>
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
</Button>
<AdminSupportText/>
</Flex>
</form>
</Button>
<AdminSupportText/>
</Flex>
</form>
</FormProvider>
);
};
......
......@@ -2,7 +2,7 @@ import { Alert, Box, Button, chakra, Flex, Link, Radio, RadioGroup } from '@chak
import { useWeb3Modal } from '@web3modal/wagmi/react';
import React from 'react';
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 type {
......@@ -19,11 +19,10 @@ import config from 'configs/app';
import useApiFetch from 'lib/api/useApiFetch';
import shortenString from 'lib/shortenString';
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 AddressVerificationFieldMessage from '../fields/AddressVerificationFieldMessage';
import AddressVerificationFieldSignature from '../fields/AddressVerificationFieldSignature';
type Fields = RootFields & AddressVerificationFormSecondStepFields;
type SignMethod = 'wallet' | 'manual';
......@@ -45,7 +44,7 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
message: signingMessage,
},
});
const { handleSubmit, formState, control, setValue, getValues, setError, clearErrors, watch } = formApi;
const { handleSubmit, formState, setValue, getValues, setError, clearErrors, watch } = formApi;
const apiFetch = useApiFetch();
......@@ -184,51 +183,69 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
})();
return (
<form noValidate onSubmit={ onSubmit }>
{ rootError && <Alert status="warning" mb={ 6 }>{ rootError }</Alert> }
<Box mb={ 8 }>
<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">
<FormProvider { ...formApi }>
<form noValidate onSubmit={ onSubmit }>
{ rootError && <Alert status="warning" mb={ 6 }>{ rootError }</Alert> }
<Box mb={ 8 }>
<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
</Link>
<span>. If you do not see your address here but are sure that you are the owner of the contract, kindly </span>
{ contactUsLink }
<span> for further assistance.</span>
</Box>
{ (contractOwner || contractCreator) && (
<Flex flexDir="column" rowGap={ 4 } mb={ 4 }>
{ contractCreator && (
<Box>
<chakra.span fontWeight={ 600 }>Contract creator: </chakra.span>
<chakra.span>{ contractCreator }</chakra.span>
</Box>
</Link>
<span>. If you do not see your address here but are sure that you are the owner of the contract, kindly </span>
{ contactUsLink }
<span> for further assistance.</span>
</Box>
{ (contractOwner || contractCreator) && (
<Flex flexDir="column" rowGap={ 4 } mb={ 4 }>
{ contractCreator && (
<Box>
<chakra.span fontWeight={ 600 }>Contract creator: </chakra.span>
<chakra.span>{ contractCreator }</chakra.span>
</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 && (
<Box>
<chakra.span fontWeight={ 600 }>Contract owner: </chakra.span>
<chakra.span>{ contractOwner }</chakra.span>
</Box>
{ signMethod === 'manual' && (
<FormFieldText<Fields>
name="signature"
placeholder="Signature hash"
isRequired
rules={{ pattern: SIGNATURE_REGEXP }}
bgColor="dialog_bg"
/>
) }
</Flex>
) }
<Flex rowGap={ 5 } flexDir="column">
<div>
<CopyToClipboard text={ signingMessage } ml="auto" display="block"/>
<AddressVerificationFieldMessage formState={ formState } control={ control }/>
</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>
) }
{ 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>
<Flex alignItems={{ base: 'flex-start', lg: 'center' }} mt={ 8 } columnGap={ 5 } rowGap={ 2 } flexDir={{ base: 'column', lg: 'row' }}>
{ button }
<AdminSupportText/>
</Flex>
</form>
</FormProvider>
);
};
......
import {
Box,
Button,
FormControl,
FormLabel,
Input,
} from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react';
import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form';
import type { SubmitHandler } from 'react-hook-form';
import { useForm, FormProvider } from 'react-hook-form';
import type { ApiKey, ApiKeys, ApiKeyErrors } from 'types/api/account';
......@@ -16,7 +13,7 @@ import type { ResourceErrorAccount } from 'lib/api/resources';
import { resourceKey } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import getErrorMessage from 'lib/getErrorMessage';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
type Props = {
data?: ApiKey;
......@@ -32,7 +29,7 @@ type Inputs = {
const NAME_MAX_LENGTH = 255;
const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const { control, handleSubmit, formState: { errors, isDirty }, setError } = useForm<Inputs>({
const formApi = useForm<Inputs>({
mode: 'onTouched',
defaultValues: {
token: data?.api_key || '',
......@@ -81,80 +78,54 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
onError: (error: ResourceErrorAccount<ApiKeyErrors>) => {
const errorMap = error.payload?.errors;
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) {
setError('name', { type: 'custom', message: getErrorMessage(errorMap, 'identity_id') });
formApi.setError('name', { type: 'custom', message: getErrorMessage(errorMap, 'identity_id') });
} else {
setAlertVisible(true);
}
},
});
const onSubmit: SubmitHandler<Inputs> = useCallback((data) => {
const onSubmit: SubmitHandler<Inputs> = useCallback(async(data) => {
setAlertVisible(false);
mutation.mutate(data);
await mutation.mutateAsync(data);
}, [ 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 (
<form noValidate onSubmit={ handleSubmit(onSubmit) }>
{ data && (
<Box marginBottom={ 5 }>
<Controller
<FormProvider { ...formApi }>
<form noValidate onSubmit={ formApi.handleSubmit(onSubmit) }>
{ data && (
<FormFieldText<Inputs>
name="token"
control={ control }
render={ renderTokenInput }
placeholder="Auto-generated API key token"
isReadOnly
bgColor="dialog_bg"
mb={ 5 }
/>
</Box>
) }
<Box marginBottom={ 8 }>
<Controller
) }
<FormFieldText<Inputs>
name="name"
control={ control }
placeholder="Application name for API key (e.g Web3 project)"
isRequired
rules={{
maxLength: NAME_MAX_LENGTH,
required: true,
}}
render={ renderNameInput }
bgColor="dialog_bg"
mb={ 8 }
/>
</Box>
<Box marginTop={ 8 }>
<Button
size="lg"
type="submit"
isDisabled={ !isDirty }
isLoading={ mutation.isPending }
>
{ data ? 'Save' : 'Generate API key' }
</Button>
</Box>
</form>
<Box marginTop={ 8 }>
<Button
size="lg"
type="submit"
isDisabled={ !formApi.formState.isDirty }
isLoading={ mutation.isPending }
>
{ data ? 'Save' : 'Generate API key' }
</Button>
</Box>
</form>
</FormProvider>
);
};
......
......@@ -21,6 +21,8 @@ import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
import Utilization from 'ui/shared/Utilization/Utilization';
import { getBaseFeeValue } from './utils';
interface Props {
data: Block;
isLoading?: boolean;
......@@ -33,6 +35,7 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
const totalReward = getBlockTotalReward(data);
const burntFees = BigNumber(data.burnt_fees || 0);
const txFees = BigNumber(data.tx_fees || 0);
const baseFeeValue = getBaseFeeValue(data.base_fee_per_gas);
return (
<ListItemMobile rowGap={ 3 } key={ String(data.height) } isAnimated>
......@@ -124,6 +127,14 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
</Flex>
</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>
);
};
......
......@@ -53,6 +53,8 @@ const BlocksTable = ({ data, isLoading, top, page, showSocketInfo, socketInfoNum
<Th width={ `${ REWARD_COL_WEIGHT / widthBase * 100 }%` }>Reward { currencyUnits.ether }</Th> }
{ !isRollup && !config.UI.views.block.hiddenFields?.burnt_fees &&
<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>
</Thead>
<Tbody>
......
......@@ -18,6 +18,8 @@ import LinkInternal from 'ui/shared/links/LinkInternal';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
import Utilization from 'ui/shared/Utilization/Utilization';
import { getBaseFeeValue } from './utils';
interface Props {
data: Block;
isLoading?: boolean;
......@@ -33,6 +35,8 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
const burntFeesIconColor = useColorModeValue('gray.500', 'inherit');
const baseFeeValue = getBaseFeeValue(data.base_fee_per_gas);
return (
<Tr
as={ motion.tr }
......@@ -129,6 +133,13 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
</Tooltip>
</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>
);
};
......
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
mode: 'onBlur',
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 methodNameRef = React.useRef<string>();
......@@ -145,7 +145,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
topic: `addresses:${ address?.toLowerCase() }`,
onSocketClose: handleSocketError,
onSocketError: handleSocketError,
isDisabled: Boolean(address && addressState.error),
isDisabled: !address || Boolean(address && addressState.error),
});
useSocketMessage({
channel,
......@@ -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)' }}>
{ !hash && <ContractVerificationFieldAddress/> }
<ContractVerificationFieldLicenseType/>
<ContractVerificationFieldMethod
control={ control }
methods={ config.verification_options }
isDisabled={ formState.isSubmitting }
/>
<ContractVerificationFieldMethod methods={ config.verification_options }/>
</Grid>
{ content }
{ 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 type { ControllerRenderProps } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types';
import { ADDRESS_REGEXP, ADDRESS_LENGTH } from 'lib/validations/address';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import FormFieldAddress from 'ui/shared/forms/fields/FormFieldAddress';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
......@@ -15,26 +12,6 @@ interface 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 (
<>
<ContractVerificationFormRow>
......@@ -43,11 +20,12 @@ const ContractVerificationFieldAddress = ({ isReadOnly }: Props) => {
</chakra.span>
</ContractVerificationFormRow>
<ContractVerificationFormRow>
<Controller
<FormFieldAddress<FormFields>
name="address"
control={ control }
render={ renderControl }
rules={{ required: true, pattern: ADDRESS_REGEXP }}
isRequired
placeholder="Smart contract / Address (0x...)"
isReadOnly={ isReadOnly }
size={{ base: 'md', lg: 'lg' }}
/>
</ContractVerificationFormRow>
</>
......
import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import { useFormContext } from 'react-hook-form';
import type { FormFields } from '../types';
import CheckboxInput from 'ui/shared/CheckboxInput';
import FormFieldCheckbox from 'ui/shared/forms/fields/FormFieldCheckbox';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
import ContractVerificationFieldConstructorArgs from './ContractVerificationFieldConstructorArgs';
const ContractVerificationFieldAutodetectArgs = () => {
const [ isOn, setIsOn ] = React.useState(true);
const { formState, control, resetField } = useFormContext<FormFields>();
const { resetField } = useFormContext<FormFields>();
const handleCheckboxChange = React.useCallback(() => {
!isOn && resetField('constructor_args');
setIsOn(prev => !prev);
}, [ 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 (
<>
<ContractVerificationFormRow>
<Controller
<FormFieldCheckbox<FormFields, 'autodetect_constructor_args'>
name="autodetect_constructor_args"
control={ control }
render={ renderControl }
label="Try to fetch constructor arguments automatically"
onChange={ handleCheckboxChange }
/>
</ContractVerificationFormRow>
{ !isOn && <ContractVerificationFieldConstructorArgs/> }
......
import { FormControl, Textarea } from '@chakra-ui/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 FieldError from 'ui/shared/forms/FieldError';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
......@@ -15,32 +11,14 @@ interface 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 (
<ContractVerificationFormRow>
<Controller
<FormFieldText<FormFields>
name="code"
control={ control }
render={ renderControl }
rules={{ required: true }}
isRequired
placeholder="Contract code"
size={{ base: 'md', lg: 'lg' }}
asComponent="Textarea"
/>
{ isVyper ? null : (
<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 { useQueryClient } from '@tanstack/react-query';
import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
import { useFormContext } from 'react-hook-form';
import type { FormFields } from '../types';
import type { SmartContractVerificationConfig } from 'types/client/contract';
import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
import IconSvg from 'ui/shared/IconSvg';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
......@@ -22,8 +20,7 @@ interface Props {
const ContractVerificationFieldCompiler = ({ isVyper }: Props) => {
const [ isNightly, setIsNightly ] = React.useState(false);
const { formState, control, getValues, resetField } = useFormContext<FormFields>();
const isMobile = useIsMobile();
const { formState, getValues, resetField } = useFormContext<FormFields>();
const queryClient = useQueryClient();
const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config'));
......@@ -46,25 +43,6 @@ const ContractVerificationFieldCompiler = ({ isVyper }: Props) => {
.slice(0, OPTIONS_LIMIT);
}, [ 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 (
<ContractVerificationFormRow>
<>
......@@ -78,11 +56,14 @@ const ContractVerificationFieldCompiler = ({ isVyper }: Props) => {
Include nightly builds
</Checkbox>
) }
<Controller
<FormFieldFancySelect<FormFields, 'compiler'>
name="compiler"
control={ control }
render={ renderControl }
rules={{ required: true }}
placeholder="Compiler (enter version or use the dropdown)"
loadOptions={ loadOptions }
defaultOptions
placeholderIcon={ <IconSvg name="search"/> }
isRequired
isAsync
/>
</>
{ isVyper ? null : (
......
import { FormControl, Link, Textarea } from '@chakra-ui/react';
import { Link } from '@chakra-ui/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 FieldError from 'ui/shared/forms/FieldError';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
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 (
<ContractVerificationFormRow>
<Controller
<FormFieldText<FormFields>
name="constructor_args"
control={ control }
render={ renderControl }
rules={{ required: true }}
isRequired
rules={{ maxLength: 255 }}
placeholder="ABI-encoded Constructor Arguments"
size={{ base: 'md', lg: 'lg' }}
asComponent="Textarea"
/>
<>
<span>Add arguments in </span>
......
import { useUpdateEffect } from '@chakra-ui/react';
import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
import { useFormContext } from 'react-hook-form';
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 FancySelect from 'ui/shared/FancySelect/FancySelect';
import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
......@@ -15,8 +13,7 @@ const SOURCIFY_ERROR_REGEXP = /\(([^()]*)\)/;
const ContractVerificationFieldContractIndex = () => {
const [ options, setOptions ] = React.useState<Array<Option>>([]);
const { formState, control, watch } = useFormContext<FormFields>();
const isMobile = useIsMobile();
const { formState, watch } = useFormContext<FormFields>();
const sources = watch('sources');
const sourcesError = 'sources' in formState.errors ? formState.errors.sources?.message : undefined;
......@@ -40,34 +37,18 @@ const ContractVerificationFieldContractIndex = () => {
setOptions([]);
}, [ 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) {
return null;
}
return (
<ContractVerificationFormRow>
<Controller
<FormFieldFancySelect<FormFields, 'contract_index'>
name="contract_index"
control={ control }
render={ renderControl }
rules={{ required: true }}
placeholder="Contract name"
options={ options }
isRequired
isAsync={ false }
/>
</ContractVerificationFormRow>
);
......
import { Link } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
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 { SmartContractVerificationConfig } from 'types/client/contract';
import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
......@@ -18,8 +15,6 @@ interface Props {
}
const ContractVerificationFieldEvmVersion = ({ isVyper }: Props) => {
const { formState, control } = useFormContext<FormFields>();
const isMobile = useIsMobile();
const queryClient = useQueryClient();
const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config'));
......@@ -27,29 +22,13 @@ const ContractVerificationFieldEvmVersion = ({ isVyper }: Props) => {
(isVyper ? config?.vyper_evm_versions : config?.solidity_evm_versions)?.map((option) => ({ label: option, value: option })) || []
), [ 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 (
<ContractVerificationFormRow>
<Controller
<FormFieldFancySelect<FormFields, 'evm_version'>
name="evm_version"
control={ control }
render={ renderControl }
rules={{ required: true }}
placeholder="EVM Version"
options={ options }
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>
......
import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types';
import CheckboxInput from 'ui/shared/CheckboxInput';
import FormFieldCheckbox from 'ui/shared/forms/fields/FormFieldCheckbox';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
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 (
<ContractVerificationFormRow>
<Controller
<FormFieldCheckbox<FormFields, 'is_yul'>
name="is_yul"
control={ control }
render={ renderControl }
label="Is Yul contract"
/>
</ContractVerificationFormRow>
);
......
......@@ -56,11 +56,9 @@ const ContractVerificationFieldLibraries = () => {
<ContractVerificationFieldLibraryItem
key={ field.id }
index={ index }
control={ control }
fieldsLength={ fields.length }
onAddFieldClick={ handleAddFieldClick }
onRemoveFieldClick={ handleRemoveFieldClick }
error={ 'libraries' in formState.errors ? formState.errors.libraries?.[index] : undefined }
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 type { Control, ControllerRenderProps, FieldError } from 'react-hook-form';
import { Controller } from 'react-hook-form';
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 InputPlaceholder from 'ui/shared/InputPlaceholder';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
const LIMIT = 10;
interface Props {
control: Control<FormFields>;
index: number;
fieldsLength: number;
error?: {
name?: FieldError;
address?: FieldError;
};
onAddFieldClick: (index: number) => void;
onRemoveFieldClick: (index: number) => void;
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 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(() => {
onAddFieldClick(index);
}, [ index, onAddFieldClick ]);
......@@ -104,11 +66,12 @@ const ContractVerificationFieldLibraryItem = ({ control, index, fieldsLength, on
</Flex>
</ContractVerificationFormRow>
<ContractVerificationFormRow>
<Controller
<FormFieldText<FormFields, `libraries.${ number }.name`>
name={ `libraries.${ index }.name` }
control={ control }
render={ renderNameControl }
rules={{ required: true }}
isRequired
rules={{ maxLength: 255 }}
placeholder="Library name (.sol file)"
size={{ base: 'md', lg: 'lg' }}
/>
{ index === 0 ? (
<>
......@@ -117,11 +80,11 @@ const ContractVerificationFieldLibraryItem = ({ control, index, fieldsLength, on
) : null }
</ContractVerificationFormRow>
<ContractVerificationFormRow>
<Controller
<FormFieldAddress<FormFields, `libraries.${ number }.address`>
name={ `libraries.${ index }.address` }
control={ control }
render={ renderAddressControl }
rules={{ required: true, pattern: ADDRESS_REGEXP }}
isRequired
placeholder="Library address (0x...)"
size={{ base: 'md', lg: 'lg' }}
/>
{ index === 0 ? (
<>
......
import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types';
import { CONTRACT_LICENSES } from 'lib/contracts/licenses';
import useIsMobile from 'lib/hooks/useIsMobile';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
const options = CONTRACT_LICENSES.map(({ label, title, type }) => ({ label: `${ title } (${ label })`, value: type }));
import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
const ContractVerificationFieldLicenseType = () => {
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;
const options = CONTRACT_LICENSES.map(({ label, title, type }) => ({ label: `${ title } (${ label })`, value: type }));
return (
<FancySelect
{ ...field }
options={ options }
size={ isMobile ? 'md' : 'lg' }
placeholder="Contract license"
isDisabled={ formState.isSubmitting }
error={ error }
/>
);
}, [ formState.errors, formState.isSubmitting, isMobile ]);
const ContractVerificationFieldLicenseType = () => {
return (
<ContractVerificationFormRow>
<Controller
<FormFieldFancySelect<FormFields, 'license_type'>
name="license_type"
control={ control }
render={ renderControl }
placeholder="Contract license"
options={ options }
/>
<span>
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.
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.
</span>
</ContractVerificationFormRow>
);
......
......@@ -13,26 +13,22 @@ import {
Box,
} from '@chakra-ui/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 { SmartContractVerificationMethod, SmartContractVerificationConfig } from 'types/client/contract';
import useIsMobile from 'lib/hooks/useIsMobile';
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 { METHOD_LABELS } from '../utils';
interface Props {
control: Control<FormFields>;
isDisabled?: boolean;
methods: SmartContractVerificationConfig['verification_options'];
}
const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props) => {
const ContractVerificationFieldMethod = ({ methods }: Props) => {
const tooltipBg = useColorModeValue('gray.700', 'gray.900');
const isMobile = useIsMobile();
......@@ -41,21 +37,6 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props
label: METHOD_LABELS[method],
})), [ 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) => {
switch (method) {
case 'flattened-code':
......@@ -128,11 +109,13 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props
</Portal>
</Popover>
</Box>
<Controller
<FormFieldFancySelect<FormFields, 'method'>
name="method"
control={ control }
render={ renderControl }
rules={{ required: true }}
placeholder="Verification method (compiler type)"
options={ options }
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 type { ControllerRenderProps } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
interface Props {
hint?: string;
isReadOnly?: boolean;
}
const ContractVerificationFieldName = ({ hint, isReadOnly }: 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 ]);
const ContractVerificationFieldName = ({ hint }: Props) => {
return (
<ContractVerificationFormRow>
<Controller
<FormFieldText<FormFields>
name="name"
control={ control }
render={ renderControl }
rules={{ required: true }}
isRequired
placeholder="Contract name"
size={{ base: 'md', lg: 'lg' }}
rules={{ maxLength: 255 }}
/>
{ hint ? <span>{ hint }</span> : (
<>
......
import { Flex, Input } from '@chakra-ui/react';
import { Flex } from '@chakra-ui/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 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';
const ContractVerificationFieldOptimization = () => {
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(() => {
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 (
<ContractVerificationFormRow>
<Flex columnGap={ 5 } h={{ base: 'auto', lg: '32px' }}>
<Controller
<FormFieldCheckbox<FormFields, 'is_optimization_enabled'>
name="is_optimization_enabled"
control={ control }
render={ renderCheckboxControl }
label="Optimization enabled"
onChange={ handleCheckboxChange }
flexShrink={ 0 }
/>
{ isEnabled && (
<Controller
<FormFieldText<FormFields, 'optimization_runs'>
name="optimization_runs"
control={ control }
render={ renderInputControl }
rules={{ required: true }}
isRequired
placeholder="Optimization runs"
type="number"
size="xs"
minW="100px"
maxW="200px"
flexShrink={ 1 }
/>
) }
</Flex>
......
......@@ -6,10 +6,10 @@ import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types';
import { Mb } from 'lib/consts';
import DragAndDropArea from 'ui/shared/forms/DragAndDropArea';
import FieldError from 'ui/shared/forms/FieldError';
import FileInput from 'ui/shared/forms/FileInput';
import FileSnippet from 'ui/shared/forms/FileSnippet';
import FieldError from 'ui/shared/forms/components/FieldError';
import DragAndDropArea from 'ui/shared/forms/inputs/file/DragAndDropArea';
import FileInput from 'ui/shared/forms/inputs/file/FileInput';
import FileSnippet from 'ui/shared/forms/inputs/file/FileSnippet';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
......
import { Box, Link } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
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 { SmartContractVerificationConfig } from 'types/client/contract';
import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
import IconSvg from 'ui/shared/IconSvg';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
......@@ -17,8 +14,6 @@ import ContractVerificationFormRow from '../ContractVerificationFormRow';
const OPTIONS_LIMIT = 50;
const ContractVerificationFieldZkCompiler = () => {
const { formState, control } = useFormContext<FormFields>();
const isMobile = useIsMobile();
const queryClient = useQueryClient();
const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config'));
......@@ -32,33 +27,17 @@ const ContractVerificationFieldZkCompiler = () => {
.slice(0, OPTIONS_LIMIT);
}, [ options ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'zk_compiler'>}) => {
const error = 'zk_compiler' in formState.errors ? formState.errors.zk_compiler : undefined;
return (
<FancySelect
{ ...field }
loadOptions={ loadOptions }
defaultOptions
size={ isMobile ? 'md' : 'lg' }
return (
<ContractVerificationFormRow>
<FormFieldFancySelect<FormFields, 'zk_compiler'>
name="zk_compiler"
placeholder="ZK compiler (enter version or use the dropdown)"
placeholderIcon={ <IconSvg name="search"/> }
isDisabled={ formState.isSubmitting }
error={ error }
loadOptions={ loadOptions }
defaultOptions
isRequired
isAsync
/>
);
}, [ formState.errors, formState.isSubmitting, isMobile, loadOptions ]);
return (
<ContractVerificationFormRow>
<Controller
name="zk_compiler"
control={ control }
render={ renderControl }
rules={{ required: true }}
/>
<Box>
<Link isExternal href="https://docs.zksync.io/zk-stack/components/compiler/specification#glossary">zksolc</Link>
<span> compiler version.</span>
......
import type { SmartContractLicenseType } from 'types/api/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 {
name: string;
......
......@@ -165,7 +165,7 @@ export function getDefaultValues(
const method = singleMethod || methodParam;
if (!method) {
return;
return { address: hash || '' };
}
const defaultValues: FormFields = { ...DEFAULT_VALUES[method], address: hash || '', license_type: licenseType };
......
import { FormControl, Input } from '@chakra-ui/react';
import _capitalize from 'lodash/capitalize';
import React from 'react';
import type { ControllerRenderProps, UseFormReturn } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { UseFormReturn } from 'react-hook-form';
import type { FormFields } from './types';
import dayjs from 'lib/date/dayjs';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
interface Props {
formApi: UseFormReturn<FormFields>;
......@@ -15,26 +13,7 @@ interface Props {
}
const CsvExportFormField = ({ formApi, name }: Props) => {
const { formState, control, 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 { formState, getValues, trigger } = formApi;
const validate = React.useCallback((newValue: string) => {
if (name === 'from') {
......@@ -57,11 +36,15 @@ const CsvExportFormField = ({ formApi, name }: Props) => {
}, [ formState.errors.from, formState.errors.to, getValues, name, trigger ]);
return (
<Controller
<FormFieldText<FormFields, typeof name>
name={ name }
control={ control }
render={ renderControl }
rules={{ required: true, validate }}
type="date"
max={ dayjs().format('YYYY-MM-DD') }
placeholder={ _capitalize(name) }
isRequired
rules={{ validate }}
size={{ base: 'md', lg: 'lg' }}
maxW={{ base: 'auto', lg: '220px' }}
/>
);
};
......
import {
Box,
Button,
FormControl,
Input,
Textarea,
} from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react';
import type { ControllerRenderProps, SubmitHandler } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form';
import type { SubmitHandler } from 'react-hook-form';
import { useForm, FormProvider } from 'react-hook-form';
import type { CustomAbi, CustomAbis, CustomAbiErrors } from 'types/api/account';
......@@ -16,9 +13,8 @@ import type { ResourceErrorAccount } from 'lib/api/resources';
import { resourceKey } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import getErrorMessage from 'lib/getErrorMessage';
import { ADDRESS_REGEXP } from 'lib/validations/address';
import AddressInput from 'ui/shared/AddressInput';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import FormFieldAddress from 'ui/shared/forms/fields/FormFieldAddress';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
type Props = {
data?: CustomAbi;
......@@ -35,7 +31,7 @@ type Inputs = {
const NAME_MAX_LENGTH = 255;
const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const { control, formState: { errors, isDirty }, handleSubmit, setError } = useForm<Inputs>({
const formApi = useForm<Inputs>({
defaultValues: {
contract_address_hash: data?.contract_address_hash || '',
name: data?.name || '',
......@@ -85,102 +81,64 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
onError: (error: ResourceErrorAccount<CustomAbiErrors>) => {
const errorMap = error.payload?.errors;
if (errorMap?.address_hash || errorMap?.name || errorMap?.abi) {
errorMap?.address_hash && setError('contract_address_hash', { type: 'custom', message: getErrorMessage(errorMap, 'address_hash') });
errorMap?.name && setError('name', { type: 'custom', message: getErrorMessage(errorMap, 'name') });
errorMap?.abi && setError('abi', { type: 'custom', message: getErrorMessage(errorMap, 'abi') });
errorMap?.address_hash && formApi.setError('contract_address_hash', { type: 'custom', message: getErrorMessage(errorMap, 'address_hash') });
errorMap?.name && formApi.setError('name', { type: 'custom', message: getErrorMessage(errorMap, 'name') });
errorMap?.abi && formApi.setError('abi', { type: 'custom', message: getErrorMessage(errorMap, 'abi') });
} 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 {
setAlertVisible(true);
}
},
});
const onSubmit: SubmitHandler<Inputs> = useCallback((formData) => {
const onSubmit: SubmitHandler<Inputs> = useCallback(async(formData) => {
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 ]);
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 (
<form noValidate onSubmit={ handleSubmit(onSubmit) }>
<Box>
<Controller
<FormProvider { ...formApi }>
<form noValidate onSubmit={ formApi.handleSubmit(onSubmit) }>
<FormFieldAddress<Inputs>
name="contract_address_hash"
control={ control }
render={ renderContractAddressInput }
rules={{
pattern: ADDRESS_REGEXP,
required: true,
}}
placeholder="Smart contract address (0x...)"
isRequired
bgColor="dialog_bg"
mb={ 5 }
/>
</Box>
<Box marginTop={ 5 }>
<Controller
<FormFieldText<Inputs>
name="name"
control={ control }
render={ renderNameInput }
rules={{ required: true }}
placeholder="Project name"
isRequired
rules={{
maxLength: NAME_MAX_LENGTH,
}}
bgColor="dialog_bg"
mb={ 5 }
/>
</Box>
<Box marginTop={ 5 }>
<Controller
<FormFieldText<Inputs>
name="abi"
control={ control }
render={ renderAbiInput }
rules={{ required: true }}
/>
</Box>
<Box marginTop={ 8 }>
<Button
placeholder="Custom ABI [{...}] (JSON format)"
isRequired
asComponent="Textarea"
bgColor="dialog_bg"
size="lg"
type="submit"
isDisabled={ !isDirty }
isLoading={ mutation.isPending }
>
{ data ? 'Save' : 'Create custom ABI' }
</Button>
</Box>
</form>
minH="300px"
mb={ 8 }
/>
<Box>
<Button
size="lg"
type="submit"
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';
import useToast from 'lib/hooks/useToast';
import * as mixpanel from 'lib/mixpanel';
import FormFieldReCaptcha from 'ui/shared/forms/fields/FormFieldReCaptcha';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import AuthModal from 'ui/snippets/auth/AuthModal';
import MyProfileFieldsEmail from './fields/MyProfileFieldsEmail';
import MyProfileFieldsName from './fields/MyProfileFieldsName';
const MIXPANEL_CONFIG = {
account_link_info: {
......@@ -80,7 +80,7 @@ const MyProfileEmail = ({ profileQuery }: Props) => {
noValidate
onSubmit={ formApi.handleSubmit(onFormSubmit) }
>
<MyProfileFieldsName/>
<FormFieldText<FormFields> name="name" placeholder="Name" isReadOnly mb={ 3 }/>
<MyProfileFieldsEmail
isReadOnly={ !config.services.reCaptchaV3.siteKey || Boolean(profileQuery.data?.email) }
defaultValue={ profileQuery.data?.email || undefined }
......
......@@ -4,9 +4,9 @@ import { useController, useFormContext } from 'react-hook-form';
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 InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props {
isReadOnly?: boolean;
......@@ -35,7 +35,7 @@ const MyProfileFieldsEmail = ({ isReadOnly, defaultValue }: Props) => {
isReadOnly={ isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text="Email" error={ fieldState.error }/>
<FormInputPlaceholder text="Email" error={ fieldState.error }/>
{ isVerified && (
<InputRightElement h="100%">
<IconSvg name="certified" boxSize={ 5 } color="green.500"/>
......
......@@ -11,7 +11,6 @@ import useAddressMetadataInfoQuery from 'lib/address/useAddressMetadataInfoQuery
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
import useAddressProfileApiQuery from 'lib/hooks/useAddressProfileApiQuery';
import useContractTabs from 'lib/hooks/useContractTabs';
import useIsSafeAddress from 'lib/hooks/useIsSafeAddress';
import getNetworkValidationActionText from 'lib/networks/getNetworkValidationActionText';
import getQueryParamString from 'lib/router/getQueryParamString';
......@@ -33,6 +32,8 @@ import AddressTokenTransfers from 'ui/address/AddressTokenTransfers';
import AddressTxs from 'ui/address/AddressTxs';
import AddressUserOps from 'ui/address/AddressUserOps';
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 AddressMetadataAlert from 'ui/address/details/AddressMetadataAlert';
import AddressQrCode from 'ui/address/details/AddressQrCode';
......@@ -138,7 +139,11 @@ const AddressPageContent = () => {
const isSafeAddress = useIsSafeAddress(!addressQuery.isPlaceholderData && addressQuery.data?.is_contract ? hash : undefined);
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(() => {
return [
......@@ -245,7 +250,7 @@ const AddressPageContent = () => {
isLoading={ contractTabs.isLoading }
/>
),
subTabs: contractTabs.tabs.map(tab => tab.id),
subTabs: CONTRACT_TAB_IDS,
} : undefined,
].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 { useRouter } from 'next/router';
import React from 'react';
......@@ -179,13 +179,19 @@ const Chart = () => {
{ !isMobile && <Text>Period</Text> }
<ChartIntervalSelect interval={ interval } onIntervalChange={ onIntervalChange }/>
</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 }>
<Text>{ isMobile ? 'Res.' : 'Resolution' }</Text>
<Skeleton isLoaded={ !isInfoLoading }>
{ isMobile ? 'Res.' : 'Resolution' }
</Skeleton>
<ChartResolutionSelect
resolution={ resolution }
onResolutionChange={ onResolutionChange }
resolutions={ lineQuery.data?.info?.resolutions || [] }
isLoading={ isInfoLoading }
/>
</Flex>
) }
......
......@@ -9,7 +9,6 @@ import useApiQuery from 'lib/api/useApiQuery';
import useDebounce from 'lib/hooks/useDebounce';
import { apos } from 'lib/html-entities';
import getQueryParamString from 'lib/router/getQueryParamString';
import { ADDRESS_REGEXP } from 'lib/validations/address';
import { ENS_DOMAIN } from 'stubs/ENS';
import { generateListStub } from 'stubs/utils';
import NameDomainsActionBar from 'ui/nameDomains/NameDomainsActionBar';
......@@ -18,6 +17,7 @@ import NameDomainsTable from 'ui/nameDomains/NameDomainsTable';
import type { Sort, SortField } from 'ui/nameDomains/utils';
import { SORT_OPTIONS, getNextSortValue } from 'ui/nameDomains/utils';
import DataListDisplay from 'ui/shared/DataListDisplay';
import { ADDRESS_REGEXP } from 'ui/shared/forms/validators/address';
import PageTitle from 'ui/shared/Page/PageTitle';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue';
......
......@@ -10,7 +10,6 @@ import type { RoutedTab } from 'ui/shared/Tabs/types';
import config from 'configs/app';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useContractTabs from 'lib/hooks/useContractTabs';
import useIsMobile from 'lib/hooks/useIsMobile';
import * as metadata from 'lib/metadata';
import getQueryParamString from 'lib/router/getQueryParamString';
......@@ -23,6 +22,8 @@ import { getTokenHoldersStub } from 'stubs/token';
import { generateListStub } from 'stubs/utils';
import AddressContract from 'ui/address/AddressContract';
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 IconSvg from 'ui/shared/IconSvg';
import Pagination from 'ui/shared/pagination/Pagination';
......@@ -191,7 +192,7 @@ const TokenPageContent = () => {
return 'Contract';
},
component: <AddressContract tabs={ contractTabs.tabs } isLoading={ contractTabs.isLoading } shouldRender={ !isLoading }/>,
subTabs: contractTabs.tabs.map(tab => tab.id),
subTabs: CONTRACT_TAB_IDS,
} : undefined,
].filter(Boolean);
......
......@@ -3,18 +3,17 @@ import {
Button,
} from '@chakra-ui/react';
import { useMutation } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form';
import React, { useState } from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { FormProvider, useForm } from 'react-hook-form';
import type { AddressTag, AddressTagErrors } from 'types/api/account';
import type { ResourceErrorAccount } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import getErrorMessage from 'lib/getErrorMessage';
import { ADDRESS_REGEXP } from 'lib/validations/address';
import AddressInput from 'ui/shared/AddressInput';
import TagInput from 'ui/shared/TagInput';
import FormFieldAddress from 'ui/shared/forms/fields/FormFieldAddress';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
const TAG_MAX_LENGTH = 35;
......@@ -33,7 +32,7 @@ type Inputs = {
const AddressForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisible }) => {
const apiFetch = useApiFetch();
const [ pending, setPending ] = useState(false);
const { control, handleSubmit, formState: { errors, isDirty }, setError } = useForm<Inputs>({
const formApi = useForm<Inputs>({
mode: 'onTouched',
defaultValues: {
address: data?.address_hash || '',
......@@ -41,7 +40,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisibl
},
});
const { mutate } = useMutation({
const { mutateAsync } = useMutation({
mutationFn: (formData: Inputs) => {
const body = {
name: formData?.tag,
......@@ -62,10 +61,10 @@ const AddressForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisibl
setPending(false);
const errorMap = error.payload?.errors;
if (errorMap?.address_hash || errorMap?.name) {
errorMap?.address_hash && setError('address', { type: 'custom', message: getErrorMessage(errorMap, 'address_hash') });
errorMap?.name && setError('tag', { type: 'custom', message: getErrorMessage(errorMap, 'name') });
errorMap?.address_hash && formApi.setError('address', { type: 'custom', message: getErrorMessage(errorMap, 'address_hash') });
errorMap?.name && formApi.setError('tag', { type: 'custom', message: getErrorMessage(errorMap, 'name') });
} 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 {
setAlertVisible(true);
}
......@@ -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);
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 (
<form noValidate onSubmit={ handleSubmit(onSubmit) }>
<Box marginBottom={ 5 }>
<Controller
<FormProvider { ...formApi }>
<form noValidate onSubmit={ formApi.handleSubmit(onSubmit) }>
<FormFieldAddress<Inputs>
name="address"
control={ control }
rules={{
pattern: ADDRESS_REGEXP,
required: true,
}}
render={ renderAddressInput }
isRequired
bgColor="dialog_bg"
mb={ 5 }
/>
</Box>
<Box marginBottom={ 8 }>
<Controller
<FormFieldText<Inputs>
name="tag"
control={ control }
placeholder="Private tag (max 35 characters)"
isRequired
rules={{
maxLength: TAG_MAX_LENGTH,
required: true,
}}
render={ renderTagInput }
bgColor="dialog_bg"
mb={ 8 }
/>
</Box>
<Box marginTop={ 8 }>
<Button
size="lg"
type="submit"
isDisabled={ !isDirty }
isLoading={ pending }
>
{ data ? 'Save changes' : 'Add tag' }
</Button>
</Box>
</form>
<Box marginTop={ 8 }>
<Button
size="lg"
type="submit"
isDisabled={ !formApi.formState.isDirty }
isLoading={ pending }
>
{ data ? 'Save changes' : 'Add tag' }
</Button>
</Box>
</form>
</FormProvider>
);
};
......
......@@ -3,9 +3,9 @@ import {
Button,
} from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form';
import React, { useState } from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { useForm, FormProvider } from 'react-hook-form';
import type { TransactionTag, TransactionTagErrors } from 'types/api/account';
......@@ -13,9 +13,8 @@ import type { ResourceErrorAccount } from 'lib/api/resources';
import { resourceKey } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import getErrorMessage from 'lib/getErrorMessage';
import { TRANSACTION_HASH_REGEXP } from 'lib/validations/transaction';
import TagInput from 'ui/shared/TagInput';
import TransactionInput from 'ui/shared/TransactionInput';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import { TRANSACTION_HASH_LENGTH, TRANSACTION_HASH_REGEXP } from 'ui/shared/forms/validators/transaction';
const TAG_MAX_LENGTH = 35;
......@@ -34,7 +33,7 @@ type Inputs = {
const TransactionForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisible }) => {
const [ pending, setPending ] = useState(false);
const { control, handleSubmit, formState: { errors, isDirty }, setError } = useForm<Inputs>({
const formApi = useForm<Inputs>({
mode: 'onTouched',
defaultValues: {
transaction: data?.transaction_hash || '',
......@@ -45,7 +44,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVi
const queryClient = useQueryClient();
const apiFetch = useApiFetch();
const { mutate } = useMutation({
const { mutateAsync } = useMutation({
mutationFn: (formData: Inputs) => {
const body = {
name: formData?.tag,
......@@ -66,10 +65,10 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVi
setPending(false);
const errorMap = error.payload?.errors;
if (errorMap?.tx_hash || errorMap?.name) {
errorMap?.tx_hash && setError('transaction', { type: 'custom', message: getErrorMessage(errorMap, 'tx_hash') });
errorMap?.name && setError('tag', { type: 'custom', message: getErrorMessage(errorMap, 'name') });
errorMap?.tx_hash && formApi.setError('transaction', { type: 'custom', message: getErrorMessage(errorMap, 'tx_hash') });
errorMap?.name && formApi.setError('tag', { type: 'custom', message: getErrorMessage(errorMap, 'name') });
} 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 {
setAlertVisible(true);
}
......@@ -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);
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 (
<form noValidate onSubmit={ handleSubmit(onSubmit) }>
<Box marginBottom={ 5 }>
<Controller
<FormProvider { ...formApi }>
<form noValidate onSubmit={ formApi.handleSubmit(onSubmit) }>
<FormFieldText<Inputs>
name="transaction"
control={ control }
placeholder="Transaction hash (0x...)"
isRequired
rules={{
maxLength: TRANSACTION_HASH_LENGTH,
pattern: TRANSACTION_HASH_REGEXP,
required: true,
}}
render={ renderTransactionInput }
bgColor="dialog_bg"
mb={ 5 }
/>
</Box>
<Box marginBottom={ 8 }>
<Controller
<FormFieldText<Inputs>
name="tag"
control={ control }
placeholder="Private tag (max 35 characters)"
isRequired
rules={{
maxLength: TAG_MAX_LENGTH,
required: true,
}}
render={ renderTagInput }
bgColor="dialog_bg"
mb={ 8 }
/>
</Box>
<Box marginTop={ 8 }>
<Button
size="lg"
type="submit"
isDisabled={ !isDirty }
isLoading={ pending }
>
{ data ? 'Save changes' : 'Add tag' }
</Button>
</Box>
</form>
<Box marginTop={ 8 }>
<Button
size="lg"
type="submit"
isDisabled={ !formApi.formState.isDirty }
isLoading={ pending }
>
{ data ? 'Save changes' : 'Add tag' }
</Button>
</Box>
</form>
</FormProvider>
);
};
......
......@@ -14,15 +14,13 @@ import useApiFetch from 'lib/api/useApiFetch';
import getErrorObj from 'lib/errors/getErrorObj';
import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
import useIsMobile from 'lib/hooks/useIsMobile';
import FormFieldEmail from 'ui/shared/forms/fields/FormFieldEmail';
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 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 { convertFormDataToRequestsBody, getFormDefaultValues } from './utils';
......@@ -82,6 +80,10 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => {
return null;
}
const fieldProps = {
size: { base: 'md', lg: 'lg' },
};
return (
<GoogleReCaptchaProvider reCaptchaKey={ appConfig.services.reCaptchaV3.siteKey }>
<FormProvider { ...formApi }>
......@@ -97,21 +99,34 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => {
<GridItem colSpan={{ base: 1, lg: 3 }} as="h2" textStyle="h4">
Company info
</GridItem>
<PublicTagsSubmitFieldRequesterName/>
<PublicTagsSubmitFieldRequesterEmail/>
<FormFieldText<FormFields> name="requesterName" isRequired placeholder="Your name" { ...fieldProps }/>
<FormFieldEmail<FormFields> name="requesterEmail" isRequired { ...fieldProps }/>
{ !isMobile && <div/> }
<PublicTagsSubmitFieldCompanyName/>
<PublicTagsSubmitFieldCompanyWebsite/>
<FormFieldText<FormFields> name="companyName" placeholder="Company name" { ...fieldProps }/>
<FormFieldUrl<FormFields> name="companyWebsite" placeholder="Company website" { ...fieldProps }/>
{ !isMobile && <div/> }
<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"/>
</GridItem>
<PublicTagsSubmitFieldAddresses/>
<PublicTagsSubmitFieldTags tagTypes={ config?.tagTypes }/>
<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 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 { useFieldArray, useFormContext } from 'react-hook-form';
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 InputPlaceholder from 'ui/shared/InputPlaceholder';
const LIMIT = 20;
const PublicTagsSubmitFieldAddresses = () => {
const { control, formState, register } = useFormContext<FormFields>();
const { control, formState } = useFormContext<FormFields>();
const { fields, insert, remove } = useFieldArray<FormFields, 'addresses'>({
name: 'addresses',
control,
......@@ -36,20 +35,15 @@ const PublicTagsSubmitFieldAddresses = () => {
return (
<>
{ fields.map((field, index) => {
const error = formState.errors?.addresses?.[ index ]?.hash;
return (
<React.Fragment key={ field.id }>
<GridItem colSpan={{ base: 1, lg: 2 }}>
<FormControl variant="floating" isRequired size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...register(`addresses.${ index }.hash`, { required: true, pattern: ADDRESS_REGEXP }) }
isInvalid={ Boolean(error) }
isDisabled={ formState.isSubmitting }
autoComplete="off"
/>
<InputPlaceholder text="Smart contract / Address (0x...)" error={ error }/>
</FormControl>
<FormFieldAddress<FormFields>
name={ `addresses.${ index }.hash` }
isRequired
placeholder="Smart contract / Address (0x...)"
size={{ base: 'md', lg: 'lg' }}
/>
</GridItem>
<GridItem display="flex" alignItems="center" columnGap={ 5 } justifyContent={{ base: 'flex-end', lg: 'flex-start' }}>
{ 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 { 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 { PublicTagType } from 'types/api/addressMetadata';
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 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 InputPlaceholder from 'ui/shared/InputPlaceholder';
import PublicTagsSubmitFieldTagColor from './PublicTagsSubmitFieldTagColor';
import PublicTagsSubmitFieldTagType from './PublicTagsSubmitFieldTagType';
......@@ -19,14 +19,13 @@ interface Props {
index: number;
field: FormFieldTag;
tagTypes: Array<PublicTagType> | undefined;
register: UseFormRegister<FormFields>;
errors: Merge<FieldError, FieldErrorsImpl<FormFieldTag>> | undefined;
isDisabled: boolean;
onAddClick?: (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 bgColorDefault = useColorModeValue('blackAlpha.50', 'whiteAlpha.100');
const bgColorError = useColorModeValue('red.50', 'red.900');
......@@ -39,6 +38,10 @@ const PublicTagsSubmitFieldTag = ({ index, isDisabled, register, errors, onAddCl
onRemoveClick?.(index);
}, [ index, onRemoveClick ]);
const fieldProps = {
size: { base: 'md', lg: 'lg' },
};
return (
<>
<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
templateColumns={{ base: '1fr', lg: 'repeat(4, 1fr)' }}
>
<GridItem colSpan={{ base: 1, lg: 2 }}>
<FormControl variant="floating" isRequired size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...register(`tags.${ index }.name`, { required: true, maxLength: 35 }) }
isInvalid={ Boolean(errors?.name) }
isDisabled={ isDisabled }
autoComplete="off"
/>
<InputPlaceholder text="Tag (max 35 characters)" error={ errors?.name }/>
</FormControl>
<FormFieldText<FormFields>
name={ `tags.${ index }.name` }
placeholder="Tag (max 35 characters)"
isRequired
rules={{ maxLength: 35 }}
{ ...fieldProps }
/>
</GridItem>
<GridItem colSpan={{ base: 1, lg: 2 }}>
<PublicTagsSubmitFieldTagType index={ index } tagTypes={ tagTypes } isDisabled={ isDisabled }/>
<PublicTagsSubmitFieldTagType index={ index } tagTypes={ tagTypes }/>
</GridItem>
<GridItem colSpan={{ base: 1, lg: 2 }}>
<FormControl variant="floating" size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...register(`tags.${ index }.url`, { validate: urlValidator }) }
isInvalid={ Boolean(errors?.url) }
isDisabled={ isDisabled }
autoComplete="off"
/>
<InputPlaceholder text="Label URL" error={ errors?.url }/>
</FormControl>
<FormFieldUrl<FormFields>
name={ `tags.${ index }.url` }
placeholder="Label URL"
{ ...fieldProps }
/>
</GridItem>
<PublicTagsSubmitFieldTagColor
fieldType="bgColor"
fieldName={ `tags.${ index }.bgColor` }
placeholder="Background (Hex)"
index={ index }
register={ register }
error={ errors?.bgColor }
isDisabled={ isDisabled }
/>
<PublicTagsSubmitFieldTagColor
fieldType="textColor"
fieldName={ `tags.${ index }.textColor` }
placeholder="Text (Hex)"
index={ index }
register={ register }
error={ errors?.textColor }
isDisabled={ isDisabled }
/>
<GridItem colSpan={{ base: 1, lg: 4 }}>
<FormControl variant="floating" size={{ base: 'md', lg: 'lg' }}>
<Textarea
{ ...register(`tags.${ index }.tooltipDescription`, { maxLength: 80 }) }
isInvalid={ Boolean(errors?.tooltipDescription) }
isDisabled={ isDisabled }
autoComplete="off"
maxH="160px"
/>
<InputPlaceholder
text="Label description (max 80 characters)"
error={ errors?.tooltipDescription }
/>
</FormControl>
<FormFieldText<FormFields>
name={ `tags.${ index }.tooltipDescription` }
placeholder="Label description (max 80 characters)"
maxH="160px"
rules={{ maxLength: 80 }}
asComponent="Textarea"
{ ...fieldProps }
/>
</GridItem>
</Grid>
</GridItem>
......
import { Circle, FormControl, Input, InputGroup, InputRightElement, useColorModeValue } from '@chakra-ui/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 useIsMobile from 'lib/hooks/useIsMobile';
import { validator as colorValidator } from 'lib/validations/color';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import FormInputPlaceholder from 'ui/shared/forms/inputs/FormInputPlaceholder';
import { validator as colorValidator } from 'ui/shared/forms/validators/color';
type ColorFieldTypes = 'bgColor' | 'textColor';
interface Props<Type extends ColorFieldTypes> {
fieldType: Type;
fieldName: `tags.${ number }.${ Type }`;
index: number;
isDisabled: boolean;
register: UseFormRegister<FormFields>;
error: FieldError | undefined;
placeholder: string;
}
const PublicTagsSubmitFieldTagColor = <Type extends ColorFieldTypes>({ isDisabled, error, fieldName, placeholder, fieldType }: Props<Type>) => {
const { register } = useFormContext<FormFields>();
const PublicTagsSubmitFieldTagColor = <Type extends ColorFieldTypes>({ error, fieldName, placeholder, fieldType }: Props<Type>) => {
const { register, formState } = useFormContext<FormFields>();
const isDisabled = formState.isSubmitting;
const circleBgColorDefault = {
bgColor: useColorModeValue('gray.100', 'gray.700'),
......@@ -57,7 +56,7 @@ const PublicTagsSubmitFieldTagColor = <Type extends ColorFieldTypes>({ isDisable
autoComplete="off"
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 }>
<Circle
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 { chakraComponents } from 'chakra-react-select';
import _capitalize from 'lodash/capitalize';
import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import { useFormContext } from 'react-hook-form';
import type { FormFields } from '../types';
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 FancySelect from 'ui/shared/FancySelect/FancySelect';
import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
import IconSvg from 'ui/shared/IconSvg';
interface Props {
index: number;
tagTypes: Array<PublicTagType> | undefined;
isDisabled: boolean;
}
const PublicTagsSubmitFieldTagType = ({ index, tagTypes, isDisabled }: Props) => {
const isMobile = useIsMobile();
const { control, watch } = useFormContext<FormFields>();
const PublicTagsSubmitFieldTagType = ({ index, tagTypes }: Props) => {
const { watch } = useFormContext<FormFields>();
const typeOptions = React.useMemo(() => tagTypes?.map((type) => ({
value: type.type,
label: _capitalize(type.type),
})), [ tagTypes ]);
})) ?? [], [ tagTypes ]);
const fieldValue = watch(`tags.${ index }.type`).value;
......@@ -63,30 +59,15 @@ const PublicTagsSubmitFieldTagType = ({ index, tagTypes, isDisabled }: Props) =>
return { SingleValue };
}, [ 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 (
<Controller
<FormFieldFancySelect<FormFields, `tags.${ number }.type`>
name={ `tags.${ index }.type` }
control={ control }
render={ renderControl }
rules={{ required: true }}
placeholder="Tag type"
options={ typeOptions }
isRequired
isAsync={ false }
isSearchable={ false }
components={ selectComponents }
/>
);
};
......
......@@ -13,7 +13,7 @@ interface 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'>({
name: 'tags',
control,
......@@ -47,7 +47,6 @@ const PublicTagsSubmitFieldTags = ({ tagTypes }: Props) => {
field={ watch(`tags.${ index }`) }
index={ index }
tagTypes={ tagTypes }
register={ register }
errors={ errors }
isDisabled={ isDisabled }
onAddClick={ fields.length < LIMIT && index === fields.length - 1 ? handleAddFieldClick : undefined }
......
import type { AddressMetadataTagType } from 'types/api/addressMetadata';
import type { Option } from 'ui/shared/forms/inputs/select/types';
export interface FormFields {
requesterName: string;
......@@ -13,10 +14,7 @@ export interface FormFields {
export interface FormFieldTag {
name: string;
type: {
label: string;
value: AddressMetadataTagType;
};
type: Option<AddressMetadataTagType>;
url: string | undefined;
bgColor: string | undefined;
textColor: string | undefined;
......
......@@ -10,7 +10,6 @@ import dayjs from 'lib/date/dayjs';
import highlightText from 'lib/highlightText';
import * as mixpanel from 'lib/mixpanel/index';
import { saveToRecentKeywords } from 'lib/recentSearchKeywords';
import { ADDRESS_REGEXP } from 'lib/validations/address';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
import * as AddressEntity from 'ui/shared/entities/address/AddressEntity';
import * as BlobEntity from 'ui/shared/entities/blob/BlobEntity';
......@@ -19,6 +18,7 @@ import * as EnsEntity from 'ui/shared/entities/ens/EnsEntity';
import * as TokenEntity from 'ui/shared/entities/token/TokenEntity';
import * as TxEntity from 'ui/shared/entities/tx/TxEntity';
import * as UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity';
import { ADDRESS_REGEXP } from 'ui/shared/forms/validators/address';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/links/LinkExternal';
......
......@@ -10,7 +10,6 @@ import dayjs from 'lib/date/dayjs';
import highlightText from 'lib/highlightText';
import * as mixpanel from 'lib/mixpanel/index';
import { saveToRecentKeywords } from 'lib/recentSearchKeywords';
import { ADDRESS_REGEXP } from 'lib/validations/address';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
import * as AddressEntity from 'ui/shared/entities/address/AddressEntity';
import * as BlobEntity from 'ui/shared/entities/blob/BlobEntity';
......@@ -19,6 +18,7 @@ import * as EnsEntity from 'ui/shared/entities/ens/EnsEntity';
import * as TokenEntity from 'ui/shared/entities/token/TokenEntity';
import * as TxEntity from 'ui/shared/entities/tx/TxEntity';
import * as UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity';
import { ADDRESS_REGEXP } from 'ui/shared/forms/validators/address';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg';
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) => {
return [ ...props.tabs, menuButton ];
}, [ 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);
useScrollToActiveTab({ activeTabIndex: props.activeTabIndex, listRef, tabsRefs, isMobile, isLoading: props.isLoading });
......@@ -80,6 +80,7 @@ const AdaptiveTabsList = (props: Props) => {
props.tabListProps)
}
>
{ props.leftSlot && <Box ref={ leftSlotRef } { ...props.leftSlotProps }> { props.leftSlot } </Box> }
{ tabsList.slice(0, props.isLoading ? 5 : Infinity).map((tab, index) => {
if (!tab.id) {
if (props.isLoading) {
......
......@@ -14,13 +14,27 @@ interface Props extends ThemingProps<'Tabs'> {
tabListProps?: ChakraProps | (({ isSticky, activeTabIndex }: { isSticky: boolean; activeTabIndex: number }) => ChakraProps);
rightSlot?: React.ReactNode;
rightSlotProps?: ChakraProps;
leftSlot?: React.ReactNode;
leftSlotProps?: ChakraProps;
stickyEnabled?: boolean;
className?: string;
onTabChange?: (index: number) => void;
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 tabIndex = useTabIndexFromQuery(tabs);
const tabsRef = useRef<HTMLDivElement>(null);
......@@ -59,6 +73,8 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, rightSlotProps, stickyEnabl
<TabsWithScroll
tabs={ tabs }
tabListProps={ tabListProps }
leftSlot={ leftSlot }
leftSlotProps={ leftSlotProps }
rightSlot={ rightSlot }
rightSlotProps={ rightSlotProps }
stickyEnabled={ stickyEnabled }
......
......@@ -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 }>
<PopoverTrigger>
<Button
as="div"
role="button"
variant="ghost"
isActive={ isOpen || isActive }
ref={ buttonRef }
......
......@@ -22,6 +22,8 @@ export interface Props extends ThemingProps<'Tabs'> {
tabListProps?: ChakraProps | (({ isSticky, activeTabIndex }: { isSticky: boolean; activeTabIndex: number }) => ChakraProps);
rightSlot?: React.ReactNode;
rightSlotProps?: ChakraProps;
leftSlot?: React.ReactNode;
leftSlotProps?: ChakraProps;
stickyEnabled?: boolean;
onTabChange?: (index: number) => void;
defaultTabIndex?: number;
......@@ -35,6 +37,8 @@ const TabsWithScroll = ({
tabListProps,
rightSlot,
rightSlotProps,
leftSlot,
leftSlotProps,
stickyEnabled,
onTabChange,
defaultTabIndex,
......@@ -102,6 +106,8 @@ const TabsWithScroll = ({
key={ isLoading + '_' + screenWidth + '_' + tabsList.map((tab) => tab.id).join(':') }
tabs={ tabs }
tabListProps={ tabListProps }
leftSlot={ leftSlot }
leftSlotProps={ leftSlotProps }
rightSlot={ rightSlot }
rightSlotProps={ rightSlotProps }
stickyEnabled={ stickyEnabled }
......
......@@ -9,10 +9,12 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab | MenuButton>, dis
const [ tabsRefs, setTabsRefs ] = React.useState<Array<React.RefObject<HTMLButtonElement>>>([]);
const listRef = React.useRef<HTMLDivElement>(null);
const rightSlotRef = React.useRef<HTMLDivElement>(null);
const leftSlotRef = React.useRef<HTMLDivElement>(null);
const calculateCut = React.useCallback(() => {
const listWidth = listRef.current?.getBoundingClientRect().width;
const rightSlotWidth = rightSlotRef.current?.getBoundingClientRect().width || 0;
const leftSlotWidth = leftSlotRef.current?.getBoundingClientRect().width || 0;
const tabWidths = tabsRefs.map((tab) => tab.current?.getBoundingClientRect().width);
const menuWidth = tabWidths[tabWidths.length - 1];
......@@ -33,11 +35,11 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab | MenuButton>, dis
if (index === array.length - 1) {
// last element
if (result.accWidth + item < listWidth - rightSlotWidth) {
if (result.accWidth + item < listWidth - rightSlotWidth - leftSlotWidth) {
return { visibleNum: result.visibleNum + 1, accWidth: result.accWidth + item };
}
} 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 };
}
}
......@@ -67,6 +69,7 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab | MenuButton>, dis
tabsRefs,
listRef,
rightSlotRef,
leftSlotRef,
};
}, [ 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 {
isFancy?: boolean;
}
const InputPlaceholder = ({ text, icon, error, isFancy }: Props) => {
const FormInputPlaceholder = ({ text, icon, error, isFancy }: Props) => {
let errorMessage = error?.message;
if (!errorMessage && error?.type === 'pattern') {
......@@ -21,13 +21,17 @@ const InputPlaceholder = ({ text, icon, error, isFancy }: Props) => {
alignItems="center"
{ ...(isFancy ? { 'data-fancy': true } : {}) }
variant="floating"
bgColor="deeppink"
>
{ icon }
<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>
);
};
export default React.memo(InputPlaceholder);
export default React.memo(FormInputPlaceholder);
......@@ -2,7 +2,7 @@ import { chakra, Center, useColorModeValue } from '@chakra-ui/react';
import type { DragEvent } from 'react';
import React from 'react';
import { getAllFileEntries, convertFileEntryToFile } from './utils/files';
import { getAllFileEntries, convertFileEntryToFile } from './utils';
interface Props {
children: React.ReactNode;
......
......@@ -6,8 +6,8 @@ import type { FieldError, FieldErrorsImpl, Merge } from 'react-hook-form';
import type { Option } from './types';
import { getChakraStyles } from 'ui/shared/FancySelect/utils';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import FormInputPlaceholder from 'ui/shared/forms/inputs/FormInputPlaceholder';
import { getChakraStyles } from 'ui/shared/forms/inputs/select/utils';
interface CommonProps {
error?: Merge<FieldError, FieldErrorsImpl<Option>> | undefined;
......@@ -24,7 +24,7 @@ interface AsyncSelectProps extends AsyncProps<Option, boolean, GroupBase<Option>
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 menuZIndex = useToken('zIndices', 'dropdown');
......@@ -58,7 +58,7 @@ const FancySelect = (props: Props, ref: React.LegacyRef<HTMLDivElement>) => {
isInvalid={ Boolean(props.error) }
useBasicStyles
/>
<InputPlaceholder
<FormInputPlaceholder
text={ typeof props.placeholder === 'string' ? props.placeholder : '' }
icon={ props.placeholderIcon }
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_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 React from 'react';
......@@ -20,35 +20,17 @@ type RadioButtonProps = UseRadioProps & RadioItemProps;
const RadioButton = (props: RadioButtonProps) => {
const { getInputProps, getRadioProps } = useRadio(props);
const buttonColor = useColorModeValue('blue.50', 'gray.800');
const checkedTextColor = useColorModeValue('blue.700', 'gray.50');
const input = getInputProps();
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) {
return (
<Button
as="label"
aria-label={ props.title }
{ ...styleProps }
variant="radio_group"
data-selected={ props.isChecked }
>
<input { ...input }/>
<Flex
......@@ -64,7 +46,8 @@ const RadioButton = (props: RadioButtonProps) => {
<Button
as="label"
leftIcon={ props.icon ? <IconSvg name={ props.icon } boxSize={ 5 } mr={ -1 }/> : undefined }
{ ...styleProps }
variant="radio_group"
data-selected={ props.isChecked }
>
<input { ...input }/>
<Flex
......
......@@ -98,7 +98,7 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig }: Props) => {
const currentStep = steps[steps.length - 1];
switch (currentStep.type) {
case 'select_method':
return 'Select a way to connect';
return 'Select a way to login';
case 'connect_wallet':
return currentStep.isAuth ? 'Add wallet' : 'Continue with wallet';
case 'email':
......
......@@ -11,8 +11,7 @@ import getErrorMessage from 'lib/errors/getErrorMessage';
import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
import useToast from 'lib/hooks/useToast';
import * as mixpanel from 'lib/mixpanel';
import AuthModalFieldEmail from '../fields/AuthModalFieldEmail';
import FormFieldEmail from 'ui/shared/forms/fields/FormFieldEmail';
interface Props {
onSubmit: (screen: Screen) => void;
......@@ -78,7 +77,13 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => {
onSubmit={ formApi.handleSubmit(onFormSubmit) }
>
<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
mt={ 6 }
type="submit"
......
......@@ -29,7 +29,7 @@ const AuthModalScreenSelectMethod = ({ onSelectMethod }: Props) => {
return (
<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>
</VStack>
);
......
......@@ -5,9 +5,9 @@ import type { SearchResultAddressOrContract } from 'types/api/search';
import dayjs from 'lib/date/dayjs';
import highlightText from 'lib/highlightText';
import { ADDRESS_REGEXP } from 'lib/validations/address';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
import * as AddressEntity from 'ui/shared/entities/address/AddressEntity';
import { ADDRESS_REGEXP } from 'ui/shared/forms/validators/address';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props {
......
import { Button, Grid, GridItem } from '@chakra-ui/react';
import { Button, Grid, GridItem, Text } from '@chakra-ui/react';
import React from 'react';
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 { TokenInfoApplication } from 'types/api/account';
......@@ -15,22 +15,15 @@ import useUpdateEffect from 'lib/hooks/useUpdateEffect';
import * as mixpanel from 'lib/mixpanel/index';
import ContentLoader from 'ui/shared/ContentLoader';
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 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 TokenInfoFieldProjectWebsite from './fields/TokenInfoFieldProjectWebsite';
import TokenInfoFieldRequesterEmail from './fields/TokenInfoFieldRequesterEmail';
import TokenInfoFieldRequesterName from './fields/TokenInfoFieldRequesterName';
import TokenInfoFieldSocialLink from './fields/TokenInfoFieldSocialLink';
import TokenInfoFieldSupport from './fields/TokenInfoFieldSupport';
import TokenInfoFieldTokenName from './fields/TokenInfoFieldTokenName';
import TokenInfoFormSectionHeader from './TokenInfoFormSectionHeader';
import TokenInfoFormStatusText from './TokenInfoFormStatusText';
import { getFormDefaultValues, prepareRequestBody } from './utils';
......@@ -58,7 +51,7 @@ const TokenInfoForm = ({ address, tokenName, application, onSubmit }: Props) =>
mode: 'onBlur',
defaultValues: getFormDefaultValues(address, tokenName, application),
});
const { handleSubmit, formState, control, trigger } = formApi;
const { handleSubmit, formState } = formApi;
React.useEffect(() => {
if (!application?.id && !openEventSent.current) {
......@@ -116,66 +109,90 @@ const TokenInfoForm = ({ address, tokenName, application, onSubmit }: Props) =>
return <ContentLoader/>;
}
const fieldProps = { control, isReadOnly: application?.status === 'IN_PROCESS' };
const fieldProps = {
size: { base: 'md', lg: 'lg' },
isReadOnly: application?.status === 'IN_PROCESS',
};
return (
<form noValidate onSubmit={ handleSubmit(onFormSubmit) } autoComplete="off" ref={ containerRef }>
<TokenInfoFormStatusText application={ application }/>
<Grid mt={ 8 } gridTemplateColumns={{ base: '1fr', lg: '1fr 1fr' }} columnGap={ 5 } rowGap={ 5 }>
<TokenInfoFieldTokenName { ...fieldProps }/>
<TokenInfoFieldAddress { ...fieldProps }/>
<TokenInfoFieldRequesterName { ...fieldProps }/>
<TokenInfoFieldRequesterEmail { ...fieldProps }/>
<TokenInfoFormSectionHeader>Project info</TokenInfoFormSectionHeader>
<TokenInfoFieldProjectName { ...fieldProps }/>
<TokenInfoFieldProjectSector { ...fieldProps } config={ configQuery.data.projectSectors }/>
<TokenInfoFieldProjectEmail { ...fieldProps }/>
<TokenInfoFieldProjectWebsite { ...fieldProps }/>
<TokenInfoFieldDocs { ...fieldProps }/>
<TokenInfoFieldSupport { ...fieldProps }/>
<GridItem colSpan={{ base: 1, lg: 2 }}>
<TokenInfoFieldIconUrl { ...fieldProps } trigger={ trigger }/>
</GridItem>
<GridItem colSpan={{ base: 1, lg: 2 }}>
<TokenInfoFieldProjectDescription { ...fieldProps }/>
</GridItem>
<TokenInfoFormSectionHeader>Links</TokenInfoFormSectionHeader>
<TokenInfoFieldSocialLink { ...fieldProps } name="github"/>
<TokenInfoFieldSocialLink { ...fieldProps } name="twitter"/>
<TokenInfoFieldSocialLink { ...fieldProps } name="telegram"/>
<TokenInfoFieldSocialLink { ...fieldProps } name="opensea"/>
<TokenInfoFieldSocialLink { ...fieldProps } name="linkedin"/>
<TokenInfoFieldSocialLink { ...fieldProps } name="facebook"/>
<TokenInfoFieldSocialLink { ...fieldProps } name="discord"/>
<TokenInfoFieldSocialLink { ...fieldProps } name="medium"/>
<TokenInfoFieldSocialLink { ...fieldProps } name="slack"/>
<TokenInfoFieldSocialLink { ...fieldProps } name="reddit"/>
<TokenInfoFormSectionHeader>Price data</TokenInfoFormSectionHeader>
<TokenInfoFieldPriceTicker { ...fieldProps } name="ticker_coin_market_cap" label="CoinMarketCap URL"/>
<TokenInfoFieldPriceTicker { ...fieldProps } name="ticker_coin_gecko" label="CoinGecko URL"/>
<GridItem colSpan={{ base: 1, lg: 2 }}>
<TokenInfoFieldPriceTicker { ...fieldProps } name="ticker_defi_llama" label="DefiLlama URL "/>
</GridItem>
<GridItem colSpan={{ base: 1, lg: 2 }}>
<TokenInfoFieldComment { ...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 { ...formApi }>
<form noValidate onSubmit={ handleSubmit(onFormSubmit) } autoComplete="off" ref={ containerRef }>
<TokenInfoFormStatusText application={ application }/>
<Grid mt={ 8 } gridTemplateColumns={{ base: '1fr', lg: '1fr 1fr' }} columnGap={ 5 } rowGap={ 5 }>
<FormFieldText<Fields> name="token_name" isRequired placeholder="Token name" { ...fieldProps } isReadOnly/>
<FormFieldAddress<Fields> name="address" isRequired placeholder="Token contract address" { ...fieldProps } isReadOnly/>
<FormFieldText<Fields> name="requester_name" isRequired placeholder="Requester name" { ...fieldProps }/>
<FormFieldEmail<Fields> name="requester_email" isRequired placeholder="Requester email" { ...fieldProps }/>
<TokenInfoFormSectionHeader>Project info</TokenInfoFormSectionHeader>
<FormFieldText<Fields> name="project_name" placeholder="Project name" { ...fieldProps }/>
<TokenInfoFieldProjectSector { ...fieldProps } config={ configQuery.data.projectSectors }/>
<FormFieldEmail<Fields> name="project_email" isRequired placeholder="Official project email address" { ...fieldProps }/>
<FormFieldUrl<Fields> name="project_website" isRequired placeholder="Official project website" { ...fieldProps }/>
<FormFieldUrl<Fields> name="docs" placeholder="Docs" { ...fieldProps }/>
<TokenInfoFieldSupport { ...fieldProps }/>
<GridItem colSpan={{ base: 1, lg: 2 }}>
<TokenInfoFieldIconUrl { ...fieldProps }/>
</GridItem>
<GridItem colSpan={{ base: 1, lg: 2 }}>
<FormFieldText<Fields>
name="project_description"
isRequired
placeholder="Project description"
maxH="160px"
rules={{ maxLength: 300 }}
asComponent="Textarea"
{ ...fieldProps }
/>
<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>
</GridItem>
<TokenInfoFormSectionHeader>Links</TokenInfoFormSectionHeader>
<TokenInfoFieldSocialLink { ...fieldProps } name="github"/>
<TokenInfoFieldSocialLink { ...fieldProps } name="twitter"/>
<TokenInfoFieldSocialLink { ...fieldProps } name="telegram"/>
<TokenInfoFieldSocialLink { ...fieldProps } name="opensea"/>
<TokenInfoFieldSocialLink { ...fieldProps } name="linkedin"/>
<TokenInfoFieldSocialLink { ...fieldProps } name="facebook"/>
<TokenInfoFieldSocialLink { ...fieldProps } name="discord"/>
<TokenInfoFieldSocialLink { ...fieldProps } name="medium"/>
<TokenInfoFieldSocialLink { ...fieldProps } name="slack"/>
<TokenInfoFieldSocialLink { ...fieldProps } name="reddit"/>
<TokenInfoFormSectionHeader>Price data</TokenInfoFormSectionHeader>
<FormFieldUrl<Fields> name="ticker_coin_market_cap" placeholder="CoinMarketCap URL" { ...fieldProps }/>
<FormFieldUrl<Fields> name="ticker_coin_gecko" placeholder="CoinGecko URL" { ...fieldProps }/>
<GridItem colSpan={{ base: 1, lg: 2 }}>
<FormFieldUrl<Fields> name="ticker_defi_llama" placeholder="DefiLlama URL" { ...fieldProps }/>
</GridItem>
<GridItem colSpan={{ base: 1, lg: 2 }}>
<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 TokenLogoPlaceholder from 'ui/shared/TokenLogoPlaceholder';
interface Props {
url: string | undefined;
onLoad?: () => void;
onError?: () => void;
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 borderColorFilled = useColorModeValue('gray.300', 'gray.600');
const borderColorError = useColorModeValue('red.400', 'red.300');
const borderColorActive = isInvalid ? borderColorError : borderColorFilled;
const borderColorActive = isInvalid ? 'error' : borderColorFilled;
return (
<Center
......@@ -24,16 +20,7 @@ const TokenInfoIconPreview = ({ url, onError, onLoad, isInvalid }: Props) => {
borderColor={ url ? borderColorActive : borderColor }
borderRadius="base"
>
<Image
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 }
/>
{ children }
</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 type { Control, UseFormTrigger } from 'react-hook-form';
import { useController } from 'react-hook-form';
import type { Fields } from '../types';
import { times } from 'lib/html-entities';
import { validator as validateUrl } from 'lib/validations/url';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import ImageUrlPreview from 'ui/shared/forms/components/ImageUrlPreview';
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';
interface Props {
control: Control<Fields>;
isReadOnly?: boolean;
trigger: UseFormTrigger<Fields>;
size?: FormControlProps['size'];
}
const TokenInfoFieldIconUrl = ({ control, isReadOnly, trigger }: Props) => {
const TokenInfoFieldIconUrl = ({ isReadOnly, size }: Props) => {
const validatePreview = React.useCallback(() => {
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 ]);
const previewUtils = useFieldWithImagePreview({ name: 'icon_url', isRequired: true });
return (
<Flex columnGap={ 5 }>
<FormControl variant="floating" id={ field.name } size={{ base: 'md', lg: 'lg' }} isRequired>
<Input
{ ...field }
onBlur={ handleBlur }
isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting }
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' }
<FormFieldUrl<Fields>
name="icon_url"
placeholder={ `Link to icon URL, link to download a SVG or 48${ times }48 PNG icon logo` }
isReadOnly={ isReadOnly }
size={ size }
{ ...previewUtils.input }
/>
<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>
);
};
......
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 type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { Fields } from '../types';
import type { TokenInfoApplicationConfig } from 'types/api/account';
import useIsMobile from 'lib/hooks/useIsMobile';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
interface Props {
control: Control<Fields>;
isReadOnly?: boolean;
config: TokenInfoApplicationConfig['projectSectors'];
}
const TokenInfoFieldProjectSector = ({ control, isReadOnly, config }: Props) => {
const isMobile = useIsMobile();
const TokenInfoFieldProjectSector = ({ isReadOnly, config }: Props) => {
const options = React.useMemo(() => {
return config.map((option) => ({ label: option, value: option }));
}, [ 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 (
<Controller
<FormFieldFancySelect<Fields, 'project_sector'>
name="project_sector"
control={ control }
render={ renderControl }
placeholder="Project industry"
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 type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { ControllerRenderProps } from 'react-hook-form';
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 IconSvg from 'ui/shared/IconSvg';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Item {
icon: IconName;
......@@ -29,38 +27,24 @@ const SETTINGS: Record<keyof SocialLinkFields, Item> = {
};
interface Props {
control: Control<Fields>;
isReadOnly?: boolean;
size?: FormControlProps['size'];
name: keyof SocialLinkFields;
}
const TokenInfoFieldSocialLink = ({ control, isReadOnly, name }: 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' }} sx={{ '.chakra-input__group input': { pr: '60px' } }}>
<InputGroup>
<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 ]);
const TokenInfoFieldSocialLink = ({ isReadOnly, size, name }: Props) => {
const rightElement = React.useCallback(({ field }: { field: ControllerRenderProps<Fields, keyof SocialLinkFields> }) => {
return <IconSvg name={ SETTINGS[name].icon } boxSize={ 6 } color={ field.value ? SETTINGS[name].color : '#718096' }/>;
}, [ name ]);
return (
<Controller
<FormFieldUrl<Fields, keyof SocialLinkFields>
name={ name }
control={ control }
render={ renderControl }
rules={{ validate: validator }}
placeholder={ SETTINGS[name].label }
rightElement={ rightElement }
isReadOnly={ isReadOnly }
size={ size }
/>
);
};
......
import { FormControl, Input } from '@chakra-ui/react';
import type { InputProps } 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 as emailValidator } from 'lib/validations/email';
import { validator as urlValidator } from 'lib/validations/url';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import { validator as emailValidator } from 'ui/shared/forms/validators/email';
import { urlValidator } from 'ui/shared/forms/validators/url';
interface Props {
control: Control<Fields>;
isReadOnly?: boolean;
size?: InputProps['size'];
}
const TokenInfoFieldSupport = ({ control, isReadOnly }: 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 TokenInfoFieldSupport = (props: Props) => {
const validate = React.useCallback((newValue: string | undefined) => {
if (typeof newValue !== 'string') {
return true;
}
const urlValidationResult = urlValidator(newValue);
const emailValidationResult = emailValidator(newValue || '');
const emailValidationResult = emailValidator(newValue);
if (urlValidationResult === true || emailValidationResult === true) {
return true;
......@@ -43,11 +29,11 @@ const TokenInfoFieldSupport = ({ control, isReadOnly }: Props) => {
}, []);
return (
<Controller
<FormFieldText<Fields, 'support'>
name="support"
control={ control }
render={ renderControl }
placeholder="Support URL or email"
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 {
address: string;
......
......@@ -6,19 +6,18 @@ import {
useDisclosure,
} from '@chakra-ui/react';
import { useMutation } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form';
import React, { useState } from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { useForm, FormProvider } from 'react-hook-form';
import type { WatchlistAddress, WatchlistErrors } from 'types/api/account';
import type { ResourceErrorAccount } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import getErrorMessage from 'lib/getErrorMessage';
import { ADDRESS_REGEXP } from 'lib/validations/address';
import AddressInput from 'ui/shared/AddressInput';
import CheckboxInput from 'ui/shared/CheckboxInput';
import TagInput from 'ui/shared/TagInput';
import FormFieldAddress from 'ui/shared/forms/fields/FormFieldAddress';
import FormFieldCheckbox from 'ui/shared/forms/fields/FormFieldCheckbox';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import AuthModal from 'ui/snippets/auth/AuthModal';
import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
......@@ -36,7 +35,7 @@ type Props = {
isAdd: boolean;
}
type Inputs = {
export type Inputs = {
address: string;
tag: string;
notification: boolean;
......@@ -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 [ pending, setPending ] = useState(false);
......@@ -84,7 +73,7 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd
notificationsDefault = data.notification_settings;
}
const { control, handleSubmit, formState: { errors, isDirty }, setError } = useForm<Inputs>({
const formApi = useForm<Inputs>({
defaultValues: {
address: data?.address_hash || '',
tag: data?.name || '',
......@@ -118,7 +107,7 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd
}
}
const { mutate } = useMutation({
const { mutateAsync } = useMutation({
mutationFn: updateWatchlist,
onSuccess: async() => {
await onSuccess();
......@@ -128,109 +117,85 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd
setPending(false);
const errorMap = error.payload?.errors;
if (errorMap?.address_hash || errorMap?.name) {
errorMap?.address_hash && setError('address', { type: 'custom', message: getErrorMessage(errorMap, 'address_hash') });
errorMap?.name && setError('tag', { type: 'custom', message: getErrorMessage(errorMap, 'name') });
errorMap?.address_hash && formApi.setError('address', { type: 'custom', message: getErrorMessage(errorMap, 'address_hash') });
errorMap?.name && formApi.setError('tag', { type: 'custom', message: getErrorMessage(errorMap, 'name') });
} 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 {
setAlertVisible(true);
}
},
});
const onSubmit: SubmitHandler<Inputs> = (formData) => {
const onSubmit: SubmitHandler<Inputs> = async(formData) => {
setAlertVisible(false);
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 (
<form noValidate onSubmit={ handleSubmit(onSubmit) }>
<Box marginBottom={ 5 }>
<Controller
<FormProvider { ...formApi }>
<form noValidate onSubmit={ formApi.handleSubmit(onSubmit) }>
<FormFieldAddress<Inputs>
name="address"
control={ control }
rules={{
pattern: ADDRESS_REGEXP,
required: true,
}}
render={ renderAddressInput }
isRequired
bgColor="dialog_bg"
mb={ 5 }
/>
</Box>
<Box marginBottom={ 8 }>
<Controller
<FormFieldText<Inputs>
name="tag"
control={ control }
placeholder="Private tag (max 35 characters)"
isRequired
rules={{
maxLength: TAG_MAX_LENGTH,
required: true,
}}
render={ renderTagInput }
bgColor="dialog_bg"
mb={ 8 }
/>
</Box>
{ userWithoutEmail ? (
<>
<Alert
status="info"
colorScheme="gray"
display="flex"
flexDirection={{ base: 'column', md: 'row' }}
alignItems={{ base: 'flex-start', lg: 'center' }}
columnGap={ 2 }
rowGap={ 2 }
w="fit-content"
>
{ userWithoutEmail ? (
<>
<Alert
status="info"
colorScheme="gray"
display="flex"
flexDirection={{ base: 'column', md: 'row' }}
alignItems={{ base: 'flex-start', lg: 'center' }}
columnGap={ 2 }
rowGap={ 2 }
w="fit-content"
>
To receive notifications you need to add an email to your profile.
<Button variant="outline" size="sm" onClick={ authModal.onOpen }>Add email</Button>
</Alert>
{ authModal.isOpen && <AuthModal initialScreen={{ type: 'email', isAuth: true }} onClose={ authModal.onClose }/> }
</>
) : (
<>
<Text variant="secondary" fontSize="sm" marginBottom={ 5 }>
<Button variant="outline" size="sm" onClick={ authModal.onOpen }>Add email</Button>
</Alert>
{ authModal.isOpen && <AuthModal initialScreen={{ type: 'email', isAuth: true }} onClose={ authModal.onClose }/> }
</>
) : (
<>
<Text variant="secondary" fontSize="sm" marginBottom={ 5 }>
Please select what types of notifications you will receive
</Text>
<Box marginBottom={ 8 }>
<AddressFormNotifications control={ control }/>
</Box>
<Text variant="secondary" fontSize="sm" marginBottom={{ base: '10px', lg: 5 }}>Notification methods</Text>
<Controller
name={ 'notification' as Checkboxes }
control={ control }
render={ renderCheckbox('Email notifications') }
/>
</>
) }
<Box marginTop={ 8 }>
<Button
size="lg"
type="submit"
isLoading={ pending }
isDisabled={ !isDirty }
>
{ !isAdd ? 'Save changes' : 'Add address' }
</Button>
</Box>
</form>
</Text>
<Box marginBottom={ 8 }>
<AddressFormNotifications/>
</Box>
<Text variant="secondary" fontSize="sm" marginBottom={{ base: '10px', lg: 5 }}>Notification methods</Text>
<FormFieldCheckbox<Inputs, 'notification'>
name="notification"
label="Email notifications"
/>
</>
) }
<Box marginTop={ 8 }>
<Button
size="lg"
type="submit"
isLoading={ pending }
isDisabled={ !formApi.formState.isDirty }
>
{ !isAdd ? 'Save changes' : 'Add address' }
</Button>
</Box>
</form>
</FormProvider>
);
};
......
import { Grid, GridItem } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import { Controller } from 'react-hook-form';
import type { Path, ControllerRenderProps, FieldValues, Control } from 'react-hook-form';
import React from 'react';
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;
......@@ -15,21 +15,12 @@ const NOTIFICATIONS_NAMES = [
`${ tokenStandardName }-721, ${ tokenStandardName }-1155 (NFT)`,
`${ tokenStandardName }-404` ];
type Props<Inputs extends FieldValues> = {
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 }/>
), []);
export default function AddressFormNotifications() {
return (
<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) => {
const incomingFieldName = `notification_settings.${ notification }.incoming` as Checkboxes;
const outgoingFieldName = `notification_settings.${ notification }.outcoming` as Checkboxes;
{ NOTIFICATIONS.map((notification, index: number) => {
const incomingFieldName = `notification_settings.${ notification }.incoming` as const;
const outgoingFieldName = `notification_settings.${ notification }.outcoming` as const;
return (
<React.Fragment key={ notification }>
<GridItem
......@@ -42,19 +33,15 @@ export default function AddressFormNotifications<Inputs extends FieldValues, Che
{ NOTIFICATIONS_NAMES[index] }
</GridItem>
<GridItem>
<Controller
<FormFieldCheckbox<FormFields, typeof incomingFieldName>
name={ incomingFieldName }
control={ control }
render={ renderCheckbox('Incoming') }
label="Incoming"
/>
</GridItem>
<GridItem>
<Controller
<FormFieldCheckbox<FormFields, typeof outgoingFieldName>
name={ outgoingFieldName }
control={ control }
render={ renderCheckbox('Outgoing') }
label="Outgoing"
/>
</GridItem>
</React.Fragment>
......
......@@ -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"
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:
version "6.5.7"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.7.tgz#8ec4da2cb2939926a1b9a73619d768207e647c8b"
......@@ -11947,12 +11934,7 @@ json5@^1.0.2:
dependencies:
minimist "^1.2.0"
json5@^2.2.1:
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:
json5@^2.2.1, json5@^2.2.2, json5@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
......@@ -13900,10 +13882,10 @@ react-google-recaptcha-v3@1.10.1:
dependencies:
hoist-non-react-statics "^3.3.2"
react-hook-form@^7.33.1:
version "7.37.0"
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.37.0.tgz#4d1738f092d3d8a3ade34ee892d97350b1032b19"
integrity sha512-6NFTxsnw+EXSpNNvLr5nFMjPdYKRryQcelTHg7zwBB6vAzfPIcZq4AExP4heVlwdzntepQgwiOQW4z7Mr99Lsg==
react-hook-form@7.52.1:
version "7.52.1"
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.52.1.tgz#ec2c96437b977f8b89ae2d541a70736c66284852"
integrity sha512-uNKIhaoICJ5KQALYZ4TOaOLElyM+xipord+Ha3crEFhTntdLvWZqVY49Wqd/0GiVCA/f9NjemLeiNPjG7Hpurg==
react-identicons@^1.2.5:
version "1.2.5"
......@@ -14581,11 +14563,11 @@ scslre@^0.1.6:
regexpp "^3.2.0"
secp256k1@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-5.0.0.tgz#be6f0c8c7722e2481e9773336d351de8cddd12f7"
integrity sha512-TKWX8xvoGHrxVdqbYeZM9w+izTF4b9z3NhSaDkdn81btvuh+ivbIMGT/zQvDtTFWhRlThpoz6LEYTr7n8A5GcA==
version "5.0.1"
resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-5.0.1.tgz#dc2c86187d48ff2da756f0f7e96417ee03c414b1"
integrity "sha1-3CyGGH1I/y2nVvD36WQX7gPEFLE= sha512-lDFs9AAIaWP9UCdtWrotXWWF9t8PWgQDcxqgAnpM9rMqxb3Oaq2J0thzPVSxBwdJgyQtkU/sYtFtbM1RSt/iYA=="
dependencies:
elliptic "^6.5.4"
elliptic "^6.5.7"
node-addon-api "^5.0.0"
node-gyp-build "^4.2.0"
......@@ -14926,7 +14908,16 @@ string-length@^4.0.1:
char-regex "^1.0.2"
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"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
......@@ -15054,7 +15045,14 @@ string_decoder@~1.1.1:
dependencies:
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"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
......@@ -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"
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"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
......@@ -16209,6 +16207,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.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:
version "8.1.0"
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