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_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
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
This diff is collapsed.
This diff is collapsed.
......@@ -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';
......
This diff is collapsed.
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,18 +90,33 @@ const ContractSubmitAuditForm = ({ address, onSuccess }: Props) => {
}, [ apiFetch, address, toast, setError, onSuccess ]);
return (
<FormProvider { ...formApi }>
<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 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"
......@@ -118,6 +129,7 @@ const ContractSubmitAuditForm = ({ address, onSuccess }: Props) => {
Send request
</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);
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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