Merge branch 'main' of github.com:blockscout/frontend into csv-download
@@ -1,2 +1,4 @@
|
||||
NEXT_PUBLIC_SENTRY_DSN=xxx
|
||||
SENTRY_CSP_REPORT_URI=xxx
|
||||
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
|
||||
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
|
||||
@@ -48,9 +48,14 @@ NEXT_PUBLIC_API_PROTOCOL=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_PROTOCOL__
|
||||
NEXT_PUBLIC_API_PORT=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_PORT__
|
||||
NEXT_PUBLIC_STATS_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_STATS_API_HOST__
|
||||
NEXT_PUBLIC_VISUALIZE_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_VISUALIZE_API_HOST__
|
||||
NEXT_PUBLIC_API_SPEC_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_SPEC_URL__
|
||||
|
||||
# external services config
|
||||
NEXT_PUBLIC_SENTRY_DSN=__PLACEHOLDER_FOR_NEXT_PUBLIC_SENTRY_DSN__
|
||||
NEXT_PUBLIC_AUTH0_CLIENT_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_AUTH0_CLIENT_ID__
|
||||
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID__
|
||||
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=__PLACEHOLDER_FOR_NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY__
|
||||
|
||||
# l2 config
|
||||
NEXT_PUBLIC_IS_L2_NETWORK=__PLACEHOLDER_FOR_NEXT_PUBLIC_IS_L2_NETWORKL__
|
||||
NEXT_PUBLIC_L1_BASE_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_L1_BASE_URL__
|
||||
|
||||
@@ -251,7 +251,7 @@
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "shared",
|
||||
"close": true,
|
||||
"close": false,
|
||||
"revealProblems": "onProblem",
|
||||
"focus": true,
|
||||
},
|
||||
|
||||
@@ -46,7 +46,7 @@ The app instance could be customized by passing following variables to NodeJS en
|
||||
| NEXT_PUBLIC_NETWORK_SHORT_NAME | `string` *(optional)* | Used for SEO attributes (page title and description) | `OoG` |
|
||||
| NEXT_PUBLIC_NETWORK_TYPE | `string` *(optional)* | Network type (used for matching pre-defined assets, e.g network logo and icon, which are stored in the project). See all possible values here | `xdai_mainnet` |
|
||||
| NEXT_PUBLIC_NETWORK_ID | `number` | Chain id, see [https://chainlist.org/](https://chainlist.org/) for the reference | `99` |
|
||||
| NEXT_PUBLIC_NETWORK_RPC_URL | `string` | Chain server RPC url, see [https://chainlist.org/](https://chainlist.org/) for the reference | `https://core.poa.network` |
|
||||
| NEXT_PUBLIC_NETWORK_RPC_URL | `string` *(optional)* | Chain server RPC url, see [https://chainlist.org/](https://chainlist.org/) for the reference. If not provided, some functionality of the explorer, related to smart contracts interaction and third-party apps integration, will be unavailable | `https://core.poa.network` |
|
||||
| NEXT_PUBLIC_NETWORK_CURRENCY_NAME | `string` | Network currency name | `Ether` |
|
||||
| NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | Network currency symbol | `ETH` |
|
||||
| NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS | `string` | Network currency decimals | `18` |
|
||||
@@ -79,6 +79,7 @@ The app instance could be customized by passing following variables to NodeJS en
|
||||
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` *(optional)* | Set to false if average block time is useless for the network | `true` |
|
||||
| NEXT_PUBLIC_DOMAIN_WITH_AD | `string` *(optional)* | The domain on which we display ads | `blockscout.com` |
|
||||
| NEXT_PUBLIC_AD_ADBUTLER_ON | `boolean` *(optional)* | Set to true to show Adbutler banner instead of Coinzilla banner | `false` |
|
||||
| NEXT_PUBLIC_API_SPEC_URL | `string` *(optional)* | Spec to be displayed on api-docs page | `https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml` |
|
||||
### App configuration
|
||||
|
||||
| Variable | Type | Description | Default value
|
||||
@@ -129,6 +130,12 @@ The app instance could be customized by passing following variables to NodeJS en
|
||||
| NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID | `string` | Project id for [WalletConnect](https://docs.walletconnect.com/2.0/web3modal/react/installation#obtain-project-id) integration | `<secret>` |
|
||||
| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | Site key for [reCAPTCHA](https://developers.google.com/recaptcha) service | `<secret>` |
|
||||
|
||||
### L2 configuration
|
||||
| Variable | Type | Description | Default value
|
||||
| --- | --- | --- | --- |
|
||||
| NEXT_PUBLIC_IS_L2_NETWORK | `boolean` *(optional)* | Set to true for L2 solutions (Optimism Bedrock based) | false |
|
||||
| NEXT_PUBLIC_L1_BASE_URL | `string` *(optional)* | Base Blockscout URL for L1 network | `'http://eth-goerli.blockscout.com'` |
|
||||
|
||||
### Marketplace app configuration properties
|
||||
|
||||
| Property | Type | Description | Example value
|
||||
|
||||
@@ -104,6 +104,10 @@ const config = Object.freeze({
|
||||
socket: apiHost ? `wss://${ apiHost }` : 'wss://blockscout.com',
|
||||
basePath: stripTrailingSlash(getEnvValue(process.env.NEXT_PUBLIC_API_BASE_PATH) || ''),
|
||||
},
|
||||
L2: {
|
||||
isL2Network: getEnvValue(process.env.NEXT_PUBLIC_IS_L2_NETWORK) === 'true',
|
||||
L1BaseUrl: getEnvValue(process.env.NEXT_PUBLIC_L1_BASE_URL),
|
||||
},
|
||||
statsApi: {
|
||||
endpoint: getEnvValue(process.env.NEXT_PUBLIC_STATS_API_HOST),
|
||||
basePath: '',
|
||||
@@ -122,6 +126,9 @@ const config = Object.freeze({
|
||||
walletConnect: {
|
||||
projectId: getEnvValue(process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID),
|
||||
},
|
||||
apiDoc: {
|
||||
specUrl: getEnvValue(process.env.NEXT_PUBLIC_API_SPEC_URL),
|
||||
},
|
||||
reCaptcha: {
|
||||
siteKey: getEnvValue(process.env.NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY) || '',
|
||||
},
|
||||
|
||||
@@ -9,6 +9,8 @@ NEXT_PUBLIC_APP_ENV=development
|
||||
NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta
|
||||
NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
|
||||
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
|
||||
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
|
||||
|
||||
|
||||
# api config
|
||||
NEXT_PUBLIC_API_HOST=blockscout.com
|
||||
|
||||
@@ -40,6 +40,10 @@ blockscout:
|
||||
_default: ENC[AES256_GCM,data:mTY6sjNKZ+VEvH47eyyoUHt//beWvuxyreu+1WrmMvkdkwR6jgXnSXh+1pTuiG8e3it96Egxraz0hjZxlkY9kSmE91/dfoqKTns=,iv:op8OuOXXeBmmUnVkCL14j4HrfjHHoN3n2XZqLdg03Mw=,tag:bbFQYi0Aa1sUdfoUjuQ/+w==,type:str]
|
||||
ACCOUNT_AUTH0_LOGOUT_URL:
|
||||
_default: ENC[AES256_GCM,data:IZFfi6pn+hy7g0wnEtP9TYHH1fNiC2gqgRHVdgm4C9smPerEvS0pq9dBwVY=,iv:BxbSInFQ6GE2loTv+IzdYr25PlyzdWZI1wdT6r+uvBg=,tag:psujCU372k59OGmlRdH9Fg==,type:str]
|
||||
RE_CAPTCHA_SECRET_KEY:
|
||||
_default: ENC[AES256_GCM,data:e9jfs4PwY+UPk0EuXZxERylKUFSUo6zsAz8oly3YwwybipP+A5rSBQ==,iv:TvrD/a+6+11IGDVFLUCn8U+H3v1YfIRrcndOVdt/taI=,tag:OAFVIznkeeeVX9UsigfP4A==,type:str]
|
||||
RE_CAPTCHA_CLIENT_KEY:
|
||||
_default: ENC[AES256_GCM,data:6GPj8CQddvUofViL26AIBsVeMaYyrQ1Mh0kYUJ0HiIJc0otDewUc2Q==,iv:1LR4GBdSqN6ciKNCRI67d8oPWsTXnVI55hPSMvLL4hE=,tag:58pE8s9X7+eMOCY6bl4Z5w==,type:str]
|
||||
scVerifier:
|
||||
environment:
|
||||
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__ACCESS_KEY:
|
||||
@@ -134,8 +138,8 @@ sops:
|
||||
azure_kv: []
|
||||
hc_vault: []
|
||||
age: []
|
||||
lastmodified: "2023-02-14T08:03:14Z"
|
||||
mac: ENC[AES256_GCM,data:B2AkQb4I83dP3UUitRCcrUfzm3nWmcknIUoMWHyYaG9jasnccbr8zZatYdpbvKFcELVTtjhYk6ly5Sx7+6sk2PZm6o7dN3yHG5lSWmnZqNXkwo42GIk/F6vzDdLutZsu8HH8pWHd9y5R272CIPOOh4+Ur0OtwiGgj3Bp1od76qM=,iv:j7aIPflH0FsYhE/iylvBh5nDmVdghhxAFvaeXlR560k=,tag:/oe6OeitIHaZ4TgM7w/0pg==,type:str]
|
||||
lastmodified: "2023-02-19T10:34:54Z"
|
||||
mac: ENC[AES256_GCM,data:c7Nlguw+82tpgNAclx6XgXQip+gHh37Wrb9pq0MQ+bgp3LmfWpNDYYtxvdkIxMPNYsLxQOvw9Kc26qYWY1qDggmkHFvIeImCScjxBmrjSAf1K61lk0NIQ7AnEVh7ahipAopUO7y/0ogcjyjJCGs/QPRg9yelsONhCIjRkeUi7mQ=,iv:Y5tWWJJCqdNujHczKnouk4TiHBwMBezKhKCmvWeL9Kc=,tag:hIjUobEooUAQuKwHFraryw==,type:str]
|
||||
pgp:
|
||||
- created_at: "2022-09-14T13:42:28Z"
|
||||
enc: |
|
||||
|
||||
@@ -491,6 +491,7 @@ frontend:
|
||||
- "/token"
|
||||
- "/accounts"
|
||||
- "/visualize"
|
||||
- "/api-docs"
|
||||
- "/csv-export"
|
||||
|
||||
resources:
|
||||
@@ -572,3 +573,5 @@ frontend:
|
||||
_default: https://rpc.ankr.com/eth_goerli
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS:
|
||||
_default: "['daily_txs','coin_price','market_cup']"
|
||||
NEXT_PUBLIC_API_SPEC_URL:
|
||||
_default: https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
|
||||
|
||||
@@ -327,6 +327,7 @@ frontend:
|
||||
- "/tokens"
|
||||
- "/accounts"
|
||||
- "/visualize"
|
||||
- "/api-docs"
|
||||
- "/csv-export"
|
||||
|
||||
resources:
|
||||
@@ -409,3 +410,5 @@ frontend:
|
||||
_default: https://rpc.ankr.com/eth_goerli
|
||||
NEXT_PUBLIC_NETWORK_EXPLORERS:
|
||||
_default: "[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/ethereum/goerli/transaction','address':'/ethereum/ethereum/goerli/address'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address'}}]"
|
||||
NEXT_PUBLIC_API_SPEC_URL:
|
||||
_default: https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 51 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.693 5.058c.498-.57 1.172-.891 1.875-.891h15.91c.351 0 .688.16.937.446l9.28 10.657c.249.285.388.672.388 1.076v24.36c0 .807-.279 1.581-.776 2.152-.498.571-1.172.892-1.875.892H13.568c-.703 0-1.377-.32-1.875-.892-.497-.57-.776-1.345-.776-2.153V7.212c0-.808.28-1.582.776-2.154Zm17.235 2.154h-15.36v33.493h23.864V16.977l-8.504-9.765Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.556 5a1.39 1.39 0 0 1 1.389 1.389v8.333h8.333a1.389 1.389 0 0 1 0 2.778h-9.722a1.389 1.389 0 0 1-1.39-1.389V6.39a1.39 1.39 0 0 1 1.39-1.389ZM22.46 25.151a1.326 1.326 0 0 0-1.875 1.875l3.04 3.04-3.04 3.04a1.326 1.326 0 0 0 1.875 1.875l3.04-3.04 3.04 3.04a1.326 1.326 0 0 0 1.875-1.875l-3.04-3.04 3.04-3.04a1.326 1.326 0 0 0-1.875-1.875l-3.04 3.04-3.04-3.04Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 921 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 30 30">
|
||||
<path fill="currentColor" fill-rule="evenodd" d="M14.688 4.75c-5.482 0-9.938 4.456-9.938 9.938 0 5.481 4.456 9.937 9.938 9.937 5.481 0 9.937-4.456 9.937-9.938 0-5.481-4.456-9.937-9.938-9.937Zm3.499 5.781a14.655 14.655 0 0 0-1.813-3.855 8.151 8.151 0 0 1 5.359 3.855h-3.546Zm-7.219 0H7.644a8.16 8.16 0 0 1 5.094-3.787 14.079 14.079 0 0 0-1.77 3.787Zm5.39 0h-3.56a12.705 12.705 0 0 1 1.78-3.382 12.64 12.64 0 0 1 1.78 3.382ZM6.5 14.687c0-.831.131-1.65.369-2.406h3.692a14.62 14.62 0 0 0-.013 4.737l-3.695.027a7.864 7.864 0 0 1-.353-2.358Zm5.83 2.327c-.3-1.57-.3-3.176 0-4.733h4.496c.3 1.555.3 3.147.028 4.705l-4.524.028Zm6.295-.047a14.364 14.364 0 0 0-.029-4.686h3.91c.238.756.369 1.575.369 2.406 0 .783-.116 1.542-.313 2.252l-3.937.028Zm-2.246 5.73a14.004 14.004 0 0 0 1.843-3.98l3.596-.026a8.21 8.21 0 0 1-5.439 4.005Zm-1.802-.472a12.283 12.283 0 0 1-1.8-3.462l3.613-.026a12.946 12.946 0 0 1-1.813 3.488Zm-1.84.406a8.208 8.208 0 0 1-5.128-3.837l3.328-.027c.394 1.347 1 2.657 1.8 3.864Z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,10 @@
|
||||
<svg viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#top-accounts_svg__a)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.585 2.973a3.412 3.412 0 0 0-3.412 3.412v17.23a3.412 3.412 0 0 0 3.412 3.412h17.23a.95.95 0 0 0 0-1.9H7.585a1.512 1.512 0 1 1 0-3.023h17.23a.95.95 0 0 0 .934-1.126.954.954 0 0 0 .016-.176V4.662c0-.933-.756-1.689-1.688-1.689H7.585ZM6.073 6.385c0-.835.677-1.512 1.512-1.512h16.28v15.33H7.585a3.4 3.4 0 0 0-1.512.353V6.385Zm8.897 1.4a3.013 3.013 0 1 0 0 6.026 3.013 3.013 0 0 0 0-6.026Zm-1.024 3.013a1.024 1.024 0 1 1 2.048 0 1.024 1.024 0 0 1-2.048 0Zm1.025 3.097c-2.316 0-4.3 1.303-5.319 3.243a.446.446 0 0 0 .395.654h1.281a.446.446 0 0 0 .366-.19 3.972 3.972 0 0 1 3.277-1.718c1.33 0 2.52.685 3.268 1.723a.446.446 0 0 0 .362.185h1.292a.446.446 0 0 0 .393-.659c-1.01-1.866-2.983-3.238-5.315-3.238Z" fill="currentColor"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="top-accounts_svg__a">
|
||||
<path fill="#fff" transform="translate(3 3)" d="M0 0h24v24H0z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,10 @@
|
||||
<svg viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#verified_svg__a)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.4 15a8.4 8.4 0 1 1-16.8 0 8.4 8.4 0 0 1 16.8 0Zm1.6 0c0 5.523-4.477 10-10 10S5 20.523 5 15 9.477 5 15 5s10 4.477 10 10Zm-5.895-3.706a.916.916 0 1 1 1.295 1.295l-6.022 6.022a1.05 1.05 0 0 1-1.485 0l-3.2-3.199a.915.915 0 0 1 1.296-1.295l2.257 2.258a.55.55 0 0 0 .778 0l5.081-5.081Z" fill="currentColor"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="verified_svg__a">
|
||||
<path fill="#fff" transform="translate(5 5)" d="M0 0h20v20H0z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 629 B |
@@ -16,22 +16,21 @@ const MAIN_DOMAINS = [ `*.${ appConfig.host }`, appConfig.host ];
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
const REPORT_URI = process.env.SENTRY_CSP_REPORT_URI;
|
||||
|
||||
function getNetworksExternalAssets() {
|
||||
function getNetworksExternalAssetsHosts() {
|
||||
const icons = featuredNetworks
|
||||
.filter(({ icon }) => typeof icon === 'string')
|
||||
.map(({ icon }) => new URL(icon as string));
|
||||
.map(({ icon }) => new URL(icon as string).host);
|
||||
|
||||
const logo = appConfig.network.logo ? new URL(appConfig.network.logo) : undefined;
|
||||
const logo = appConfig.network.logo ? new URL(appConfig.network.logo).host : undefined;
|
||||
|
||||
return logo ? icons.concat(logo) : icons;
|
||||
}
|
||||
|
||||
function getMarketplaceAppsOrigins() {
|
||||
return appConfig.marketplaceAppList.map(({ url }) => url);
|
||||
}
|
||||
|
||||
function getMarketplaceAppsLogosOrigins() {
|
||||
return appConfig.marketplaceAppList.map(({ logo }) => new URL(logo));
|
||||
function getMarketplaceAppsHosts() {
|
||||
return {
|
||||
frames: appConfig.marketplaceAppList.map(({ url }) => new URL(url).host),
|
||||
logos: appConfig.marketplaceAppList.map(({ logo }) => new URL(logo).host),
|
||||
};
|
||||
}
|
||||
|
||||
// we cannot use lodash/uniq in middleware code since it calls new Set() and it'is causing an error in Nextjs
|
||||
@@ -46,7 +45,7 @@ function unique(array: Array<string | undefined>) {
|
||||
}
|
||||
|
||||
function makePolicyMap() {
|
||||
const networkExternalAssets = getNetworksExternalAssets();
|
||||
const marketplaceAppsHosts = getMarketplaceAppsHosts();
|
||||
|
||||
return {
|
||||
'default-src': [
|
||||
@@ -135,10 +134,10 @@ function makePolicyMap() {
|
||||
'avatars.githubusercontent.com', // github avatars
|
||||
|
||||
// network assets
|
||||
...networkExternalAssets.map((url) => url.host),
|
||||
...getNetworksExternalAssetsHosts(),
|
||||
|
||||
// marketplace apps logos
|
||||
...getMarketplaceAppsLogosOrigins().map((url) => url.host),
|
||||
...marketplaceAppsHosts.logos,
|
||||
|
||||
// ad
|
||||
'servedbyadbutler.com',
|
||||
@@ -172,7 +171,7 @@ function makePolicyMap() {
|
||||
],
|
||||
|
||||
'frame-src': [
|
||||
...getMarketplaceAppsOrigins(),
|
||||
...marketplaceAppsHosts.frames,
|
||||
|
||||
// ad
|
||||
'request-global.czilladx.com',
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { Address } from 'types/api/address';
|
||||
|
||||
import notEmpty from 'lib/notEmpty';
|
||||
import ContractCode from 'ui/address/contract/ContractCode';
|
||||
import ContractRead from 'ui/address/contract/ContractRead';
|
||||
import ContractWrite from 'ui/address/contract/ContractWrite';
|
||||
|
||||
export default function useContractTabs(data: Address | undefined) {
|
||||
return React.useMemo(() => {
|
||||
return [
|
||||
{ id: 'contact_code', title: 'Code', component: <ContractCode addressHash={ data?.hash }/> },
|
||||
// this is not implemented in api yet
|
||||
// data?.has_decompiled_code ?
|
||||
// { id: 'contact_decompiled_code', title: 'Decompiled code', component: <div>Decompiled code</div> } :
|
||||
// undefined,
|
||||
data?.has_methods_read ?
|
||||
{ id: 'read_contract', title: 'Read contract', component: <ContractRead addressHash={ data?.hash }/> } :
|
||||
undefined,
|
||||
data?.has_methods_read_proxy ?
|
||||
{ id: 'read_proxy', title: 'Read proxy', component: <ContractRead addressHash={ data?.hash } isProxy/> } :
|
||||
undefined,
|
||||
data?.has_custom_methods_read ?
|
||||
{ id: 'read_custom_methods', title: 'Read custom', component: <ContractRead addressHash={ data?.hash } isCustomAbi/> } :
|
||||
undefined,
|
||||
data?.has_methods_write ?
|
||||
{ id: 'write_contract', title: 'Write contract', component: <ContractWrite addressHash={ data?.hash }/> } :
|
||||
undefined,
|
||||
data?.has_methods_write_proxy ?
|
||||
{ id: 'write_proxy', title: 'Write proxy', component: <ContractWrite addressHash={ data?.hash } isProxy/> } :
|
||||
undefined,
|
||||
data?.has_custom_methods_write ?
|
||||
{ id: 'write_custom_methods', title: 'Write custom', component: <ContractWrite addressHash={ data?.hash } isCustomAbi/> } :
|
||||
undefined,
|
||||
].filter(notEmpty);
|
||||
}, [ data ]);
|
||||
}
|
||||
@@ -14,14 +14,6 @@ export default function useGetCsrfToken() {
|
||||
const url = buildUrl('csrf');
|
||||
const apiResponse = await fetch(url, { credentials: 'include' });
|
||||
const csrfFromHeader = apiResponse.headers.get('x-bs-account-csrf');
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('>>> RESPONSE HEADERS <<<');
|
||||
// eslint-disable-next-line no-console
|
||||
console.table([ {
|
||||
'content-length': apiResponse.headers.get('content-length'),
|
||||
'x-bs-account-csrf': csrfFromHeader,
|
||||
} ]);
|
||||
|
||||
return csrfFromHeader ? { token: csrfFromHeader } : undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,14 +7,16 @@ import abiIcon from 'icons/ABI.svg';
|
||||
import apiKeysIcon from 'icons/API.svg';
|
||||
import appsIcon from 'icons/apps.svg';
|
||||
import blocksIcon from 'icons/block.svg';
|
||||
// import gearIcon from 'icons/gear.svg';
|
||||
import gearIcon from 'icons/gear.svg';
|
||||
import globeIcon from 'icons/globe-b.svg';
|
||||
import privateTagIcon from 'icons/privattags.svg';
|
||||
import profileIcon from 'icons/profile.svg';
|
||||
import publicTagIcon from 'icons/publictags.svg';
|
||||
import statsIcon from 'icons/stats.svg';
|
||||
import tokensIcon from 'icons/token.svg';
|
||||
import topAccountsIcon from 'icons/top-accounts.svg';
|
||||
import transactionsIcon from 'icons/transactions.svg';
|
||||
import walletIcon from 'icons/wallet.svg';
|
||||
// import verifiedIcon from 'icons/verified.svg';
|
||||
import watchlistIcon from 'icons/watchlist.svg';
|
||||
import notEmpty from 'lib/notEmpty';
|
||||
|
||||
@@ -26,31 +28,64 @@ export interface NavItem {
|
||||
isNewUi?: boolean;
|
||||
}
|
||||
|
||||
export interface NavGroupItem extends Omit<NavItem, 'nextRoute'> {
|
||||
subItems: Array<NavItem>;
|
||||
}
|
||||
|
||||
interface ReturnType {
|
||||
mainNavItems: Array<NavItem>;
|
||||
mainNavItems: Array<NavItem | NavGroupItem>;
|
||||
accountNavItems: Array<NavItem>;
|
||||
profileItem: NavItem;
|
||||
}
|
||||
|
||||
export function isGroupItem(item: NavItem | NavGroupItem): item is NavGroupItem {
|
||||
return 'subItems' in item;
|
||||
}
|
||||
|
||||
export default function useNavItems(): ReturnType {
|
||||
const isMarketplaceFilled = appConfig.marketplaceAppList.length > 0;
|
||||
const isMarketplaceFilled = appConfig.marketplaceAppList.length > 0 && appConfig.network.rpcUrl;
|
||||
const hasAPIDocs = appConfig.apiDoc.specUrl;
|
||||
|
||||
const router = useRouter();
|
||||
const pathname = router.pathname;
|
||||
|
||||
return React.useMemo(() => {
|
||||
const mainNavItems = [
|
||||
const blockchainNavItems: Array<NavItem> = [
|
||||
{ text: 'Top accounts', nextRoute: { pathname: '/accounts' as const }, icon: topAccountsIcon, isActive: pathname === '/accounts', isNewUi: true },
|
||||
{ text: 'Blocks', nextRoute: { pathname: '/blocks' as const }, icon: blocksIcon, isActive: pathname.startsWith('/block'), isNewUi: true },
|
||||
{ text: 'Transactions', nextRoute: { pathname: '/txs' as const }, icon: transactionsIcon, isActive: pathname.startsWith('/tx'), isNewUi: true },
|
||||
// eslint-disable-next-line max-len
|
||||
// { text: 'Verified contracts', nextRoute: { pathname: '/verified_contracts' as const }, icon: verifiedIcon, isActive: pathname === '/verified_contracts', isNewUi: false },
|
||||
];
|
||||
|
||||
const otherNavItems: Array<NavItem> = [
|
||||
hasAPIDocs ? {
|
||||
text: 'API documentation',
|
||||
nextRoute: { pathname: '/api-docs' as const },
|
||||
// FIXME: need icon for this item
|
||||
icon: topAccountsIcon,
|
||||
isActive: pathname === '/api-docs',
|
||||
isNewUi: true,
|
||||
} : null,
|
||||
].filter(notEmpty);
|
||||
|
||||
const mainNavItems = [
|
||||
{
|
||||
text: 'Blockchain',
|
||||
icon: globeIcon,
|
||||
isActive: blockchainNavItems.some(item => item.isActive),
|
||||
isNewUi: true,
|
||||
subItems: blockchainNavItems,
|
||||
},
|
||||
{ text: 'Tokens', nextRoute: { pathname: '/tokens' as const }, icon: tokensIcon, isActive: pathname.startsWith('/token'), isNewUi: true },
|
||||
{ text: 'Accounts', nextRoute: { pathname: '/accounts' as const }, icon: walletIcon, isActive: pathname === '/accounts', isNewUi: true },
|
||||
isMarketplaceFilled ?
|
||||
{ text: 'Apps', nextRoute: { pathname: '/apps' as const }, icon: appsIcon, isActive: pathname.startsWith('/app'), isNewUi: true } : null,
|
||||
{ text: 'Charts & stats', nextRoute: { pathname: '/stats' as const }, icon: statsIcon, isActive: pathname === '/stats', isNewUi: true },
|
||||
// there should be custom site sections like Stats, Faucet, More, etc but never an 'other'
|
||||
// examples https://explorer-edgenet.polygon.technology/ and https://explorer.celo.org/
|
||||
// at this stage custom menu items is under development, we will implement it later
|
||||
// { text: 'Other', url: link('other'), icon: gearIcon, isActive: pathname === 'other' },
|
||||
otherNavItems.length > 0 ?
|
||||
{ text: 'Other', icon: gearIcon, isActive: otherNavItems.some(item => item.isActive), subItems: otherNavItems } : null,
|
||||
].filter(notEmpty);
|
||||
|
||||
const accountNavItems = [
|
||||
@@ -87,5 +122,5 @@ export default function useNavItems(): ReturnType {
|
||||
text: 'My profile', nextRoute: { pathname: '/auth/profile' as const }, icon: profileIcon, isActive: pathname === '/auth/profile', isNewUi: true };
|
||||
|
||||
return { mainNavItems, accountNavItems, profileItem };
|
||||
}, [ isMarketplaceFilled, pathname ]);
|
||||
}, [ hasAPIDocs, isMarketplaceFilled, pathname ]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { GetServerSideProps } from 'next';
|
||||
|
||||
import appConfig from 'configs/app/config';
|
||||
import type { Props } from 'lib/next/getServerSideProps';
|
||||
import { getServerSideProps as getServerSidePropsBase } from 'lib/next/getServerSideProps';
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<Props> = async(args) => {
|
||||
if (!appConfig.L2.isL2Network) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
|
||||
return getServerSidePropsBase(args);
|
||||
};
|
||||
@@ -36,8 +36,10 @@ export function middleware(req: NextRequest) {
|
||||
|
||||
/**
|
||||
* Configure which routes should pass through the Middleware.
|
||||
* Exclude all `_next` urls.
|
||||
*/
|
||||
export const config = {
|
||||
matcher: [ '/', '/:notunderscore((?!_next).+)' ],
|
||||
// matcher: [
|
||||
// '/((?!.*\\.|api\\/|node-api\\/).*)', // exclude all static + api + node-api routes
|
||||
// ],
|
||||
};
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
const withTM = require('next-transpile-modules')([
|
||||
'react-syntax-highlighter',
|
||||
'swagger-client',
|
||||
'swagger-ui-react',
|
||||
]);
|
||||
const withRoutes = require('nextjs-routes/config')({
|
||||
outDir: 'types',
|
||||
});
|
||||
@@ -7,7 +12,7 @@ const headers = require('./configs/nextjs/headers');
|
||||
const redirects = require('./configs/nextjs/redirects');
|
||||
const rewrites = require('./configs/nextjs/rewrites');
|
||||
|
||||
const moduleExports = {
|
||||
const moduleExports = withTM({
|
||||
include: path.resolve(__dirname, 'icons'),
|
||||
reactStrictMode: true,
|
||||
webpack(config, { webpack }) {
|
||||
@@ -34,6 +39,11 @@ const moduleExports = {
|
||||
redirects,
|
||||
headers,
|
||||
output: 'standalone',
|
||||
};
|
||||
api: {
|
||||
// disable body parser since we use next.js api only for local development and as a proxy
|
||||
// otherwise it is impossible to upload large files (over 1Mb)
|
||||
bodyParser: false,
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = withRoutes(moduleExports);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"dev:local": "./node_modules/.bin/dotenv -e ./configs/envs/.env.localhost -e ./configs/envs/.env.secrets next dev | ./node_modules/.bin/pino-pretty",
|
||||
"dev:poa_core": "./node_modules/.bin/dotenv -e ./configs/envs/.env.poa_core -e ./configs/envs/.env.common -e ./configs/envs/.env.secrets next dev | ./node_modules/.bin/pino-pretty",
|
||||
"dev:goerli": "./node_modules/.bin/dotenv -e ./configs/envs/.env.goerli -e ./configs/envs/.env.common -e ./configs/envs/.env.secrets next dev | ./node_modules/.bin/pino-pretty",
|
||||
"dev:goerli:l2test": "./node_modules/.bin/dotenv -e ./configs/envs/.env.goerli.l2test -e ./configs/envs/.env.common -e ./configs/envs/.env.secrets next dev | ./node_modules/.bin/pino-pretty",
|
||||
"build": "next build",
|
||||
"build:docker": "docker build --build-arg GIT_COMMIT_SHA=$(git rev-parse HEAD) -t blockscout ./",
|
||||
"start": "next start",
|
||||
@@ -69,6 +70,7 @@
|
||||
"react-identicons": "^1.2.5",
|
||||
"react-jazzicon": "^1.0.4",
|
||||
"react-scroll": "^1.8.7",
|
||||
"swagger-ui-react": "^4.15.5",
|
||||
"use-font-face-observer": "^1.2.1",
|
||||
"wagmi": "^0.10.6"
|
||||
},
|
||||
@@ -85,6 +87,7 @@
|
||||
"@types/qrcode": "^1.5.0",
|
||||
"@types/react": "18.0.9",
|
||||
"@types/react-dom": "18.0.5",
|
||||
"@types/swagger-ui-react": "^4.11.0",
|
||||
"@types/react-google-recaptcha": "^2.1.5",
|
||||
"@types/ws": "^8.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.27.0",
|
||||
@@ -101,6 +104,7 @@
|
||||
"jest-environment-jsdom": "^29.2.1",
|
||||
"lint-staged": ">=10",
|
||||
"mockdate": "^3.0.5",
|
||||
"next-transpile-modules": "^10.0.0",
|
||||
"playwright": "^1.28.0",
|
||||
"svgo": "^2.8.0",
|
||||
"ts-jest": "^29.0.3",
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { NextPage, GetServerSideProps } from 'next';
|
||||
import Head from 'next/head';
|
||||
import React from 'react';
|
||||
|
||||
import appConfig from 'configs/app/config';
|
||||
import getNetworkTitle from 'lib/networks/getNetworkTitle';
|
||||
import { getServerSideProps as getServerSidePropsBase } from 'lib/next/getServerSideProps';
|
||||
import type { Props } from 'lib/next/getServerSideProps';
|
||||
import SwaggerUI from 'ui/apiDocs/SwaggerUI';
|
||||
import Page from 'ui/shared/Page/Page';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
|
||||
const AppsPage: NextPage = () => {
|
||||
const networkTitle = getNetworkTitle();
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageTitle text="API Documentation"/>
|
||||
<Head><title>{ `API for the ${ networkTitle }` }</title></Head>
|
||||
<SwaggerUI/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppsPage;
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<Props> = async(args) => {
|
||||
if (!appConfig.apiDoc.specUrl) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
|
||||
return getServerSidePropsBase(args);
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { WebSocketServer } from 'ws';
|
||||
|
||||
import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
|
||||
import type { NewBlockSocketResponse } from 'types/api/block';
|
||||
import type { SmartContractVerificationResponse } from 'types/api/contract';
|
||||
|
||||
type ReturnType = () => Promise<WebSocket>;
|
||||
|
||||
@@ -59,6 +60,7 @@ export function sendMessage(socket: WebSocket, channel: Channel, msg: 'token_bal
|
||||
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transaction', payload: { transaction: number }): void;
|
||||
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'pending_transaction', payload: { pending_transaction: number }): void;
|
||||
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'new_block', payload: NewBlockSocketResponse): void;
|
||||
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'verification_result', payload: SmartContractVerificationResponse): void;
|
||||
export function sendMessage(socket: WebSocket, channel: Channel, msg: string, payload: unknown): void {
|
||||
socket.send(JSON.stringify([
|
||||
...channel,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
*,
|
||||
*::after,
|
||||
*::before {
|
||||
transition-delay: 0s !important;
|
||||
transition-duration: 0s !important;
|
||||
animation-delay: -0.0001s !important;
|
||||
animation-duration: 0s !important;
|
||||
animation-play-state: paused !important;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import './fonts.css';
|
||||
import './index.css';
|
||||
import { beforeMount } from '@playwright/experimental-ct-react/hooks';
|
||||
import _defaultsDeep from 'lodash/defaultsDeep';
|
||||
import MockDate from 'mockdate';
|
||||
|
||||
@@ -20,6 +20,9 @@ const semanticTokens = {
|
||||
_dark: 'red.300',
|
||||
},
|
||||
},
|
||||
shadows: {
|
||||
action_bar: '0 4px 4px -4px rgb(0 0 0 / 10%), 0 2px 4px -4px rgb(0 0 0 / 6%)',
|
||||
},
|
||||
};
|
||||
|
||||
export default semanticTokens;
|
||||
|
||||
@@ -42,7 +42,7 @@ export interface TokenInstance {
|
||||
image_url: string | null;
|
||||
animation_url: string | null;
|
||||
external_app_url: string | null;
|
||||
metadata: unknown;
|
||||
metadata: Record<string, unknown> | null;
|
||||
owner: AddressParam;
|
||||
token: TokenInfo;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ declare module "nextjs-routes" {
|
||||
| DynamicRoute<"/address/[hash]", { "hash": string }>
|
||||
| StaticRoute<"/api/csrf">
|
||||
| StaticRoute<"/api/proxy">
|
||||
| StaticRoute<"/api-docs">
|
||||
| DynamicRoute<"/apps/[id]", { "id": string }>
|
||||
| StaticRoute<"/apps">
|
||||
| StaticRoute<"/auth/auth0">
|
||||
|
||||
@@ -76,7 +76,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<Hide below="lg" ssr={ false }>
|
||||
<SkeletonTable columns={ [ '17%', '17%', '16%', '25%', '25%' ] }/>
|
||||
<SkeletonTable columns={ [ '17%', '17%', '16%', '25%', '25%' ] } isLong/>
|
||||
</Hide>
|
||||
<Show below="lg" ssr={ false }>
|
||||
<SkeletonList/>
|
||||
@@ -125,7 +125,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
|
||||
return (
|
||||
<Box>
|
||||
{ query.isPaginationVisible && (
|
||||
<ActionBar mt={ -6 }>
|
||||
<ActionBar mt={ -6 } showShadow={ query.isLoading }>
|
||||
<Pagination ml="auto" { ...query.pagination }/>
|
||||
</ActionBar>
|
||||
) }
|
||||
|
||||
@@ -17,60 +17,74 @@ import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
|
||||
|
||||
interface Props {
|
||||
tabs: Array<RoutedSubTab>;
|
||||
addressHash?: string;
|
||||
}
|
||||
|
||||
export const currentChain: Chain = {
|
||||
id: Number(appConfig.network.id),
|
||||
name: appConfig.network.name || '',
|
||||
network: appConfig.network.name || '',
|
||||
nativeCurrency: {
|
||||
decimals: appConfig.network.currency.decimals,
|
||||
name: appConfig.network.currency.name || '',
|
||||
symbol: appConfig.network.currency.symbol || '',
|
||||
},
|
||||
rpcUrls: {
|
||||
'default': {
|
||||
http: [ appConfig.network.rpcUrl || '' ],
|
||||
},
|
||||
},
|
||||
blockExplorers: {
|
||||
'default': {
|
||||
name: 'Blockscout',
|
||||
url: appConfig.baseUrl,
|
||||
},
|
||||
},
|
||||
};
|
||||
const { wagmiClient, ethereumClient } = (() => {
|
||||
try {
|
||||
const currentChain: Chain = {
|
||||
id: Number(appConfig.network.id),
|
||||
name: appConfig.network.name || '',
|
||||
network: appConfig.network.name || '',
|
||||
nativeCurrency: {
|
||||
decimals: appConfig.network.currency.decimals,
|
||||
name: appConfig.network.currency.name || '',
|
||||
symbol: appConfig.network.currency.symbol || '',
|
||||
},
|
||||
rpcUrls: {
|
||||
'default': {
|
||||
http: [ appConfig.network.rpcUrl || '' ],
|
||||
},
|
||||
},
|
||||
blockExplorers: {
|
||||
'default': {
|
||||
name: 'Blockscout',
|
||||
url: appConfig.baseUrl,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const chains = [ currentChain ];
|
||||
const chains = [ currentChain ];
|
||||
|
||||
const { provider } = configureChains(chains, [
|
||||
walletConnectProvider({ projectId: appConfig.walletConnect.projectId || '' }),
|
||||
]);
|
||||
const wagmiClient = createClient({
|
||||
autoConnect: true,
|
||||
connectors: modalConnectors({ appName: 'web3Modal', chains }),
|
||||
provider,
|
||||
});
|
||||
const { provider } = configureChains(chains, [
|
||||
walletConnectProvider({ projectId: appConfig.walletConnect.projectId || '' }),
|
||||
]);
|
||||
const wagmiClient = createClient({
|
||||
autoConnect: true,
|
||||
connectors: modalConnectors({ appName: 'web3Modal', chains }),
|
||||
provider,
|
||||
});
|
||||
const ethereumClient = new EthereumClient(wagmiClient, chains);
|
||||
|
||||
const ethereumClient = new EthereumClient(wagmiClient, chains);
|
||||
return { wagmiClient, ethereumClient };
|
||||
} catch (error) {
|
||||
return { wagmiClient: undefined, ethereumClient: undefined };
|
||||
}
|
||||
})();
|
||||
|
||||
const TAB_LIST_PROPS = {
|
||||
columnGap: 3,
|
||||
};
|
||||
|
||||
const AddressContract = ({ tabs }: Props) => {
|
||||
const AddressContract = ({ addressHash, tabs }: Props) => {
|
||||
const modalZIndex = useToken<string>('zIndices', 'modal');
|
||||
const web3ModalTheme = useColorModeValue('light', 'dark');
|
||||
|
||||
const noProviderTabs = React.useMemo(() => tabs.filter(({ id }) => id === 'contact_code'), [ tabs ]);
|
||||
if (!wagmiClient || !ethereumClient) {
|
||||
return <RoutedTabs tabs={ noProviderTabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS }/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<WagmiConfig client={ wagmiClient }>
|
||||
<ContractContextProvider>
|
||||
<ContractContextProvider addressHash={ addressHash }>
|
||||
<RoutedTabs tabs={ tabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS }/>
|
||||
</ContractContextProvider>
|
||||
<Web3Modal
|
||||
projectId={ appConfig.walletConnect.projectId }
|
||||
ethereumClient={ ethereumClient }
|
||||
themeZIndex={ Number(modalZIndex) }
|
||||
themeMode={ useColorModeValue('light', 'dark') }
|
||||
themeMode={ web3ModalTheme }
|
||||
themeBackground="themeColor"
|
||||
/>
|
||||
</WagmiConfig>
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as countersMock from 'mocks/address/counters';
|
||||
import * as tokensMock from 'mocks/address/tokens';
|
||||
import TestApp from 'playwright/TestApp';
|
||||
import buildApiUrl from 'playwright/utils/buildApiUrl';
|
||||
import insertAdPlaceholder from 'playwright/utils/insertAdPlaceholder';
|
||||
|
||||
import AddressDetails from './AddressDetails';
|
||||
import MockAddressPage from './testUtils/MockAddressPage';
|
||||
@@ -44,6 +45,8 @@ test('contract +@mobile', async({ mount, page }) => {
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
await insertAdPlaceholder(page);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
@@ -82,6 +85,8 @@ test('token', async({ mount, page }) => {
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
await insertAdPlaceholder(page);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
@@ -102,5 +107,7 @@ test('validator +@mobile', async({ mount, page }) => {
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
await insertAdPlaceholder(page);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import AddressLink from 'ui/shared/address/AddressLink';
|
||||
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
|
||||
import DataFetchAlert from 'ui/shared/DataFetchAlert';
|
||||
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
|
||||
import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem';
|
||||
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
|
||||
import LinkExternal from 'ui/shared/LinkExternal';
|
||||
import LinkInternal from 'ui/shared/LinkInternal';
|
||||
@@ -92,7 +93,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
|
||||
<Text fontSize="sm">Verify with other explorers</Text>
|
||||
{ explorers.map((explorer) => {
|
||||
const url = new URL(explorer.paths.address + '/' + addressHash, explorer.baseUrl);
|
||||
return <LinkExternal key={ explorer.baseUrl } title={ explorer.title } href={ url.toString() }/>;
|
||||
return <LinkExternal key={ explorer.baseUrl } href={ url.toString() }>{ explorer.title }</LinkExternal>;
|
||||
}) }
|
||||
</Flex>
|
||||
) }
|
||||
@@ -193,6 +194,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
|
||||
</LinkInternal>
|
||||
</DetailsInfoItem>
|
||||
) }
|
||||
<DetailsSponsoredItem/>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -9,13 +9,13 @@ import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
|
||||
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
|
||||
import { apos } from 'lib/html-entities';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import AddressIntTxsSkeletonDesktop from 'ui/address/internals/AddressIntTxsSkeletonDesktop';
|
||||
import AddressIntTxsSkeletonMobile from 'ui/address/internals/AddressIntTxsSkeletonMobile';
|
||||
import AddressIntTxsTable from 'ui/address/internals/AddressIntTxsTable';
|
||||
import EmptySearchResult from 'ui/apps/EmptySearchResult';
|
||||
import ActionBar from 'ui/shared/ActionBar';
|
||||
import DataFetchAlert from 'ui/shared/DataFetchAlert';
|
||||
import Pagination from 'ui/shared/Pagination';
|
||||
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
|
||||
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
|
||||
|
||||
import AddressCsvExportLink from './AddressCsvExportLink';
|
||||
import AddressTxsFilter from './AddressTxsFilter';
|
||||
@@ -43,29 +43,33 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
|
||||
onFilterChange({ filter: newVal });
|
||||
}, [ onFilterChange ]);
|
||||
|
||||
if (isLoading) {
|
||||
const content = (() => {
|
||||
if (isError) {
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<>
|
||||
<Show below="lg" ssr={ false }>
|
||||
<SkeletonList/>
|
||||
</Show>
|
||||
<Hide below="lg" ssr={ false }>
|
||||
<SkeletonTable columns={ [ '15%', '15%', '10%', '20%', '20%', '20%' ] } isLong/>
|
||||
</Hide>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.items.length === 0 && !filterValue) {
|
||||
return <Text as="span">There are no internal transactions for this address.</Text>;
|
||||
}
|
||||
|
||||
if (data.items.length === 0) {
|
||||
return <EmptySearchResult text={ `Couldn${ apos }t find any transaction that matches your query.` }/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Show below="lg" ssr={ false }><AddressIntTxsSkeletonMobile/></Show>
|
||||
<Hide below="lg" ssr={ false }><AddressIntTxsSkeletonDesktop/></Hide>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
|
||||
if (data.items.length === 0 && !filterValue) {
|
||||
return <Text as="span">There are no internal transactions for this address.</Text>;
|
||||
}
|
||||
|
||||
let content;
|
||||
|
||||
if (data.items.length === 0) {
|
||||
content = <EmptySearchResult text={ `Couldn${ apos }t find any transaction that matches your query.` }/>;
|
||||
} else {
|
||||
content = (
|
||||
<>
|
||||
<Show below="lg" ssr={ false }>
|
||||
<AddressIntTxsList data={ data.items } currentAddress={ hash }/>
|
||||
@@ -75,11 +79,11 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
|
||||
</Hide>
|
||||
</>
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionBar mt={ -6 } justifyContent="left">
|
||||
<ActionBar mt={ -6 } justifyContent="left" showShadow={ isLoading }>
|
||||
<AddressTxsFilter
|
||||
defaultFilter={ filterValue }
|
||||
onFilterChange={ handleFilterChange }
|
||||
|
||||
@@ -25,7 +25,7 @@ const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>
|
||||
}
|
||||
|
||||
const bar = isPaginationVisible ? (
|
||||
<ActionBar mt={ -6 }>
|
||||
<ActionBar mt={ -6 } showShadow>
|
||||
<Pagination ml="auto" { ...pagination }/>
|
||||
</ActionBar>
|
||||
) : null;
|
||||
|
||||
@@ -169,7 +169,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
|
||||
return (
|
||||
<>
|
||||
<Hide below="lg" ssr={ false }>
|
||||
<SkeletonTable columns={ [ '44px', '185px', '160px', '25%', '25%', '25%', '25%' ] }/>
|
||||
<SkeletonTable columns={ [ '44px', '185px', '160px', '25%', '25%', '25%', '25%' ] } isLong/>
|
||||
</Hide>
|
||||
<Show below="lg" ssr={ false }>
|
||||
<SkeletonList/>
|
||||
@@ -253,7 +253,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
|
||||
<>
|
||||
{ isMobile && tokenFilterComponent }
|
||||
{ !isActionBarHidden && (
|
||||
<ActionBar mt={ -6 }>
|
||||
<ActionBar mt={ -6 } showShadow={ isLoading }>
|
||||
{ !isMobile && tokenFilterComponent }
|
||||
{ !tokenFilter && (
|
||||
<TokenTransferFilter
|
||||
|
||||
@@ -138,7 +138,7 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
|
||||
return (
|
||||
<>
|
||||
{ !isMobile && (
|
||||
<ActionBar mt={ -6 }>
|
||||
<ActionBar mt={ -6 } showShadow={ addressTxsQuery.isLoading }>
|
||||
{ filter }
|
||||
{ currentAddress && <AddressCsvExportLink address={ currentAddress } type="transactions" ml="auto"/> }
|
||||
{ addressTxsQuery.isPaginationVisible && <Pagination { ...addressTxsQuery.pagination } ml={ 8 }/> }
|
||||
@@ -152,6 +152,8 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
|
||||
showSocketInfo={ addressTxsQuery.pagination.page === 1 }
|
||||
socketInfoAlert={ socketAlert }
|
||||
socketInfoNum={ newItemsCount }
|
||||
top={ 80 }
|
||||
hasLongSkeleton
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
@@ -43,6 +43,10 @@ const AddressCoinBalanceHistory = ({ query }: Props) => {
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
|
||||
if (query.data.items.length === 0 && !query.isPaginationVisible) {
|
||||
return <span>There is no coin balance history for this address</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Hide below="lg" ssr={ false }>
|
||||
|
||||
@@ -23,7 +23,7 @@ test('verified with changed byte code +@mobile +@dark-mode', async({ mount, page
|
||||
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<ContractCode/>
|
||||
<ContractCode addressHash={ addressHash }/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
@@ -39,7 +39,7 @@ test('verified with multiple sources +@mobile', async({ mount, page }) => {
|
||||
|
||||
await mount(
|
||||
<TestApp>
|
||||
<ContractCode/>
|
||||
<ContractCode addressHash={ addressHash }/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
@@ -57,7 +57,7 @@ test('verified via sourcify', async({ mount, page }) => {
|
||||
|
||||
await mount(
|
||||
<TestApp>
|
||||
<ContractCode/>
|
||||
<ContractCode addressHash={ addressHash }/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
@@ -73,7 +73,7 @@ test('self destructed', async({ mount, page }) => {
|
||||
|
||||
await mount(
|
||||
<TestApp>
|
||||
<ContractCode/>
|
||||
<ContractCode addressHash={ addressHash }/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
@@ -90,7 +90,7 @@ test('with twin address alert +@mobile', async({ mount, page }) => {
|
||||
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<ContractCode/>
|
||||
<ContractCode addressHash={ addressHash }/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
@@ -106,7 +106,7 @@ test('with proxy address alert +@mobile', async({ mount, page }) => {
|
||||
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<ContractCode/>
|
||||
<ContractCode addressHash={ addressHash }/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
@@ -122,7 +122,7 @@ test('non verified', async({ mount, page }) => {
|
||||
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<ContractCode/>
|
||||
<ContractCode addressHash={ addressHash }/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Flex, Skeleton, Button, Grid, GridItem, Text, Alert, Link, chakra, Box } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { route } from 'nextjs-routes';
|
||||
import React from 'react';
|
||||
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import Address from 'ui/shared/address/Address';
|
||||
import AddressIcon from 'ui/shared/address/AddressIcon';
|
||||
import AddressLink from 'ui/shared/address/AddressLink';
|
||||
@@ -16,6 +14,10 @@ import RawDataSnippet from 'ui/shared/RawDataSnippet';
|
||||
|
||||
import ContractSourceCode from './ContractSourceCode';
|
||||
|
||||
type Props = {
|
||||
addressHash?: string;
|
||||
}
|
||||
|
||||
const InfoItem = chakra(({ label, value, className }: { label: string; value: string; className?: string }) => (
|
||||
<GridItem display="flex" columnGap={ 6 } wordBreak="break-all" className={ className }>
|
||||
<Text w="170px" flexShrink={ 0 } fontWeight={ 500 }>{ label }</Text>
|
||||
@@ -23,10 +25,7 @@ const InfoItem = chakra(({ label, value, className }: { label: string; value: st
|
||||
</GridItem>
|
||||
));
|
||||
|
||||
const ContractCode = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const addressHash = getQueryParamString(router.query.hash);
|
||||
const ContractCode = ({ addressHash }: Props) => {
|
||||
const { data, isLoading, isError } = useApiQuery('contract', {
|
||||
pathParams: { hash: addressHash },
|
||||
queryOptions: {
|
||||
@@ -62,7 +61,7 @@ const ContractCode = () => {
|
||||
ml="auto"
|
||||
mr={ 3 }
|
||||
as="a"
|
||||
href={ route({ pathname: '/address/[hash]/contract_verification', query: { hash: addressHash } }) }
|
||||
href={ route({ pathname: '/address/[hash]/contract_verification', query: { hash: addressHash || '' } }) }
|
||||
>
|
||||
Verify & publish
|
||||
</Button>
|
||||
@@ -115,7 +114,7 @@ const ContractCode = () => {
|
||||
{ data.is_verified_via_sourcify && (
|
||||
<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 } title="View contract in Sourcify repository" fontSize="md"/> }
|
||||
{ data.sourcify_repo_url && <LinkExternal href={ data.sourcify_repo_url } fontSize="md">View contract in Sourcify repository</LinkExternal> }
|
||||
</Alert>
|
||||
) }
|
||||
{ data.is_changed_bytecode && (
|
||||
@@ -131,7 +130,9 @@ const ContractCode = () => {
|
||||
<AddressLink type="address" hash={ data.verified_twin_address_hash } truncation="constant" ml={ 2 }/>
|
||||
</Address>
|
||||
<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>
|
||||
<LinkInternal href={ route({ pathname: '/address/[hash]/contract_verification', query: { hash: addressHash || '' } }) }>
|
||||
Verify & Publish
|
||||
</LinkInternal>
|
||||
<span> page</span>
|
||||
</Alert>
|
||||
) }
|
||||
|
||||
@@ -35,50 +35,55 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, renderC
|
||||
setId((id) => id + 1);
|
||||
}, []);
|
||||
|
||||
if (data.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion allowMultiple position="relative" onChange={ handleAccordionStateChange } index={ expandedSections }>
|
||||
{ data.map((item, index) => {
|
||||
return (
|
||||
<AccordionItem key={ index } as="section" _first={{ borderTopWidth: '0', '.chakra-accordion__button': { pr: '150px' } }}>
|
||||
<h2>
|
||||
<AccordionButton px={ 0 } py={ 3 } _hover={{ bgColor: 'inherit' }} wordBreak="break-all" textAlign="left">
|
||||
<Box as="span" fontFamily="heading" fontWeight={ 500 } fontSize="lg" mr={ 1 }>
|
||||
{ index + 1 }. { item.type === 'fallback' || item.type === 'receive' ? item.type : item.name }
|
||||
</Box>
|
||||
{ item.type === 'fallback' && (
|
||||
<Hint
|
||||
label={
|
||||
`The fallback function is executed on a call to the contract if none of the other functions match
|
||||
<>
|
||||
<Flex mb={ 3 }>
|
||||
<Box fontWeight={ 500 }>Contract information</Box>
|
||||
<Link onClick={ handleExpandAll } ml="auto">{ expandedSections.length === data.length ? 'Collapse' : 'Expand' } all</Link>
|
||||
<Link onClick={ handleReset } ml={ 3 }>Reset</Link>
|
||||
</Flex>
|
||||
<Accordion allowMultiple position="relative" onChange={ handleAccordionStateChange } index={ expandedSections }>
|
||||
{ data.map((item, index) => {
|
||||
return (
|
||||
<AccordionItem key={ index } as="section" _first={{ borderTopWidth: '0' }}>
|
||||
<h2>
|
||||
<AccordionButton px={ 0 } py={ 3 } _hover={{ bgColor: 'inherit' }} wordBreak="break-all" textAlign="left">
|
||||
<Box as="span" fontWeight={ 500 } mr={ 1 }>
|
||||
{ index + 1 }. { item.type === 'fallback' || item.type === 'receive' ? item.type : item.name }
|
||||
</Box>
|
||||
{ item.type === 'fallback' && (
|
||||
<Hint
|
||||
label={
|
||||
`The fallback function is executed on a call to the contract if none of the other functions match
|
||||
the given function signature, or if no data was supplied at all and there is no receive Ether function.
|
||||
The fallback function always receives data, but in order to also receive Ether it must be marked payable.`
|
||||
}/>
|
||||
) }
|
||||
{ item.type === 'receive' && (
|
||||
<Hint
|
||||
label={
|
||||
`The receive function is executed on a call to the contract with empty calldata.
|
||||
}/>
|
||||
) }
|
||||
{ item.type === 'receive' && (
|
||||
<Hint
|
||||
label={
|
||||
`The receive function is executed on a call to the contract with empty calldata.
|
||||
This is the function that is executed on plain Ether transfers (e.g. via .send() or .transfer()).
|
||||
If no such function exists, but a payable fallback function exists, the fallback function will be called on a plain Ether transfer.
|
||||
If neither a receive Ether nor a payable fallback function is present,
|
||||
the contract cannot receive Ether through regular transactions and throws an exception.`
|
||||
}/>
|
||||
) }
|
||||
<AccordionIcon/>
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel pb={ 4 } px={ 0 }>
|
||||
{ renderContent(item, index, id) }
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
);
|
||||
}) }
|
||||
{ data.length > 0 && (
|
||||
<Flex columnGap={ 3 } position="absolute" top={ 0 } right={ 0 } py={ 3 } lineHeight="27px">
|
||||
<Link onClick={ handleExpandAll }>{ expandedSections.length === data.length ? 'Collapse' : 'Expand' } all</Link>
|
||||
<Link onClick={ handleReset }>Reset</Link>
|
||||
</Flex>
|
||||
) }
|
||||
</Accordion>
|
||||
}/>
|
||||
) }
|
||||
<AccordionIcon/>
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel pb={ 4 } px={ 0 }>
|
||||
{ renderContent(item, index, id) }
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
);
|
||||
}) }
|
||||
</Accordion>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ test('base view +@mobile +@dark-mode', async({ mount, page }) => {
|
||||
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<ContractRead/>
|
||||
<ContractRead addressHash={ addressHash }/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
@@ -57,7 +57,7 @@ test('error result', async({ mount, page }) => {
|
||||
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<ContractRead/>
|
||||
<ContractRead addressHash={ addressHash }/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Alert, Flex } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { useAccount } from 'wagmi';
|
||||
|
||||
@@ -7,7 +6,6 @@ import type { SmartContractReadMethod, SmartContractQueryMethodRead } from 'type
|
||||
|
||||
import useApiFetch from 'lib/api/useApiFetch';
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion';
|
||||
import ContentLoader from 'ui/shared/ContentLoader';
|
||||
import DataFetchAlert from 'ui/shared/DataFetchAlert';
|
||||
@@ -20,17 +18,15 @@ import ContractMethodConstant from './ContractMethodConstant';
|
||||
import ContractReadResult from './ContractReadResult';
|
||||
|
||||
interface Props {
|
||||
addressHash?: string;
|
||||
isProxy?: boolean;
|
||||
isCustomAbi?: boolean;
|
||||
}
|
||||
|
||||
const ContractRead = ({ isProxy, isCustomAbi }: Props) => {
|
||||
const router = useRouter();
|
||||
const ContractRead = ({ addressHash, isProxy, isCustomAbi }: Props) => {
|
||||
const apiFetch = useApiFetch();
|
||||
const { address: userAddress } = useAccount();
|
||||
|
||||
const addressHash = getQueryParamString(router.query.hash);
|
||||
|
||||
const { data, isLoading, isError } = useApiQuery(isProxy ? 'contract_methods_read_proxy' : 'contract_methods_read', {
|
||||
pathParams: { hash: addressHash },
|
||||
queryParams: {
|
||||
|
||||
@@ -23,7 +23,7 @@ test('base view +@mobile', async({ mount, page }) => {
|
||||
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<ContractWrite/>
|
||||
<ContractWrite addressHash={ addressHash }/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import _capitalize from 'lodash/capitalize';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { useAccount, useSigner } from 'wagmi';
|
||||
|
||||
@@ -7,7 +6,6 @@ import type { SmartContractWriteMethod } from 'types/api/contract';
|
||||
|
||||
import config from 'configs/app/config';
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion';
|
||||
import ContentLoader from 'ui/shared/ContentLoader';
|
||||
import DataFetchAlert from 'ui/shared/DataFetchAlert';
|
||||
@@ -21,14 +19,12 @@ import ContractWriteResult from './ContractWriteResult';
|
||||
import { getNativeCoinValue, isExtendedError } from './utils';
|
||||
|
||||
interface Props {
|
||||
addressHash?: string;
|
||||
isProxy?: boolean;
|
||||
isCustomAbi?: boolean;
|
||||
}
|
||||
|
||||
const ContractWrite = ({ isProxy, isCustomAbi }: Props) => {
|
||||
const router = useRouter();
|
||||
|
||||
const addressHash = getQueryParamString(router.query.hash);
|
||||
const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
|
||||
const { data: signer } = useSigner();
|
||||
const { isConnected } = useAccount();
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 94 KiB |
@@ -1,6 +1,5 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import type { Contract } from 'ethers';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { useContract, useProvider, useSigner } from 'wagmi';
|
||||
|
||||
@@ -9,6 +8,7 @@ import type { Address } from 'types/api/address';
|
||||
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
|
||||
|
||||
type ProviderProps = {
|
||||
addressHash?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -24,13 +24,11 @@ const ContractContext = React.createContext<TContractContext>({
|
||||
custom: null,
|
||||
});
|
||||
|
||||
export function ContractContextProvider({ children }: ProviderProps) {
|
||||
const router = useRouter();
|
||||
export function ContractContextProvider({ addressHash, children }: ProviderProps) {
|
||||
const provider = useProvider();
|
||||
const { data: signer } = useSigner();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const addressHash = router.query.hash?.toString();
|
||||
const { data: contractInfo } = useApiQuery('contract', {
|
||||
pathParams: { hash: addressHash },
|
||||
queryOptions: {
|
||||
|
||||
@@ -20,7 +20,7 @@ const AddressNameInfo = ({ data }: Props) => {
|
||||
hint="Token name and symbol"
|
||||
>
|
||||
<LinkInternal href={ route({ pathname: '/token/[hash]', query: { hash: data.token.address } }) }>
|
||||
{ data.token.name }{ symbol }
|
||||
{ data.token.name || 'Unnamed token' }{ symbol }
|
||||
</LinkInternal>
|
||||
</DetailsInfoItem>
|
||||
);
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
|
||||
|
||||
const TxInternalsSkeletonDesktop = () => {
|
||||
return (
|
||||
<SkeletonTable columns={ [ '15%', '15%', '10%', '20%', '20%', '20%' ] }/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TxInternalsSkeletonDesktop;
|
||||
@@ -1,48 +0,0 @@
|
||||
import { Skeleton, SkeletonCircle, Flex, Box } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
const TxInternalsSkeletonMobile = () => {
|
||||
return (
|
||||
<Box>
|
||||
{ Array.from(Array(2)).map((item, index) => (
|
||||
<Flex
|
||||
key={ index }
|
||||
rowGap={ 3 }
|
||||
flexDirection="column"
|
||||
paddingY={ 6 }
|
||||
borderTopWidth="1px"
|
||||
borderColor="divider"
|
||||
_last={{
|
||||
borderBottomWidth: '1px',
|
||||
}}
|
||||
>
|
||||
<Flex h={ 6 }>
|
||||
<Skeleton w="100px" mr={ 2 }/>
|
||||
<Skeleton w="90px"/>
|
||||
</Flex>
|
||||
<Flex h={ 6 }>
|
||||
<Skeleton w="100%" mr={ 2 }/>
|
||||
<Skeleton w="60px"/>
|
||||
</Flex>
|
||||
<Flex h={ 6 }>
|
||||
<Skeleton w="70px" mr={ 2 }/>
|
||||
<Skeleton w="30px"/>
|
||||
</Flex>
|
||||
<Flex h={ 6 }>
|
||||
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
|
||||
<Skeleton flexGrow={ 1 } mr={ 3 }/>
|
||||
<Skeleton w={ 6 } mr={ 3 }/>
|
||||
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
|
||||
<Skeleton flexGrow={ 1 } mr={ 3 }/>
|
||||
</Flex>
|
||||
<Flex h={ 6 }>
|
||||
<Skeleton w="70px" mr={ 2 }/>
|
||||
<Skeleton w="30px"/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)) }
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default TxInternalsSkeletonMobile;
|
||||
@@ -0,0 +1,75 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
const SwaggerUIReact = dynamic(() => import('swagger-ui-react'), {
|
||||
loading: () => <Spinner/>,
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
import { Box, Spinner, useColorModeValue } from '@chakra-ui/react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import React from 'react';
|
||||
|
||||
import appConfig from 'configs/app/config';
|
||||
|
||||
import 'swagger-ui-react/swagger-ui.css';
|
||||
|
||||
const DEFAULT_SERVER = 'blockscout.com/poa/core';
|
||||
|
||||
const NeverShowInfoPlugin = () => {
|
||||
return {
|
||||
components: {
|
||||
SchemesContainer: () => null,
|
||||
ServersContainer: () => null,
|
||||
InfoContainer: () => null,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const SwaggerUI = () => {
|
||||
const swaggerStyle = {
|
||||
'.scheme-container, .opblock-tag': {
|
||||
display: 'none',
|
||||
},
|
||||
'.swagger-ui': {
|
||||
color: useColorModeValue('blackAlpha.800', 'whiteAlpha.800'),
|
||||
},
|
||||
'.swagger-ui .opblock-summary-control:focus': {
|
||||
outline: 'none',
|
||||
},
|
||||
// eslint-disable-next-line max-len
|
||||
'.swagger-ui .opblock .opblock-summary-path, .swagger-ui .opblock .opblock-summary-description, .swagger-ui div, .swagger-ui p, .swagger-ui h5, .swagger-ui .response-col_links, .swagger-ui h4, .swagger-ui table thead tr th, .swagger-ui table thead tr td, .swagger-ui .parameter__name, .swagger-ui .parameter__type, .swagger-ui .response-col_status, .swagger-ui .tab li, .swagger-ui .opblock .opblock-section-header h4': {
|
||||
color: 'unset',
|
||||
},
|
||||
'.swagger-ui input': {
|
||||
color: 'blackAlpha.800',
|
||||
},
|
||||
'.swagger-ui .opblock .opblock-section-header': {
|
||||
background: useColorModeValue('whiteAlpha.800', 'blackAlpha.800'),
|
||||
},
|
||||
'.swagger-ui .response-col_description__inner p, .swagger-ui .parameters-col_description p': {
|
||||
margin: 0,
|
||||
},
|
||||
'.swagger-ui .wrapper': {
|
||||
padding: 0,
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const reqInterceptor = React.useCallback((req: any) => {
|
||||
if (!req.loadSpec) {
|
||||
req.url = req.url.replace(DEFAULT_SERVER, appConfig.api.host);
|
||||
}
|
||||
return req;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box sx={ swaggerStyle }>
|
||||
<SwaggerUIReact
|
||||
url={ appConfig.apiDoc.specUrl }
|
||||
plugins={ [ NeverShowInfoPlugin ] }
|
||||
requestInterceptor={ reqInterceptor }
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SwaggerUI;
|
||||
@@ -118,7 +118,7 @@ const BlockDetails = () => {
|
||||
hint="The number of transactions in the block"
|
||||
>
|
||||
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height, tab: 'txs' } }) }>
|
||||
{ data.tx_count } transactions
|
||||
{ data.tx_count } transaction{ data.tx_count === 1 ? '' : 's' }
|
||||
</LinkInternal>
|
||||
</DetailsInfoItem>
|
||||
<DetailsInfoItem
|
||||
@@ -274,18 +274,18 @@ const BlockDetails = () => {
|
||||
<DetailsInfoItem
|
||||
title="Difficulty"
|
||||
hint={ `Block difficulty for ${ validatorTitle }, used to calibrate block generation time` }
|
||||
whiteSpace="normal"
|
||||
wordBreak="break-all"
|
||||
>
|
||||
{ BigNumber(data.difficulty).toFormat() }
|
||||
<Box whiteSpace="nowrap" overflow="hidden">
|
||||
<HashStringShortenDynamic hash={ BigNumber(data.difficulty).toFormat() }/>
|
||||
</Box>
|
||||
</DetailsInfoItem>
|
||||
<DetailsInfoItem
|
||||
title="Total difficulty"
|
||||
hint="Total difficulty of the chain until this block"
|
||||
whiteSpace="normal"
|
||||
wordBreak="break-all"
|
||||
>
|
||||
{ BigNumber(data.total_difficulty).toFormat() }
|
||||
<Box whiteSpace="nowrap" overflow="hidden">
|
||||
<HashStringShortenDynamic hash={ BigNumber(data.total_difficulty).toFormat() }/>
|
||||
</Box>
|
||||
</DetailsInfoItem>
|
||||
|
||||
{ sectionGap }
|
||||
|
||||
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 94 KiB |
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
|
||||
import type { SmartContractVerificationConfig } from 'types/api/contract';
|
||||
|
||||
import * as socketServer from 'playwright/fixtures/socketServer';
|
||||
import TestApp from 'playwright/TestApp';
|
||||
|
||||
import ContractVerificationForm from './ContractVerificationForm';
|
||||
@@ -34,6 +35,7 @@ const formConfig: SmartContractVerificationConfig = {
|
||||
'sourcify',
|
||||
'multi-part',
|
||||
'vyper-code',
|
||||
'vyper-multi-part',
|
||||
],
|
||||
vyper_compiler_versions: [
|
||||
'v0.3.7+commit.6020b8bb',
|
||||
@@ -60,8 +62,9 @@ test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) =
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
await page.getByText(/flattened source code/i).click();
|
||||
await page.getByText(/optimization enabled/i).click();
|
||||
await component.getByLabel(/verification method/i).focus();
|
||||
await component.getByLabel(/verification method/i).type('solidity');
|
||||
await page.getByRole('button', { name: /flattened source code/i }).click();
|
||||
await page.getByText(/add contract libraries/i).click();
|
||||
await page.locator('button[aria-label="add"]').click();
|
||||
|
||||
@@ -76,28 +79,61 @@ test('standard input json method', async({ mount, page }) => {
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
await page.getByText(/via standard/i).click();
|
||||
await component.getByLabel(/verification method/i).focus();
|
||||
await component.getByLabel(/verification method/i).type('solidity');
|
||||
await page.getByRole('button', { name: /standard json input/i }).click();
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('sourcify method +@dark-mode +@mobile', async({ mount, page }) => {
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<ContractVerificationForm config={ formConfig } hash={ hash }/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
test.describe('sourcify', () => {
|
||||
const testWithSocket = test.extend<socketServer.SocketServerFixture>({
|
||||
createSocket: socketServer.createSocket,
|
||||
});
|
||||
testWithSocket.describe.configure({ mode: 'serial', timeout: 20_000 });
|
||||
|
||||
await page.getByText(/via sourcify/i).click();
|
||||
await page.getByText(/upload files/i).click();
|
||||
await page.locator('input[name="sources"]').setInputFiles([
|
||||
'./playwright/mocks/file_mock_1.json',
|
||||
'./playwright/mocks/file_mock_2.json',
|
||||
'./playwright/mocks/file_mock_with_very_long_name.json',
|
||||
]);
|
||||
testWithSocket('with multiple contracts +@mobile', async({ mount, page, createSocket }) => {
|
||||
const component = await mount(
|
||||
<TestApp withSocket>
|
||||
<ContractVerificationForm config={ formConfig } hash={ hash }/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
await component.getByLabel(/verification method/i).focus();
|
||||
await component.getByLabel(/verification method/i).type('solidity');
|
||||
await page.getByRole('button', { name: /sourcify/i }).click();
|
||||
|
||||
await page.getByText(/drop files/i).click();
|
||||
await page.locator('input[name="sources"]').setInputFiles([
|
||||
'./playwright/mocks/file_mock_1.json',
|
||||
'./playwright/mocks/file_mock_2.json',
|
||||
'./playwright/mocks/file_mock_with_very_long_name.json',
|
||||
]);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
|
||||
const socket = await createSocket();
|
||||
const channel = await socketServer.joinChannel(socket, `addresses:${ hash.toLowerCase() }`);
|
||||
|
||||
await page.getByRole('button', { name: /verify/i }).click();
|
||||
|
||||
socketServer.sendMessage(socket, channel, 'verification_result', {
|
||||
status: 'error',
|
||||
errors: {
|
||||
// eslint-disable-next-line max-len
|
||||
files: [ 'Detected 5 contracts (ERC20, IERC20, IERC20Metadata, Context, MockERC20), but can only verify 1 at a time. Please choose a main contract and click Verify again.' ],
|
||||
},
|
||||
});
|
||||
|
||||
await component.getByLabel(/contract name/i).focus();
|
||||
await component.getByLabel(/contract name/i).type('e');
|
||||
const contractNameOption = page.getByRole('button', { name: /MockERC20/i });
|
||||
|
||||
await expect(contractNameOption).toBeVisible();
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
});
|
||||
|
||||
test('multi-part files method', async({ mount, page }) => {
|
||||
@@ -108,7 +144,9 @@ test('multi-part files method', async({ mount, page }) => {
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
await page.getByText(/via multi-part files/i).click();
|
||||
await component.getByLabel(/verification method/i).focus();
|
||||
await component.getByLabel(/verification method/i).type('solidity');
|
||||
await page.getByRole('button', { name: /multi-part files/i }).click();
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -121,7 +159,24 @@ test('vyper contract method', async({ mount, page }) => {
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
await page.getByText(/vyper contract/i).click();
|
||||
await component.getByLabel(/verification method/i).focus();
|
||||
await component.getByLabel(/verification method/i).type('vyper');
|
||||
await page.getByRole('button', { name: /contract/i }).click();
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('vyper multi-part method', async({ mount, page }) => {
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<ContractVerificationForm config={ formConfig } hash={ hash }/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
await component.getByLabel(/verification method/i).focus();
|
||||
await component.getByLabel(/verification method/i).type('vyper');
|
||||
await page.getByRole('button', { name: /multi-part files/i }).click();
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, chakra } from '@chakra-ui/react';
|
||||
import { Button, chakra, useUpdateEffect } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import type { SubmitHandler } from 'react-hook-form';
|
||||
@@ -20,7 +20,7 @@ import ContractVerificationSourcify from './methods/ContractVerificationSourcify
|
||||
import ContractVerificationStandardInput from './methods/ContractVerificationStandardInput';
|
||||
import ContractVerificationVyperContract from './methods/ContractVerificationVyperContract';
|
||||
import ContractVerificationVyperMultiPartFile from './methods/ContractVerificationVyperMultiPartFile';
|
||||
import { prepareRequestBody, formatSocketErrors } from './utils';
|
||||
import { prepareRequestBody, formatSocketErrors, DEFAULT_VALUES } from './utils';
|
||||
|
||||
const METHOD_COMPONENTS = {
|
||||
'flattened-code': <ContractVerificationFlattenSourceCode/>,
|
||||
@@ -40,11 +40,9 @@ interface Props {
|
||||
const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Props) => {
|
||||
const formApi = useForm<FormFields>({
|
||||
mode: 'onBlur',
|
||||
defaultValues: {
|
||||
method: methodFromQuery,
|
||||
},
|
||||
defaultValues: methodFromQuery ? DEFAULT_VALUES[methodFromQuery] : undefined,
|
||||
});
|
||||
const { control, handleSubmit, watch, formState, setError } = formApi;
|
||||
const { control, handleSubmit, watch, formState, setError, reset } = formApi;
|
||||
const submitPromiseResolver = React.useRef<(value: unknown) => void>();
|
||||
|
||||
const apiFetch = useApiFetch();
|
||||
@@ -56,7 +54,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
|
||||
|
||||
try {
|
||||
await apiFetch('contract_verification_via', {
|
||||
pathParams: { method: data.method, hash },
|
||||
pathParams: { method: data.method.value, hash: hash.toLowerCase() },
|
||||
fetchParams: {
|
||||
method: 'POST',
|
||||
body,
|
||||
@@ -109,7 +107,10 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
|
||||
variant: 'subtle',
|
||||
isClosable: true,
|
||||
});
|
||||
}, [ formState.isSubmitting, toast ]);
|
||||
// callback should not change when form is submitted
|
||||
// otherwise it will resubscribe to channel, but we don't want that since in that case we might miss verification result message
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ toast ]);
|
||||
|
||||
const channel = useSocketChannel({
|
||||
topic: `addresses:${ hash.toLowerCase() }`,
|
||||
@@ -124,7 +125,15 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
|
||||
});
|
||||
|
||||
const method = watch('method');
|
||||
const content = METHOD_COMPONENTS[method] || null;
|
||||
const content = METHOD_COMPONENTS[method?.value] || null;
|
||||
const methodValue = method?.value;
|
||||
|
||||
useUpdateEffect(() => {
|
||||
if (methodValue) {
|
||||
reset(DEFAULT_VALUES[methodValue]);
|
||||
}
|
||||
// !!! should run only when method is changed
|
||||
}, [ methodValue ]);
|
||||
|
||||
return (
|
||||
<FormProvider { ...formApi }>
|
||||
@@ -134,8 +143,8 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
|
||||
>
|
||||
<ContractVerificationFieldMethod
|
||||
control={ control }
|
||||
isDisabled={ Boolean(method) }
|
||||
methods={ config.verification_options }
|
||||
isDisabled={ formState.isSubmitting }
|
||||
/>
|
||||
{ content }
|
||||
{ Boolean(method) && (
|
||||
|
||||
@@ -15,8 +15,8 @@ const ContractVerificationMethod = ({ title, children }: Props) => {
|
||||
|
||||
return (
|
||||
<section ref={ ref }>
|
||||
<Text variant="secondary" mt={ 12 } mb={ 5 } fontSize="sm">{ title }</Text>
|
||||
<Grid columnGap="30px" rowGap={{ base: 2, lg: 4 }} templateColumns={{ base: '1fr', lg: 'minmax(auto, 680px) minmax(0, 340px)' }}>
|
||||
<Text fontWeight={ 500 } fontSize="lg" fontFamily="heading" mt={ 12 } mb={ 5 }>{ title }</Text>
|
||||
<Grid columnGap="30px" rowGap={{ base: 2, lg: 5 }} templateColumns={{ base: '1fr', lg: 'minmax(0, 680px) minmax(0, 340px)' }}>
|
||||
{ children }
|
||||
</Grid>
|
||||
</section>
|
||||
|
||||
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 51 KiB |
@@ -34,7 +34,6 @@ const ContractVerificationFieldAutodetectArgs = () => {
|
||||
name="autodetect_constructor_args"
|
||||
control={ control }
|
||||
render={ renderControl }
|
||||
defaultValue={ true }
|
||||
/>
|
||||
</ContractVerificationFormRow>
|
||||
{ !isOn && <ContractVerificationFieldConstructorArgs/> }
|
||||
|
||||
@@ -41,7 +41,6 @@ const ContractVerificationFieldCode = ({ isVyper }: Props) => {
|
||||
control={ control }
|
||||
render={ renderControl }
|
||||
rules={{ required: true }}
|
||||
defaultValue=""
|
||||
/>
|
||||
{ isVyper ? null : (
|
||||
<>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Code } from '@chakra-ui/react';
|
||||
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';
|
||||
@@ -20,26 +20,30 @@ interface Props {
|
||||
}
|
||||
|
||||
const ContractVerificationFieldCompiler = ({ isVyper }: Props) => {
|
||||
const { formState, control } = useFormContext<FormFields>();
|
||||
const [ isNightly, setIsNightly ] = React.useState(false);
|
||||
const { formState, control, getValues, resetField } = useFormContext<FormFields>();
|
||||
const isMobile = useIsMobile();
|
||||
const queryClient = useQueryClient();
|
||||
const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config'));
|
||||
|
||||
const handleCheckboxChange = React.useCallback(() => {
|
||||
if (isNightly) {
|
||||
const field = getValues('compiler');
|
||||
field?.value.includes('nightly') && resetField('compiler', { defaultValue: null });
|
||||
}
|
||||
setIsNightly(prev => !prev);
|
||||
}, [ getValues, isNightly, resetField ]);
|
||||
|
||||
const options = React.useMemo(() => (
|
||||
(isVyper ? config?.vyper_compiler_versions : config?.solidity_compiler_versions)?.map((option) => ({ label: option, value: option })) || []
|
||||
), [ config?.solidity_compiler_versions, config?.vyper_compiler_versions, isVyper ]);
|
||||
|
||||
const loadOptions = React.useCallback(async(inputValue: string) => {
|
||||
return options
|
||||
.filter(({ label }) => {
|
||||
if (!inputValue) {
|
||||
return !label.toLowerCase().includes('nightly');
|
||||
}
|
||||
|
||||
return label.toLowerCase().includes(inputValue.toLowerCase());
|
||||
})
|
||||
.filter(({ label }) => !inputValue || label.toLowerCase().includes(inputValue.toLowerCase()))
|
||||
.filter(({ label }) => isNightly ? true : !label.includes('nightly'))
|
||||
.slice(0, OPTIONS_LIMIT);
|
||||
}, [ options ]);
|
||||
}, [ isNightly, options ]);
|
||||
|
||||
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'compiler'>}) => {
|
||||
const error = 'compiler' in formState.errors ? formState.errors.compiler : undefined;
|
||||
@@ -61,20 +65,32 @@ const ContractVerificationFieldCompiler = ({ isVyper }: Props) => {
|
||||
|
||||
return (
|
||||
<ContractVerificationFormRow>
|
||||
<Controller
|
||||
name="compiler"
|
||||
control={ control }
|
||||
render={ renderControl }
|
||||
rules={{ required: true }}
|
||||
/>
|
||||
<>
|
||||
{ !isVyper && (
|
||||
<Checkbox
|
||||
size="lg"
|
||||
mb={ 2 }
|
||||
onChange={ handleCheckboxChange }
|
||||
isDisabled={ formState.isSubmitting }
|
||||
>
|
||||
Include nightly builds
|
||||
</Checkbox>
|
||||
) }
|
||||
<Controller
|
||||
name="compiler"
|
||||
control={ control }
|
||||
render={ renderControl }
|
||||
rules={{ required: true }}
|
||||
/>
|
||||
</>
|
||||
{ isVyper ? null : (
|
||||
<>
|
||||
<span>The compiler version is specified in </span>
|
||||
<chakra.div mt={{ base: 0, lg: 8 }}>
|
||||
<span >The compiler version is specified in </span>
|
||||
<Code color="text_secondary">pragma solidity X.X.X</Code>
|
||||
<span>. Use the compiler version rather than the nightly build. If using the Solidity compiler, run </span>
|
||||
<Code color="text_secondary">solc —version</Code>
|
||||
<span> to check.</span>
|
||||
</>
|
||||
</chakra.div>
|
||||
) }
|
||||
</ContractVerificationFormRow>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
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 type { FormFields } from '../types';
|
||||
import type { Option } from 'ui/shared/FancySelect/types';
|
||||
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import FancySelect from 'ui/shared/FancySelect/FancySelect';
|
||||
|
||||
import ContractVerificationFormRow from '../ContractVerificationFormRow';
|
||||
|
||||
const SOURCIFY_ERROR_REGEXP = /\(([^()]*)\)/;
|
||||
|
||||
const ContractVerificationFieldContractIndex = () => {
|
||||
const [ options, setOptions ] = React.useState<Array<Option>>([]);
|
||||
const { formState, control, watch } = useFormContext<FormFields>();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const sources = watch('sources');
|
||||
const sourcesError = 'sources' in formState.errors ? formState.errors.sources?.message : undefined;
|
||||
|
||||
useUpdateEffect(() => {
|
||||
if (!sourcesError) {
|
||||
return;
|
||||
}
|
||||
|
||||
const matchResult = sourcesError.match(SOURCIFY_ERROR_REGEXP);
|
||||
const parsedMethods = matchResult?.[1].split(',');
|
||||
if (!Array.isArray(parsedMethods) || parsedMethods.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newOptions = parsedMethods.map((option, index) => ({ label: option, value: String(index + 1) }));
|
||||
setOptions(newOptions);
|
||||
}, [ sourcesError ]);
|
||||
|
||||
useUpdateEffect(() => {
|
||||
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
|
||||
name="contract_index"
|
||||
control={ control }
|
||||
render={ renderControl }
|
||||
rules={{ required: true }}
|
||||
/>
|
||||
</ContractVerificationFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ContractVerificationFieldContractIndex);
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Checkbox } from '@chakra-ui/react';
|
||||
import { Checkbox, useUpdateEffect } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
|
||||
@@ -8,13 +8,21 @@ import ContractVerificationFormRow from '../ContractVerificationFormRow';
|
||||
import ContractVerificationFieldLibraryItem from './ContractVerificationFieldLibraryItem';
|
||||
|
||||
const ContractVerificationFieldLibraries = () => {
|
||||
const { formState, control } = useFormContext<FormFields>();
|
||||
const { formState, control, getValues } = useFormContext<FormFields>();
|
||||
const { fields, append, remove, insert } = useFieldArray({
|
||||
name: 'libraries',
|
||||
control,
|
||||
});
|
||||
const [ isEnabled, setIsEnabled ] = React.useState(fields.length > 0);
|
||||
|
||||
const value = getValues('libraries');
|
||||
|
||||
useUpdateEffect(() => {
|
||||
if (!value || value.length === 0) {
|
||||
setIsEnabled(false);
|
||||
}
|
||||
}, [ value ]);
|
||||
|
||||
const handleCheckboxChange = React.useCallback(() => {
|
||||
if (!isEnabled) {
|
||||
append({ name: '', address: '' });
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
RadioGroup,
|
||||
Radio,
|
||||
Stack,
|
||||
Text,
|
||||
Link,
|
||||
Icon,
|
||||
chakra,
|
||||
@@ -14,16 +10,23 @@ import {
|
||||
PopoverBody,
|
||||
useColorModeValue,
|
||||
DarkMode,
|
||||
useBoolean,
|
||||
ListItem,
|
||||
OrderedList,
|
||||
Grid,
|
||||
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/api/contract';
|
||||
import type { SmartContractVerificationConfig, SmartContractVerificationMethod } from 'types/api/contract';
|
||||
|
||||
import infoIcon from 'icons/info.svg';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import FancySelect from 'ui/shared/FancySelect/FancySelect';
|
||||
|
||||
import { METHOD_LABELS } from '../utils';
|
||||
|
||||
interface Props {
|
||||
control: Control<FormFields>;
|
||||
@@ -32,91 +35,93 @@ interface Props {
|
||||
}
|
||||
|
||||
const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props) => {
|
||||
const [ isPopoverOpen, setIsPopoverOpen ] = useBoolean();
|
||||
const tooltipBg = useColorModeValue('gray.700', 'gray.900');
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const renderItem = React.useCallback((method: SmartContractVerificationMethod) => {
|
||||
const options = React.useMemo(() => methods.map((method) => ({
|
||||
value: method,
|
||||
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 }
|
||||
/>
|
||||
);
|
||||
}, [ isDisabled, isMobile, options ]);
|
||||
|
||||
const renderPopoverListItem = React.useCallback((method: SmartContractVerificationMethod) => {
|
||||
switch (method) {
|
||||
case 'flattened-code':
|
||||
return 'Via flattened source code';
|
||||
return <ListItem>Verification through flattened source code.</ListItem>;
|
||||
case 'multi-part':
|
||||
return <ListItem>Verification of multi-part Solidity files.</ListItem>;
|
||||
case 'sourcify':
|
||||
return <ListItem>Verification through <Link href="https://sourcify.dev/" target="_blank">Sourcify</Link>.</ListItem>;
|
||||
case 'standard-input':
|
||||
return (
|
||||
<>
|
||||
<span>Via standard </span>
|
||||
<ListItem>
|
||||
<span>Verification using </span>
|
||||
<Link
|
||||
href={ isDisabled ? undefined : 'https://docs.soliditylang.org/en/latest/using-the-compiler.html#input-description' }
|
||||
href="https://docs.soliditylang.org/en/latest/using-the-compiler.html#input-description"
|
||||
target="_blank"
|
||||
cursor={ isDisabled ? 'not-allowed' : 'pointer' }
|
||||
>
|
||||
Input JSON
|
||||
Standard input JSON
|
||||
</Link>
|
||||
</>
|
||||
<span> file.</span>
|
||||
</ListItem>
|
||||
);
|
||||
case 'sourcify':
|
||||
return (
|
||||
<>
|
||||
<span>Via sourcify: sources and metadata JSON file</span>
|
||||
<Popover trigger="hover" isLazy isOpen={ isDisabled ? false : isPopoverOpen } onOpen={ setIsPopoverOpen.on } onClose={ setIsPopoverOpen.off }>
|
||||
case 'vyper-code':
|
||||
return <ListItem>Verification of Vyper contract.</ListItem>;
|
||||
case 'vyper-multi-part':
|
||||
return <ListItem>Verification of multi-part Vyper files.</ListItem>;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Grid columnGap="30px" rowGap={{ base: 2, lg: 4 }} templateColumns={{ base: '1fr', lg: 'minmax(auto, 680px) minmax(0, 340px)' }}>
|
||||
<div>
|
||||
<Box mb={ 5 }>
|
||||
<chakra.span fontWeight={ 500 } fontSize="lg" fontFamily="heading">
|
||||
Currently, Blockscout supports { methods.length } contract verification methods
|
||||
</chakra.span>
|
||||
<Popover trigger="hover" isLazy placement={ isMobile ? 'bottom-end' : 'right-start' } offset={ [ -8, 8 ] }>
|
||||
<PopoverTrigger>
|
||||
<chakra.span cursor={ isDisabled ? 'not-allowed' : 'pointer' } display="inline-block" verticalAlign="middle" h="24px" ml={ 1 }>
|
||||
<chakra.span display="inline-block" ml={ 1 } cursor="pointer" verticalAlign="middle" h="22px">
|
||||
<Icon as={ infoIcon } boxSize={ 5 } color="link" _hover={{ color: 'link_hovered' }}/>
|
||||
</chakra.span>
|
||||
</PopoverTrigger>
|
||||
<Portal>
|
||||
<PopoverContent bgColor={ tooltipBg }>
|
||||
<PopoverContent bgColor={ tooltipBg } w={{ base: '300px', lg: '380px' }}>
|
||||
<PopoverArrow bgColor={ tooltipBg }/>
|
||||
<PopoverBody color="white">
|
||||
<DarkMode>
|
||||
<div>
|
||||
<span>Verification through </span>
|
||||
<Link href="https://sourcify.dev/" target="_blank">Sourcify</Link>
|
||||
</div>
|
||||
<div>
|
||||
<span>a) if smart-contract already verified on Sourcify, it will automatically fetch the data from the </span>
|
||||
<Link href="https://repo.sourcify.dev/" target="_blank">repo</Link>
|
||||
</div>
|
||||
<div>
|
||||
b) otherwise you will be asked to upload source files and JSON metadata file(s).
|
||||
</div>
|
||||
<span>Currently, Blockscout supports { methods.length } methods:</span>
|
||||
<OrderedList>
|
||||
{ methods.map(renderPopoverListItem) }
|
||||
</OrderedList>
|
||||
</DarkMode>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Portal>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
case 'multi-part':
|
||||
return 'Via multi-part files';
|
||||
case 'vyper-code':
|
||||
return 'Vyper contract';
|
||||
case 'vyper-multi-part':
|
||||
return 'Via multi-part Vyper files';
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, [ isDisabled, isPopoverOpen, setIsPopoverOpen.off, setIsPopoverOpen.on, tooltipBg ]);
|
||||
|
||||
const renderRadioGroup = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'method'>}) => {
|
||||
return (
|
||||
<RadioGroup defaultValue="add" colorScheme="blue" isDisabled={ isDisabled } isFocusable={ !isDisabled } { ...field } >
|
||||
<Stack spacing={ 4 }>
|
||||
{ methods.map((method) => {
|
||||
return <Radio key={ method } value={ method } size="lg">{ renderItem(method) }</Radio>;
|
||||
}) }
|
||||
</Stack>
|
||||
</RadioGroup>
|
||||
);
|
||||
}, [ isDisabled, methods, renderItem ]);
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Text variant="secondary" fontSize="sm" mb={ 5 }>Smart-contract verification method</Text>
|
||||
<Controller
|
||||
name="method"
|
||||
control={ control }
|
||||
render={ renderRadioGroup }
|
||||
/>
|
||||
</Box>
|
||||
<Controller
|
||||
name="method"
|
||||
control={ control }
|
||||
render={ renderControl }
|
||||
rules={{ required: true }}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -41,7 +41,6 @@ const ContractVerificationFieldName = ({ hint }: Props) => {
|
||||
control={ control }
|
||||
render={ renderControl }
|
||||
rules={{ required: true }}
|
||||
defaultValue=""
|
||||
/>
|
||||
{ hint ? <span>{ hint }</span> : (
|
||||
<>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FormControl, Input } from '@chakra-ui/react';
|
||||
import { Flex, Input } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import type { ControllerRenderProps } from 'react-hook-form';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
@@ -6,7 +6,6 @@ import { Controller, useFormContext } from 'react-hook-form';
|
||||
import type { FormFields } from '../types';
|
||||
|
||||
import CheckboxInput from 'ui/shared/CheckboxInput';
|
||||
import InputPlaceholder from 'ui/shared/InputPlaceholder';
|
||||
|
||||
import ContractVerificationFormRow from '../ContractVerificationFormRow';
|
||||
|
||||
@@ -14,56 +13,59 @@ 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'>}) => (
|
||||
<CheckboxInput<FormFields, 'is_optimization_enabled'>
|
||||
text="Optimization enabled"
|
||||
field={ field }
|
||||
onChange={ handleCheckboxChange }
|
||||
isDisabled={ formState.isSubmitting }
|
||||
/>
|
||||
<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 (
|
||||
<FormControl variant="floating" id={ field.name } size={{ base: 'md', lg: 'lg' }} isRequired>
|
||||
<Input
|
||||
{ ...field }
|
||||
required
|
||||
isDisabled={ formState.isSubmitting }
|
||||
autoComplete="off"
|
||||
type="number"
|
||||
/>
|
||||
<InputPlaceholder text="Optimization runs"/>
|
||||
</FormControl>
|
||||
<Input
|
||||
{ ...field }
|
||||
required
|
||||
isDisabled={ formState.isSubmitting }
|
||||
autoComplete="off"
|
||||
type="number"
|
||||
placeholder="Optimization runs"
|
||||
size="xs"
|
||||
minW="100px"
|
||||
maxW="200px"
|
||||
flexShrink={ 1 }
|
||||
isInvalid={ Boolean(error) }
|
||||
/>
|
||||
);
|
||||
}, [ formState.isSubmitting ]);
|
||||
}, [ error, formState.isSubmitting ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContractVerificationFormRow>
|
||||
<ContractVerificationFormRow>
|
||||
<Flex columnGap={ 5 } h={{ base: 'auto', lg: '32px' }}>
|
||||
<Controller
|
||||
name="is_optimization_enabled"
|
||||
control={ control }
|
||||
render={ renderCheckboxControl }
|
||||
defaultValue={ true }
|
||||
/>
|
||||
</ContractVerificationFormRow>
|
||||
{ isEnabled && (
|
||||
<ContractVerificationFormRow>
|
||||
{ isEnabled && (
|
||||
<Controller
|
||||
name="optimization_runs"
|
||||
control={ control }
|
||||
render={ renderInputControl }
|
||||
rules={{ required: true }}
|
||||
defaultValue="200"
|
||||
/>
|
||||
</ContractVerificationFormRow>
|
||||
) }
|
||||
</>
|
||||
) }
|
||||
</Flex>
|
||||
</ContractVerificationFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Text, Button, Box, chakra, Flex } from '@chakra-ui/react';
|
||||
import { Text, Button, Box, Flex } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import type { ControllerRenderProps, FieldPathValue, ValidateResult } from 'react-hook-form';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
@@ -6,7 +6,7 @@ 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 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';
|
||||
@@ -19,11 +19,10 @@ interface Props {
|
||||
fileTypes: Array<FileTypes>;
|
||||
multiple?: boolean;
|
||||
title: string;
|
||||
className?: string;
|
||||
hint: string;
|
||||
}
|
||||
|
||||
const ContractVerificationFieldSources = ({ fileTypes, multiple, title, className, hint }: Props) => {
|
||||
const ContractVerificationFieldSources = ({ fileTypes, multiple, title, hint }: Props) => {
|
||||
const { setValue, getValues, control, formState, clearErrors } = useFormContext<FormFields>();
|
||||
|
||||
const error = 'sources' in formState.errors ? formState.errors.sources : undefined;
|
||||
@@ -42,11 +41,28 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam
|
||||
|
||||
}, [ getValues, clearErrors, setValue ]);
|
||||
|
||||
const renderUploadButton = React.useCallback(() => {
|
||||
return (
|
||||
<div>
|
||||
<Text fontWeight={ 500 } color="text_secondary" mb={ 3 }>{ title }</Text>
|
||||
<Button size="sm" variant="outline">
|
||||
Drop file{ multiple ? 's' : '' } or click here
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}, [ multiple, title ]);
|
||||
|
||||
const renderFiles = React.useCallback((files: Array<File>) => {
|
||||
const errorList = fileError?.message?.split(';');
|
||||
|
||||
return (
|
||||
<Box display="grid" gridTemplateColumns={{ base: 'minmax(0, 1fr)', lg: 'minmax(0, 1fr) minmax(0, 1fr)' }} columnGap={ 3 } rowGap={ 3 }>
|
||||
<Box
|
||||
display="grid"
|
||||
gridTemplateColumns={{ base: 'minmax(0, 1fr)', lg: 'minmax(0, 1fr) minmax(0, 1fr)' }}
|
||||
columnGap={ 3 }
|
||||
rowGap={ 3 }
|
||||
w="100%"
|
||||
>
|
||||
{ files.map((file, index) => (
|
||||
<Box key={ file.name + file.lastModified + index }>
|
||||
<FileSnippet
|
||||
@@ -55,42 +71,36 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam
|
||||
onRemove={ handleFileRemove }
|
||||
index={ index }
|
||||
isDisabled={ formState.isSubmitting }
|
||||
error={ errorList?.[index] }
|
||||
/>
|
||||
{ errorList?.[index] && <FieldError message={ errorList?.[index] } mt={ 1 } px={ 3 }/> }
|
||||
</Box>
|
||||
)) }
|
||||
</Box>
|
||||
);
|
||||
}, [ formState.isSubmitting, handleFileRemove, fileError ]);
|
||||
|
||||
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'sources'>}) => (
|
||||
<>
|
||||
<FileInput<FormFields, 'sources'> accept={ fileTypes.join(',') } multiple={ multiple } field={ field }>
|
||||
{ () => (
|
||||
<Flex
|
||||
flexDir="column"
|
||||
alignItems="flex-start"
|
||||
rowGap={ 2 }
|
||||
w="100%"
|
||||
display={ field.value && field.value.length > 0 && !multiple ? 'none' : 'block' }
|
||||
mb={ field.value && field.value.length > 0 ? 2 : 0 }
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
// mb={ 2 }
|
||||
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'sources'>}) => {
|
||||
const hasValue = field.value && field.value.length > 0;
|
||||
return (
|
||||
<>
|
||||
<FileInput<FormFields, 'sources'> accept={ fileTypes.join(',') } multiple={ multiple } field={ field }>
|
||||
{ ({ onChange }) => (
|
||||
<Flex
|
||||
flexDir="column"
|
||||
alignItems="flex-start"
|
||||
rowGap={ 2 }
|
||||
w="100%"
|
||||
>
|
||||
Upload file{ multiple ? 's' : '' }
|
||||
</Button>
|
||||
{ /* design is not ready */ }
|
||||
{ /* <DragAndDropArea onDrop={ onChange }/> */ }
|
||||
</Flex>
|
||||
) }
|
||||
</FileInput>
|
||||
{ field.value && field.value.length > 0 && renderFiles(field.value) }
|
||||
{ commonError?.message && <FieldError message={ commonError.type === 'required' ? 'Field is required' : commonError.message }/> }
|
||||
</>
|
||||
), [ fileTypes, commonError, multiple, renderFiles ]);
|
||||
<DragAndDropArea onDrop={ onChange } p={{ base: 3, lg: 6 }} isDisabled={ formState.isSubmitting }>
|
||||
{ hasValue ? renderFiles(field.value) : renderUploadButton() }
|
||||
</DragAndDropArea>
|
||||
</Flex>
|
||||
) }
|
||||
</FileInput>
|
||||
{ commonError?.message && <FieldError message={ commonError.type === 'required' ? 'Field is required' : commonError.message }/> }
|
||||
</>
|
||||
);
|
||||
}, [ fileTypes, multiple, commonError, formState.isSubmitting, renderFiles, renderUploadButton ]);
|
||||
|
||||
const validateFileType = React.useCallback(async(value: FieldPathValue<FormFields, 'sources'>): Promise<ValidateResult> => {
|
||||
if (Array.isArray(value)) {
|
||||
@@ -114,30 +124,34 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const validateQuantity = React.useCallback(async(value: FieldPathValue<FormFields, 'sources'>): Promise<ValidateResult> => {
|
||||
if (!multiple && Array.isArray(value) && value.length > 1) {
|
||||
return 'You can upload only one file';
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [ multiple ]);
|
||||
|
||||
const rules = React.useMemo(() => ({
|
||||
required: true,
|
||||
validate: {
|
||||
file_type: validateFileType,
|
||||
file_size: validateFileSize,
|
||||
quantity: validateQuantity,
|
||||
},
|
||||
}), [ validateFileSize, validateFileType ]);
|
||||
}), [ validateFileSize, validateFileType, validateQuantity ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContractVerificationFormRow >
|
||||
<Text fontWeight={ 500 } className={ className } mt={ 4 }>{ title }</Text>
|
||||
</ContractVerificationFormRow>
|
||||
<ContractVerificationFormRow>
|
||||
<Controller
|
||||
name="sources"
|
||||
control={ control }
|
||||
render={ renderControl }
|
||||
rules={ rules }
|
||||
/>
|
||||
{ hint ? <span>{ hint }</span> : null }
|
||||
</ContractVerificationFormRow>
|
||||
</>
|
||||
<ContractVerificationFormRow>
|
||||
<Controller
|
||||
name="sources"
|
||||
control={ control }
|
||||
render={ renderControl }
|
||||
rules={ rules }
|
||||
/>
|
||||
{ hint ? <span>{ hint }</span> : null }
|
||||
</ContractVerificationFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(chakra(ContractVerificationFieldSources));
|
||||
export default React.memo(ContractVerificationFieldSources);
|
||||
|
||||
@@ -12,9 +12,9 @@ import ContractVerificationFieldOptimization from '../fields/ContractVerificatio
|
||||
|
||||
const ContractVerificationFlattenSourceCode = () => {
|
||||
return (
|
||||
<ContractVerificationMethod title="New Solidity/Yul Smart Contract Verification">
|
||||
<ContractVerificationFieldIsYul/>
|
||||
<ContractVerificationMethod title="Contract verification via Solidity (fattened source code)">
|
||||
<ContractVerificationFieldName/>
|
||||
<ContractVerificationFieldIsYul/>
|
||||
<ContractVerificationFieldCompiler/>
|
||||
<ContractVerificationFieldEvmVersion/>
|
||||
<ContractVerificationFieldOptimization/>
|
||||
|
||||
@@ -11,7 +11,7 @@ const FILE_TYPES = [ '.sol' as const, '.yul' as const ];
|
||||
|
||||
const ContractVerificationMultiPartFile = () => {
|
||||
return (
|
||||
<ContractVerificationMethod title="New Solidity/Yul Smart Contract Verification">
|
||||
<ContractVerificationMethod title="Contract verification via Solidity (multi-part files)">
|
||||
<ContractVerificationFieldCompiler/>
|
||||
<ContractVerificationFieldEvmVersion/>
|
||||
<ContractVerificationFieldOptimization/>
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
import ContractVerificationMethod from '../ContractVerificationMethod';
|
||||
import ContractVerificationFieldContractIndex from '../fields/ContractVerificationFieldContractIndex';
|
||||
import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources';
|
||||
|
||||
const FILE_TYPES = [ '.json' as const, '.sol' as const ];
|
||||
|
||||
const ContractVerificationSourcify = () => {
|
||||
return (
|
||||
<ContractVerificationMethod title="New Smart Contract Verification">
|
||||
<ContractVerificationMethod title="Contract verification via Solidity (Sourcify)">
|
||||
<ContractVerificationFieldSources
|
||||
fileTypes={ FILE_TYPES }
|
||||
multiple
|
||||
title="Sources and Metadata JSON" mt={ 0 }
|
||||
title="Sources and Metadata JSON"
|
||||
hint="Upload all Solidity contract source files and JSON metadata file(s) created during contract compilation."
|
||||
/>
|
||||
<ContractVerificationFieldContractIndex/>
|
||||
</ContractVerificationMethod>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ const FILE_TYPES = [ '.json' as const ];
|
||||
|
||||
const ContractVerificationStandardInput = () => {
|
||||
return (
|
||||
<ContractVerificationMethod title="New Smart Contract Verification">
|
||||
<ContractVerificationMethod title="Contract verification via Solidity (standard JSON input) ">
|
||||
<ContractVerificationFieldName/>
|
||||
<ContractVerificationFieldCompiler/>
|
||||
<ContractVerificationFieldSources
|
||||
|
||||
@@ -8,7 +8,7 @@ import ContractVerificationFieldName from '../fields/ContractVerificationFieldNa
|
||||
|
||||
const ContractVerificationVyperContract = () => {
|
||||
return (
|
||||
<ContractVerificationMethod title="New Vyper Smart Contract Verification">
|
||||
<ContractVerificationMethod title="Contract verification via Vyper (contract)">
|
||||
<ContractVerificationFieldName hint="Must match the name specified in the code."/>
|
||||
<ContractVerificationFieldCompiler isVyper/>
|
||||
<ContractVerificationFieldCode isVyper/>
|
||||
|
||||
@@ -9,7 +9,7 @@ const FILE_TYPES = [ '.vy' as const ];
|
||||
|
||||
const ContractVerificationVyperMultiPartFile = () => {
|
||||
return (
|
||||
<ContractVerificationMethod title="New Vyper Smart Contract Verification">
|
||||
<ContractVerificationMethod title="Contract verification via Vyper (multi-part files)">
|
||||
<ContractVerificationFieldCompiler isVyper/>
|
||||
<ContractVerificationFieldEvmVersion isVyper/>
|
||||
<ContractVerificationFieldSources
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import type { SmartContractVerificationMethod } from 'types/api/contract';
|
||||
import type { Option } from 'ui/shared/FancySelect/types';
|
||||
|
||||
export interface ContractLibrary {
|
||||
name: string;
|
||||
address: string;
|
||||
}
|
||||
|
||||
interface MethodOption {
|
||||
label: string;
|
||||
value: SmartContractVerificationMethod;
|
||||
}
|
||||
|
||||
export interface FormFieldsFlattenSourceCode {
|
||||
method: 'flattened-code';
|
||||
method: MethodOption;
|
||||
is_yul: boolean;
|
||||
name: string;
|
||||
compiler: Option;
|
||||
@@ -19,7 +26,7 @@ export interface FormFieldsFlattenSourceCode {
|
||||
}
|
||||
|
||||
export interface FormFieldsStandardInput {
|
||||
method: 'standard-input';
|
||||
method: MethodOption;
|
||||
name: string;
|
||||
compiler: Option;
|
||||
sources: Array<File>;
|
||||
@@ -28,12 +35,13 @@ export interface FormFieldsStandardInput {
|
||||
}
|
||||
|
||||
export interface FormFieldsSourcify {
|
||||
method: 'sourcify';
|
||||
method: MethodOption;
|
||||
sources: Array<File>;
|
||||
contract_index?: Option;
|
||||
}
|
||||
|
||||
export interface FormFieldsMultiPartFile {
|
||||
method: 'multi-part';
|
||||
method: MethodOption;
|
||||
compiler: Option;
|
||||
evm_version: Option;
|
||||
is_optimization_enabled: boolean;
|
||||
@@ -43,7 +51,7 @@ export interface FormFieldsMultiPartFile {
|
||||
}
|
||||
|
||||
export interface FormFieldsVyperContract {
|
||||
method: 'vyper-code';
|
||||
method: MethodOption;
|
||||
name: string;
|
||||
compiler: Option;
|
||||
code: string;
|
||||
@@ -51,7 +59,7 @@ export interface FormFieldsVyperContract {
|
||||
}
|
||||
|
||||
export interface FormFieldsVyperMultiPartFile {
|
||||
method: 'vyper-multi-part';
|
||||
method: MethodOption;
|
||||
compiler: Option;
|
||||
evm_version: Option;
|
||||
sources: Array<File>;
|
||||
|
||||