Commit c441f01a authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into csv-download

parents 6d1b99ec fcdf0f2d
NEXT_PUBLIC_SENTRY_DSN=xxx NEXT_PUBLIC_SENTRY_DSN=xxx
SENTRY_CSP_REPORT_URI=xxx SENTRY_CSP_REPORT_URI=xxx
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
\ No newline at end of file
...@@ -48,9 +48,14 @@ NEXT_PUBLIC_API_PROTOCOL=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_PROTOCOL__ ...@@ -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_API_PORT=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_PORT__
NEXT_PUBLIC_STATS_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_STATS_API_HOST__ 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_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 # external services config
NEXT_PUBLIC_SENTRY_DSN=__PLACEHOLDER_FOR_NEXT_PUBLIC_SENTRY_DSN__ NEXT_PUBLIC_SENTRY_DSN=__PLACEHOLDER_FOR_NEXT_PUBLIC_SENTRY_DSN__
NEXT_PUBLIC_AUTH0_CLIENT_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_AUTH0_CLIENT_ID__ 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_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__ 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 @@ ...@@ -251,7 +251,7 @@
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "shared", "panel": "shared",
"close": true, "close": false,
"revealProblems": "onProblem", "revealProblems": "onProblem",
"focus": true, "focus": true,
}, },
......
...@@ -46,7 +46,7 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -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_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_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_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_NAME | `string` | Network currency name | `Ether` |
| NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | Network currency symbol | `ETH` | | NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | Network currency symbol | `ETH` |
| NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS | `string` | Network currency decimals | `18` | | 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 ...@@ -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_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_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_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 ### App configuration
| Variable | Type | Description | Default value | Variable | Type | Description | Default value
...@@ -129,6 +130,12 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -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_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>` | | 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 ### Marketplace app configuration properties
| Property | Type | Description | Example value | Property | Type | Description | Example value
......
...@@ -104,6 +104,10 @@ const config = Object.freeze({ ...@@ -104,6 +104,10 @@ const config = Object.freeze({
socket: apiHost ? `wss://${ apiHost }` : 'wss://blockscout.com', socket: apiHost ? `wss://${ apiHost }` : 'wss://blockscout.com',
basePath: stripTrailingSlash(getEnvValue(process.env.NEXT_PUBLIC_API_BASE_PATH) || ''), 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: { statsApi: {
endpoint: getEnvValue(process.env.NEXT_PUBLIC_STATS_API_HOST), endpoint: getEnvValue(process.env.NEXT_PUBLIC_STATS_API_HOST),
basePath: '', basePath: '',
...@@ -122,6 +126,9 @@ const config = Object.freeze({ ...@@ -122,6 +126,9 @@ const config = Object.freeze({
walletConnect: { walletConnect: {
projectId: getEnvValue(process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID), projectId: getEnvValue(process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID),
}, },
apiDoc: {
specUrl: getEnvValue(process.env.NEXT_PUBLIC_API_SPEC_URL),
},
reCaptcha: { reCaptcha: {
siteKey: getEnvValue(process.env.NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY) || '', siteKey: getEnvValue(process.env.NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY) || '',
}, },
......
...@@ -9,6 +9,8 @@ NEXT_PUBLIC_APP_ENV=development ...@@ -9,6 +9,8 @@ NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom 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 # api config
NEXT_PUBLIC_API_HOST=blockscout.com NEXT_PUBLIC_API_HOST=blockscout.com
......
This diff is collapsed.
...@@ -17,6 +17,7 @@ NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cup'] ...@@ -17,6 +17,7 @@ NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cup']
NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT=radial-gradient(at 12% 37%, hsla(324,73%,67%,1) 0px, transparent 50%), radial-gradient(at 62% 14%, hsla(256,87%,73%,1) 0px, transparent 50%), radial-gradient(at 84% 80%, hsla(128,75%,73%,1) 0px, transparent 50%), radial-gradient(at 57% 46%, hsla(285,63%,72%,1) 0px, transparent 50%), radial-gradient(at 37% 30%, hsla(174,70%,61%,1) 0px, transparent 50%), radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%), radial-gradient(at 67% 57%, hsla(14,95%,76%,1) 0px, transparent 50%) NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT=radial-gradient(at 12% 37%, hsla(324,73%,67%,1) 0px, transparent 50%), radial-gradient(at 62% 14%, hsla(256,87%,73%,1) 0px, transparent 50%), radial-gradient(at 84% 80%, hsla(128,75%,73%,1) 0px, transparent 50%), radial-gradient(at 57% 46%, hsla(285,63%,72%,1) 0px, transparent 50%), radial-gradient(at 37% 30%, hsla(174,70%,61%,1) 0px, transparent 50%), radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%), radial-gradient(at 67% 57%, hsla(14,95%,76%,1) 0px, transparent 50%)
#NEXT_PUBLIC_NETWORK_LOGO=https://placekitten.com/300/60 #NEXT_PUBLIC_NETWORK_LOGO=https://placekitten.com/300/60
#NEXT_PUBLIC_NETWORK_SMALL_LOGO=https://placekitten.com/300/300 #NEXT_PUBLIC_NETWORK_SMALL_LOGO=https://placekitten.com/300/300
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
# network config # network config
NEXT_PUBLIC_NETWORK_NAME=POA NEXT_PUBLIC_NETWORK_NAME=POA
...@@ -32,6 +33,7 @@ NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true ...@@ -32,6 +33,7 @@ NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_MARKETPLACE_APP_LIST=[{'author': 'Blockscout','id':'token-approval-tracker','title':'Token Approval Tracker','logo':'https://approval-tracker.vercel.app/icon-192.png','categories':['security','tools'],'shortDescription':'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.','site':'https://docs.blockscout.com/for-users/blockscout-apps/token-approval-tracker','description':'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.','url':'https://approval-tracker.vercel.app/'},{'author': 'Revoke','id':'revoke.cash','title':'Revoke.cash','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FVBMGyUFnd6CScjfK7CYQ%252Frevoke_sing.png%3Falt%3Dmedia%26token%3D9ab94986-7ab1-41c8-bf7e-d9ce11d23182','categories':['security','tools'],'shortDescription': 'Revoke.cash comes in as a preventative tool to manage your token allowances and practice proper wallet hygiene. By regularly revoking active allowances you reduce the chances of becoming the victim of allowance exploits.','site': 'https://revoke.cash/about','description': 'Revoke.cash comes in as a preventative tool to manage your token allowances and practice proper wallet hygiene. By regularly revoking active allowances you reduce the chances of becoming the victim of allowance exploits.','url':'https://revoke.cash/'},{'author':'Aave','id': 'aave','title': 'Aave','logo': 'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FrZkUTIUCG7Zx8BW6Em34%252FAave.png%3Falt%3Dmedia%26token%3D249797a4-4c1e-4372-9cd2-3e48e05e5f30','categories':['tools'],'shortDescription':'Aave is a decentralised non-custodial liquidity market protocol where users can participate as suppliers or borrowers. Suppliers provide liquidity to the market to earn a passive income, while borrowers are able to borrow in an overcollateralised (perpetually) or undercollateralised (one-block liquidity) fashion.','site': 'https://docs.aave.com/faq/','description': 'Aave is a decentralised non-custodial liquidity market protocol where users can participate as suppliers or borrowers. Suppliers provide liquidity to the market to earn a passive income, while borrowers are able to borrow in an overcollateralised (perpetually) or undercollateralised (one-block liquidity) fashion.','url': 'https://staging.aave.com/'},{'author':'LooksRare','id':'looksrare','external':true,'title':'LooksRare','logo': 'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FeAI4gy3qPMt68mZOZHAx%252FLooksRare.png%3Falt%3Dmedia%26token%3D44c01439-ae09-40aa-b904-3a9ce5b2e002','categories':['tools'],'shortDescription': 'LooksRare is the web3 NFT Marketplace where traders and collectors have earned over $1.3 Billion in rewards.','site':'https://docs.looksrare.org/about/welcome-to-looksrare','description':'LooksRare is the web3 NFT Marketplace where traders and collectors have earned over $1.3 Billion in rewards.','url': 'https://goerli.looksrare.org/'},{'author':'zkSync Bridge','id':'zksync-bridge','external':true,'title':'zkSync Bridge','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FrtQsaAz9BjGBc35tVAnq%252FzkSync.png%3Falt%3Dmedia%26token%3D5c18171c-8ccf-4a88-8f44-680cbf238115','categories':['security','tools'],'shortDescription':'zkSync 2.0 Goerli Bridge','site':'https://v2-docs.zksync.io/dev/','description':'zkSync 2.0 Goerli Bridge','url':'https://portal.zksync.io/bridge'},{'author':'dYdX','id':'dydx','external':true,'title':'dYdX','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FCrOglR72wpi0UhEscwe4%252Fdxdy.png%3Falt%3Dmedia%26token%3D8811909e-93e3-487c-9614-dffce37223e9','categories': ['security','tools'],'shortDescription':'dYdX is a leading decentralized exchange that currently supports perpetual trading. dYdX runs on smart contracts on the Ethereum blockchain, and allows users to trade with no intermediaries.','site':'https://help.dydx.exchange/en/articles/3047379-introduction-and-overview','description':'dYdX is a leading decentralized exchange that currently supports perpetual trading. dYdX runs on smart contracts on the Ethereum blockchain, and allows users to trade with no intermediaries.','url':'https://trade.stage.dydx.exchange/portfolio/overview'},{'author':'MetalSwap','id':'metalswap','title':'MetalSwap','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252F8xqldTvxb6avrwVVc3rS%252FMetalSwap.png%3Falt%3Dmedia%26token%3D92d2db99-853a-487d-8d8c-8cdeaeaaf014','categories':['security','tools'],'shortDescription':'MetalSwap is a decentralised platform that enables hedging swaps in financial markets with the aim of providing a hedge for commodity traders and an investment opportunity for those who contribute to the shared liquidity of the project.','site':'https://docs.metalswap.finance/','description':'MetalSwap is a decentralised platform that enables hedging swaps in financial markets with the aim of providing a hedge for commodity traders and an investment opportunity for those who contribute to the shared liquidity of the project.','url':'https://demo.metalswap.finance/'},{'author':'FaucetDao','id':'faucetdao','title':'FaucetDao','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252Ffnnt3ZNZhzRwqMM5YYjD%252FPlaceholder.png%3Falt%3Dmedia%26token%3D507571bb-d76f-4d96-a35e-2b278608f7ca','categories':['tools'],'shortDescription':'FaucetDao is a decentralised community fund providing liquidity and support to early-stage well vetted blockchain projects.','site':'https://linktr.ee/faucet_dao','description':'FaucetDao is a decentralised community fund providing liquidity and support to early-stage well vetted blockchain projects.','url':'https://www.faucetdao.shop/swap?chain=goerli'},{'author':'Uniswap','id':'uniswap','title':'Uniswap','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FJc0QAyeaBmFIL97tSmGv%252FUniswap.png%3Falt%3Dmedia%26token%3D5d25d796-c273-4e22-92fa-ff85206bec76','categories':['tools'],'shortDescription':'Uniswap is a cryptocurrency exchange which uses a decentralized network protocol.','site':'https://docs.uniswap.org/','description':'Uniswap is a cryptocurrency exchange which uses a decentralized network protocol.','url':'https://app.uniswap.org/swap'}] NEXT_PUBLIC_MARKETPLACE_APP_LIST=[{'author': 'Blockscout','id':'token-approval-tracker','title':'Token Approval Tracker','logo':'https://approval-tracker.vercel.app/icon-192.png','categories':['security','tools'],'shortDescription':'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.','site':'https://docs.blockscout.com/for-users/blockscout-apps/token-approval-tracker','description':'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.','url':'https://approval-tracker.vercel.app/'},{'author': 'Revoke','id':'revoke.cash','title':'Revoke.cash','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FVBMGyUFnd6CScjfK7CYQ%252Frevoke_sing.png%3Falt%3Dmedia%26token%3D9ab94986-7ab1-41c8-bf7e-d9ce11d23182','categories':['security','tools'],'shortDescription': 'Revoke.cash comes in as a preventative tool to manage your token allowances and practice proper wallet hygiene. By regularly revoking active allowances you reduce the chances of becoming the victim of allowance exploits.','site': 'https://revoke.cash/about','description': 'Revoke.cash comes in as a preventative tool to manage your token allowances and practice proper wallet hygiene. By regularly revoking active allowances you reduce the chances of becoming the victim of allowance exploits.','url':'https://revoke.cash/'},{'author':'Aave','id': 'aave','title': 'Aave','logo': 'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FrZkUTIUCG7Zx8BW6Em34%252FAave.png%3Falt%3Dmedia%26token%3D249797a4-4c1e-4372-9cd2-3e48e05e5f30','categories':['tools'],'shortDescription':'Aave is a decentralised non-custodial liquidity market protocol where users can participate as suppliers or borrowers. Suppliers provide liquidity to the market to earn a passive income, while borrowers are able to borrow in an overcollateralised (perpetually) or undercollateralised (one-block liquidity) fashion.','site': 'https://docs.aave.com/faq/','description': 'Aave is a decentralised non-custodial liquidity market protocol where users can participate as suppliers or borrowers. Suppliers provide liquidity to the market to earn a passive income, while borrowers are able to borrow in an overcollateralised (perpetually) or undercollateralised (one-block liquidity) fashion.','url': 'https://staging.aave.com/'},{'author':'LooksRare','id':'looksrare','external':true,'title':'LooksRare','logo': 'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FeAI4gy3qPMt68mZOZHAx%252FLooksRare.png%3Falt%3Dmedia%26token%3D44c01439-ae09-40aa-b904-3a9ce5b2e002','categories':['tools'],'shortDescription': 'LooksRare is the web3 NFT Marketplace where traders and collectors have earned over $1.3 Billion in rewards.','site':'https://docs.looksrare.org/about/welcome-to-looksrare','description':'LooksRare is the web3 NFT Marketplace where traders and collectors have earned over $1.3 Billion in rewards.','url': 'https://goerli.looksrare.org/'},{'author':'zkSync Bridge','id':'zksync-bridge','external':true,'title':'zkSync Bridge','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FrtQsaAz9BjGBc35tVAnq%252FzkSync.png%3Falt%3Dmedia%26token%3D5c18171c-8ccf-4a88-8f44-680cbf238115','categories':['security','tools'],'shortDescription':'zkSync 2.0 Goerli Bridge','site':'https://v2-docs.zksync.io/dev/','description':'zkSync 2.0 Goerli Bridge','url':'https://portal.zksync.io/bridge'},{'author':'dYdX','id':'dydx','external':true,'title':'dYdX','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FCrOglR72wpi0UhEscwe4%252Fdxdy.png%3Falt%3Dmedia%26token%3D8811909e-93e3-487c-9614-dffce37223e9','categories': ['security','tools'],'shortDescription':'dYdX is a leading decentralized exchange that currently supports perpetual trading. dYdX runs on smart contracts on the Ethereum blockchain, and allows users to trade with no intermediaries.','site':'https://help.dydx.exchange/en/articles/3047379-introduction-and-overview','description':'dYdX is a leading decentralized exchange that currently supports perpetual trading. dYdX runs on smart contracts on the Ethereum blockchain, and allows users to trade with no intermediaries.','url':'https://trade.stage.dydx.exchange/portfolio/overview'},{'author':'MetalSwap','id':'metalswap','title':'MetalSwap','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252F8xqldTvxb6avrwVVc3rS%252FMetalSwap.png%3Falt%3Dmedia%26token%3D92d2db99-853a-487d-8d8c-8cdeaeaaf014','categories':['security','tools'],'shortDescription':'MetalSwap is a decentralised platform that enables hedging swaps in financial markets with the aim of providing a hedge for commodity traders and an investment opportunity for those who contribute to the shared liquidity of the project.','site':'https://docs.metalswap.finance/','description':'MetalSwap is a decentralised platform that enables hedging swaps in financial markets with the aim of providing a hedge for commodity traders and an investment opportunity for those who contribute to the shared liquidity of the project.','url':'https://demo.metalswap.finance/'},{'author':'FaucetDao','id':'faucetdao','title':'FaucetDao','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252Ffnnt3ZNZhzRwqMM5YYjD%252FPlaceholder.png%3Falt%3Dmedia%26token%3D507571bb-d76f-4d96-a35e-2b278608f7ca','categories':['tools'],'shortDescription':'FaucetDao is a decentralised community fund providing liquidity and support to early-stage well vetted blockchain projects.','site':'https://linktr.ee/faucet_dao','description':'FaucetDao is a decentralised community fund providing liquidity and support to early-stage well vetted blockchain projects.','url':'https://www.faucetdao.shop/swap?chain=goerli'},{'author':'Uniswap','id':'uniswap','title':'Uniswap','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FJc0QAyeaBmFIL97tSmGv%252FUniswap.png%3Falt%3Dmedia%26token%3D5d25d796-c273-4e22-92fa-ff85206bec76','categories':['tools'],'shortDescription':'Uniswap is a cryptocurrency exchange which uses a decentralized network protocol.','site':'https://docs.uniswap.org/','description':'Uniswap is a cryptocurrency exchange which uses a decentralized network protocol.','url':'https://app.uniswap.org/swap'}]
NEXT_PUBLIC_NETWORK_RPC_URL=https://core.poa.network
# api config # api config
NEXT_PUBLIC_API_BASE_PATH=/poa/core NEXT_PUBLIC_API_BASE_PATH=/poa/core
......
...@@ -40,6 +40,10 @@ blockscout: ...@@ -40,6 +40,10 @@ blockscout:
_default: ENC[AES256_GCM,data:mTY6sjNKZ+VEvH47eyyoUHt//beWvuxyreu+1WrmMvkdkwR6jgXnSXh+1pTuiG8e3it96Egxraz0hjZxlkY9kSmE91/dfoqKTns=,iv:op8OuOXXeBmmUnVkCL14j4HrfjHHoN3n2XZqLdg03Mw=,tag:bbFQYi0Aa1sUdfoUjuQ/+w==,type:str] _default: ENC[AES256_GCM,data:mTY6sjNKZ+VEvH47eyyoUHt//beWvuxyreu+1WrmMvkdkwR6jgXnSXh+1pTuiG8e3it96Egxraz0hjZxlkY9kSmE91/dfoqKTns=,iv:op8OuOXXeBmmUnVkCL14j4HrfjHHoN3n2XZqLdg03Mw=,tag:bbFQYi0Aa1sUdfoUjuQ/+w==,type:str]
ACCOUNT_AUTH0_LOGOUT_URL: ACCOUNT_AUTH0_LOGOUT_URL:
_default: ENC[AES256_GCM,data:IZFfi6pn+hy7g0wnEtP9TYHH1fNiC2gqgRHVdgm4C9smPerEvS0pq9dBwVY=,iv:BxbSInFQ6GE2loTv+IzdYr25PlyzdWZI1wdT6r+uvBg=,tag:psujCU372k59OGmlRdH9Fg==,type:str] _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: scVerifier:
environment: environment:
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__ACCESS_KEY: SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__ACCESS_KEY:
...@@ -134,8 +138,8 @@ sops: ...@@ -134,8 +138,8 @@ sops:
azure_kv: [] azure_kv: []
hc_vault: [] hc_vault: []
age: [] age: []
lastmodified: "2023-02-14T08:03:14Z" lastmodified: "2023-02-19T10:34:54Z"
mac: ENC[AES256_GCM,data:B2AkQb4I83dP3UUitRCcrUfzm3nWmcknIUoMWHyYaG9jasnccbr8zZatYdpbvKFcELVTtjhYk6ly5Sx7+6sk2PZm6o7dN3yHG5lSWmnZqNXkwo42GIk/F6vzDdLutZsu8HH8pWHd9y5R272CIPOOh4+Ur0OtwiGgj3Bp1od76qM=,iv:j7aIPflH0FsYhE/iylvBh5nDmVdghhxAFvaeXlR560k=,tag:/oe6OeitIHaZ4TgM7w/0pg==,type:str] mac: ENC[AES256_GCM,data:c7Nlguw+82tpgNAclx6XgXQip+gHh37Wrb9pq0MQ+bgp3LmfWpNDYYtxvdkIxMPNYsLxQOvw9Kc26qYWY1qDggmkHFvIeImCScjxBmrjSAf1K61lk0NIQ7AnEVh7ahipAopUO7y/0ogcjyjJCGs/QPRg9yelsONhCIjRkeUi7mQ=,iv:Y5tWWJJCqdNujHczKnouk4TiHBwMBezKhKCmvWeL9Kc=,tag:hIjUobEooUAQuKwHFraryw==,type:str]
pgp: pgp:
- created_at: "2022-09-14T13:42:28Z" - created_at: "2022-09-14T13:42:28Z"
enc: | enc: |
......
...@@ -491,6 +491,7 @@ frontend: ...@@ -491,6 +491,7 @@ frontend:
- "/token" - "/token"
- "/accounts" - "/accounts"
- "/visualize" - "/visualize"
- "/api-docs"
- "/csv-export" - "/csv-export"
resources: resources:
...@@ -572,3 +573,5 @@ frontend: ...@@ -572,3 +573,5 @@ frontend:
_default: https://rpc.ankr.com/eth_goerli _default: https://rpc.ankr.com/eth_goerli
NEXT_PUBLIC_HOMEPAGE_CHARTS: NEXT_PUBLIC_HOMEPAGE_CHARTS:
_default: "['daily_txs','coin_price','market_cup']" _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: ...@@ -327,6 +327,7 @@ frontend:
- "/tokens" - "/tokens"
- "/accounts" - "/accounts"
- "/visualize" - "/visualize"
- "/api-docs"
- "/csv-export" - "/csv-export"
resources: resources:
...@@ -409,3 +410,5 @@ frontend: ...@@ -409,3 +410,5 @@ frontend:
_default: https://rpc.ankr.com/eth_goerli _default: https://rpc.ankr.com/eth_goerli
NEXT_PUBLIC_NETWORK_EXPLORERS: 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'}}]" _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
<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>
<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>
<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>
<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>
...@@ -16,22 +16,21 @@ const MAIN_DOMAINS = [ `*.${ appConfig.host }`, appConfig.host ]; ...@@ -16,22 +16,21 @@ const MAIN_DOMAINS = [ `*.${ appConfig.host }`, appConfig.host ];
// eslint-disable-next-line no-restricted-properties // eslint-disable-next-line no-restricted-properties
const REPORT_URI = process.env.SENTRY_CSP_REPORT_URI; const REPORT_URI = process.env.SENTRY_CSP_REPORT_URI;
function getNetworksExternalAssets() { function getNetworksExternalAssetsHosts() {
const icons = featuredNetworks const icons = featuredNetworks
.filter(({ icon }) => typeof icon === 'string') .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; return logo ? icons.concat(logo) : icons;
} }
function getMarketplaceAppsOrigins() { function getMarketplaceAppsHosts() {
return appConfig.marketplaceAppList.map(({ url }) => url); return {
} frames: appConfig.marketplaceAppList.map(({ url }) => new URL(url).host),
logos: appConfig.marketplaceAppList.map(({ logo }) => new URL(logo).host),
function getMarketplaceAppsLogosOrigins() { };
return appConfig.marketplaceAppList.map(({ logo }) => new URL(logo));
} }
// we cannot use lodash/uniq in middleware code since it calls new Set() and it'is causing an error in Nextjs // 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>) { ...@@ -46,7 +45,7 @@ function unique(array: Array<string | undefined>) {
} }
function makePolicyMap() { function makePolicyMap() {
const networkExternalAssets = getNetworksExternalAssets(); const marketplaceAppsHosts = getMarketplaceAppsHosts();
return { return {
'default-src': [ 'default-src': [
...@@ -135,10 +134,10 @@ function makePolicyMap() { ...@@ -135,10 +134,10 @@ function makePolicyMap() {
'avatars.githubusercontent.com', // github avatars 'avatars.githubusercontent.com', // github avatars
// network assets // network assets
...networkExternalAssets.map((url) => url.host), ...getNetworksExternalAssetsHosts(),
// marketplace apps logos // marketplace apps logos
...getMarketplaceAppsLogosOrigins().map((url) => url.host), ...marketplaceAppsHosts.logos,
// ad // ad
'servedbyadbutler.com', 'servedbyadbutler.com',
...@@ -172,7 +171,7 @@ function makePolicyMap() { ...@@ -172,7 +171,7 @@ function makePolicyMap() {
], ],
'frame-src': [ 'frame-src': [
...getMarketplaceAppsOrigins(), ...marketplaceAppsHosts.frames,
// ad // ad
'request-global.czilladx.com', 'request-global.czilladx.com',
......
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() { ...@@ -14,14 +14,6 @@ export default function useGetCsrfToken() {
const url = buildUrl('csrf'); const url = buildUrl('csrf');
const apiResponse = await fetch(url, { credentials: 'include' }); const apiResponse = await fetch(url, { credentials: 'include' });
const csrfFromHeader = apiResponse.headers.get('x-bs-account-csrf'); 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; return csrfFromHeader ? { token: csrfFromHeader } : undefined;
} }
......
...@@ -7,14 +7,16 @@ import abiIcon from 'icons/ABI.svg'; ...@@ -7,14 +7,16 @@ import abiIcon from 'icons/ABI.svg';
import apiKeysIcon from 'icons/API.svg'; import apiKeysIcon from 'icons/API.svg';
import appsIcon from 'icons/apps.svg'; import appsIcon from 'icons/apps.svg';
import blocksIcon from 'icons/block.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 privateTagIcon from 'icons/privattags.svg';
import profileIcon from 'icons/profile.svg'; import profileIcon from 'icons/profile.svg';
import publicTagIcon from 'icons/publictags.svg'; import publicTagIcon from 'icons/publictags.svg';
import statsIcon from 'icons/stats.svg'; import statsIcon from 'icons/stats.svg';
import tokensIcon from 'icons/token.svg'; import tokensIcon from 'icons/token.svg';
import topAccountsIcon from 'icons/top-accounts.svg';
import transactionsIcon from 'icons/transactions.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 watchlistIcon from 'icons/watchlist.svg';
import notEmpty from 'lib/notEmpty'; import notEmpty from 'lib/notEmpty';
...@@ -26,31 +28,64 @@ export interface NavItem { ...@@ -26,31 +28,64 @@ export interface NavItem {
isNewUi?: boolean; isNewUi?: boolean;
} }
export interface NavGroupItem extends Omit<NavItem, 'nextRoute'> {
subItems: Array<NavItem>;
}
interface ReturnType { interface ReturnType {
mainNavItems: Array<NavItem>; mainNavItems: Array<NavItem | NavGroupItem>;
accountNavItems: Array<NavItem>; accountNavItems: Array<NavItem>;
profileItem: NavItem; profileItem: NavItem;
} }
export function isGroupItem(item: NavItem | NavGroupItem): item is NavGroupItem {
return 'subItems' in item;
}
export default function useNavItems(): ReturnType { 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 router = useRouter();
const pathname = router.pathname; const pathname = router.pathname;
return React.useMemo(() => { 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: '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 }, { 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: '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 ? isMarketplaceFilled ?
{ text: 'Apps', nextRoute: { pathname: '/apps' as const }, icon: appsIcon, isActive: pathname.startsWith('/app'), isNewUi: true } : null, { 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 }, { 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' // 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/ // 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 // 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); ].filter(notEmpty);
const accountNavItems = [ const accountNavItems = [
...@@ -87,5 +122,5 @@ export default function useNavItems(): ReturnType { ...@@ -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 }; text: 'My profile', nextRoute: { pathname: '/auth/profile' as const }, icon: profileIcon, isActive: pathname === '/auth/profile', isNewUi: true };
return { mainNavItems, accountNavItems, profileItem }; return { mainNavItems, accountNavItems, profileItem };
}, [ isMarketplaceFilled, pathname ]); }, [ hasAPIDocs, isMarketplaceFilled, pathname ]);
} }
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) { ...@@ -36,8 +36,10 @@ export function middleware(req: NextRequest) {
/** /**
* Configure which routes should pass through the Middleware. * Configure which routes should pass through the Middleware.
* Exclude all `_next` urls.
*/ */
export const config = { export const config = {
matcher: [ '/', '/:notunderscore((?!_next).+)' ], matcher: [ '/', '/:notunderscore((?!_next).+)' ],
// matcher: [
// '/((?!.*\\.|api\\/|node-api\\/).*)', // exclude all static + api + node-api routes
// ],
}; };
const withTM = require('next-transpile-modules')([
'react-syntax-highlighter',
'swagger-client',
'swagger-ui-react',
]);
const withRoutes = require('nextjs-routes/config')({ const withRoutes = require('nextjs-routes/config')({
outDir: 'types', outDir: 'types',
}); });
...@@ -7,7 +12,7 @@ const headers = require('./configs/nextjs/headers'); ...@@ -7,7 +12,7 @@ const headers = require('./configs/nextjs/headers');
const redirects = require('./configs/nextjs/redirects'); const redirects = require('./configs/nextjs/redirects');
const rewrites = require('./configs/nextjs/rewrites'); const rewrites = require('./configs/nextjs/rewrites');
const moduleExports = { const moduleExports = withTM({
include: path.resolve(__dirname, 'icons'), include: path.resolve(__dirname, 'icons'),
reactStrictMode: true, reactStrictMode: true,
webpack(config, { webpack }) { webpack(config, { webpack }) {
...@@ -34,6 +39,11 @@ const moduleExports = { ...@@ -34,6 +39,11 @@ const moduleExports = {
redirects, redirects,
headers, headers,
output: 'standalone', 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); module.exports = withRoutes(moduleExports);
...@@ -12,6 +12,7 @@ ...@@ -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: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: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": "./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": "next build",
"build:docker": "docker build --build-arg GIT_COMMIT_SHA=$(git rev-parse HEAD) -t blockscout ./", "build:docker": "docker build --build-arg GIT_COMMIT_SHA=$(git rev-parse HEAD) -t blockscout ./",
"start": "next start", "start": "next start",
...@@ -69,6 +70,7 @@ ...@@ -69,6 +70,7 @@
"react-identicons": "^1.2.5", "react-identicons": "^1.2.5",
"react-jazzicon": "^1.0.4", "react-jazzicon": "^1.0.4",
"react-scroll": "^1.8.7", "react-scroll": "^1.8.7",
"swagger-ui-react": "^4.15.5",
"use-font-face-observer": "^1.2.1", "use-font-face-observer": "^1.2.1",
"wagmi": "^0.10.6" "wagmi": "^0.10.6"
}, },
...@@ -85,6 +87,7 @@ ...@@ -85,6 +87,7 @@
"@types/qrcode": "^1.5.0", "@types/qrcode": "^1.5.0",
"@types/react": "18.0.9", "@types/react": "18.0.9",
"@types/react-dom": "18.0.5", "@types/react-dom": "18.0.5",
"@types/swagger-ui-react": "^4.11.0",
"@types/react-google-recaptcha": "^2.1.5", "@types/react-google-recaptcha": "^2.1.5",
"@types/ws": "^8.5.3", "@types/ws": "^8.5.3",
"@typescript-eslint/eslint-plugin": "^5.27.0", "@typescript-eslint/eslint-plugin": "^5.27.0",
...@@ -101,6 +104,7 @@ ...@@ -101,6 +104,7 @@
"jest-environment-jsdom": "^29.2.1", "jest-environment-jsdom": "^29.2.1",
"lint-staged": ">=10", "lint-staged": ">=10",
"mockdate": "^3.0.5", "mockdate": "^3.0.5",
"next-transpile-modules": "^10.0.0",
"playwright": "^1.28.0", "playwright": "^1.28.0",
"svgo": "^2.8.0", "svgo": "^2.8.0",
"ts-jest": "^29.0.3", "ts-jest": "^29.0.3",
......
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'; ...@@ -4,6 +4,7 @@ import { WebSocketServer } from 'ws';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address'; import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import type { NewBlockSocketResponse } from 'types/api/block'; import type { NewBlockSocketResponse } from 'types/api/block';
import type { SmartContractVerificationResponse } from 'types/api/contract';
type ReturnType = () => Promise<WebSocket>; type ReturnType = () => Promise<WebSocket>;
...@@ -59,6 +60,7 @@ export function sendMessage(socket: WebSocket, channel: Channel, msg: 'token_bal ...@@ -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: '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: '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: '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 { export function sendMessage(socket: WebSocket, channel: Channel, msg: string, payload: unknown): void {
socket.send(JSON.stringify([ socket.send(JSON.stringify([
...channel, ...channel,
......
*,
*::after,
*::before {
transition-delay: 0s !important;
transition-duration: 0s !important;
animation-delay: -0.0001s !important;
animation-duration: 0s !important;
animation-play-state: paused !important;
}
\ No newline at end of file
import './fonts.css'; import './fonts.css';
import './index.css';
import { beforeMount } from '@playwright/experimental-ct-react/hooks'; import { beforeMount } from '@playwright/experimental-ct-react/hooks';
import _defaultsDeep from 'lodash/defaultsDeep'; import _defaultsDeep from 'lodash/defaultsDeep';
import MockDate from 'mockdate'; import MockDate from 'mockdate';
......
...@@ -20,6 +20,9 @@ const semanticTokens = { ...@@ -20,6 +20,9 @@ const semanticTokens = {
_dark: 'red.300', _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; export default semanticTokens;
...@@ -42,7 +42,7 @@ export interface TokenInstance { ...@@ -42,7 +42,7 @@ export interface TokenInstance {
image_url: string | null; image_url: string | null;
animation_url: string | null; animation_url: string | null;
external_app_url: string | null; external_app_url: string | null;
metadata: unknown; metadata: Record<string, unknown> | null;
owner: AddressParam; owner: AddressParam;
token: TokenInfo; token: TokenInfo;
} }
......
...@@ -16,6 +16,7 @@ declare module "nextjs-routes" { ...@@ -16,6 +16,7 @@ declare module "nextjs-routes" {
| DynamicRoute<"/address/[hash]", { "hash": string }> | DynamicRoute<"/address/[hash]", { "hash": string }>
| StaticRoute<"/api/csrf"> | StaticRoute<"/api/csrf">
| StaticRoute<"/api/proxy"> | StaticRoute<"/api/proxy">
| StaticRoute<"/api-docs">
| DynamicRoute<"/apps/[id]", { "id": string }> | DynamicRoute<"/apps/[id]", { "id": string }>
| StaticRoute<"/apps"> | StaticRoute<"/apps">
| StaticRoute<"/auth/auth0"> | StaticRoute<"/auth/auth0">
......
...@@ -76,7 +76,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => { ...@@ -76,7 +76,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
return ( return (
<> <>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<SkeletonTable columns={ [ '17%', '17%', '16%', '25%', '25%' ] }/> <SkeletonTable columns={ [ '17%', '17%', '16%', '25%', '25%' ] } isLong/>
</Hide> </Hide>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
<SkeletonList/> <SkeletonList/>
...@@ -125,7 +125,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => { ...@@ -125,7 +125,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
return ( return (
<Box> <Box>
{ query.isPaginationVisible && ( { query.isPaginationVisible && (
<ActionBar mt={ -6 }> <ActionBar mt={ -6 } showShadow={ query.isLoading }>
<Pagination ml="auto" { ...query.pagination }/> <Pagination ml="auto" { ...query.pagination }/>
</ActionBar> </ActionBar>
) } ) }
......
...@@ -17,9 +17,12 @@ import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; ...@@ -17,9 +17,12 @@ import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
interface Props { interface Props {
tabs: Array<RoutedSubTab>; tabs: Array<RoutedSubTab>;
addressHash?: string;
} }
export const currentChain: Chain = { const { wagmiClient, ethereumClient } = (() => {
try {
const currentChain: Chain = {
id: Number(appConfig.network.id), id: Number(appConfig.network.id),
name: appConfig.network.name || '', name: appConfig.network.name || '',
network: appConfig.network.name || '', network: appConfig.network.name || '',
...@@ -39,38 +42,49 @@ export const currentChain: Chain = { ...@@ -39,38 +42,49 @@ export const currentChain: Chain = {
url: appConfig.baseUrl, url: appConfig.baseUrl,
}, },
}, },
}; };
const chains = [ currentChain ]; const chains = [ currentChain ];
const { provider } = configureChains(chains, [ const { provider } = configureChains(chains, [
walletConnectProvider({ projectId: appConfig.walletConnect.projectId || '' }), walletConnectProvider({ projectId: appConfig.walletConnect.projectId || '' }),
]); ]);
const wagmiClient = createClient({ const wagmiClient = createClient({
autoConnect: true, autoConnect: true,
connectors: modalConnectors({ appName: 'web3Modal', chains }), connectors: modalConnectors({ appName: 'web3Modal', chains }),
provider, 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 = { const TAB_LIST_PROPS = {
columnGap: 3, columnGap: 3,
}; };
const AddressContract = ({ tabs }: Props) => { const AddressContract = ({ addressHash, tabs }: Props) => {
const modalZIndex = useToken<string>('zIndices', 'modal'); 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 ( return (
<WagmiConfig client={ wagmiClient }> <WagmiConfig client={ wagmiClient }>
<ContractContextProvider> <ContractContextProvider addressHash={ addressHash }>
<RoutedTabs tabs={ tabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS }/> <RoutedTabs tabs={ tabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS }/>
</ContractContextProvider> </ContractContextProvider>
<Web3Modal <Web3Modal
projectId={ appConfig.walletConnect.projectId } projectId={ appConfig.walletConnect.projectId }
ethereumClient={ ethereumClient } ethereumClient={ ethereumClient }
themeZIndex={ Number(modalZIndex) } themeZIndex={ Number(modalZIndex) }
themeMode={ useColorModeValue('light', 'dark') } themeMode={ web3ModalTheme }
themeBackground="themeColor" themeBackground="themeColor"
/> />
</WagmiConfig> </WagmiConfig>
......
...@@ -11,6 +11,7 @@ import * as countersMock from 'mocks/address/counters'; ...@@ -11,6 +11,7 @@ import * as countersMock from 'mocks/address/counters';
import * as tokensMock from 'mocks/address/tokens'; import * as tokensMock from 'mocks/address/tokens';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
import insertAdPlaceholder from 'playwright/utils/insertAdPlaceholder';
import AddressDetails from './AddressDetails'; import AddressDetails from './AddressDetails';
import MockAddressPage from './testUtils/MockAddressPage'; import MockAddressPage from './testUtils/MockAddressPage';
...@@ -44,6 +45,8 @@ test('contract +@mobile', async({ mount, page }) => { ...@@ -44,6 +45,8 @@ test('contract +@mobile', async({ mount, page }) => {
{ hooksConfig }, { hooksConfig },
); );
await insertAdPlaceholder(page);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -82,6 +85,8 @@ test('token', async({ mount, page }) => { ...@@ -82,6 +85,8 @@ test('token', async({ mount, page }) => {
{ hooksConfig }, { hooksConfig },
); );
await insertAdPlaceholder(page);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -102,5 +107,7 @@ test('validator +@mobile', async({ mount, page }) => { ...@@ -102,5 +107,7 @@ test('validator +@mobile', async({ mount, page }) => {
{ hooksConfig }, { hooksConfig },
); );
await insertAdPlaceholder(page);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -16,6 +16,7 @@ import AddressLink from 'ui/shared/address/AddressLink'; ...@@ -16,6 +16,7 @@ import AddressLink from 'ui/shared/address/AddressLink';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo'; import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
...@@ -92,7 +93,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -92,7 +93,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
<Text fontSize="sm">Verify with other explorers</Text> <Text fontSize="sm">Verify with other explorers</Text>
{ explorers.map((explorer) => { { explorers.map((explorer) => {
const url = new URL(explorer.paths.address + '/' + addressHash, explorer.baseUrl); 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> </Flex>
) } ) }
...@@ -193,6 +194,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -193,6 +194,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
</LinkInternal> </LinkInternal>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
<DetailsSponsoredItem/>
</Grid> </Grid>
</Box> </Box>
); );
......
...@@ -9,13 +9,13 @@ import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; ...@@ -9,13 +9,13 @@ import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import AddressIntTxsSkeletonDesktop from 'ui/address/internals/AddressIntTxsSkeletonDesktop';
import AddressIntTxsSkeletonMobile from 'ui/address/internals/AddressIntTxsSkeletonMobile';
import AddressIntTxsTable from 'ui/address/internals/AddressIntTxsTable'; import AddressIntTxsTable from 'ui/address/internals/AddressIntTxsTable';
import EmptySearchResult from 'ui/apps/EmptySearchResult'; import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Pagination from 'ui/shared/Pagination'; 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 AddressCsvExportLink from './AddressCsvExportLink';
import AddressTxsFilter from './AddressTxsFilter'; import AddressTxsFilter from './AddressTxsFilter';
...@@ -43,29 +43,33 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE ...@@ -43,29 +43,33 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
onFilterChange({ filter: newVal }); onFilterChange({ filter: newVal });
}, [ onFilterChange ]); }, [ onFilterChange ]);
const content = (() => {
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) { if (isLoading) {
return ( return (
<> <>
<Show below="lg" ssr={ false }><AddressIntTxsSkeletonMobile/></Show> <Show below="lg" ssr={ false }>
<Hide below="lg" ssr={ false }><AddressIntTxsSkeletonDesktop/></Hide> <SkeletonList/>
</Show>
<Hide below="lg" ssr={ false }>
<SkeletonTable columns={ [ '15%', '15%', '10%', '20%', '20%', '20%' ] } isLong/>
</Hide>
</> </>
); );
} }
if (isError) {
return <DataFetchAlert/>;
}
if (data.items.length === 0 && !filterValue) { if (data.items.length === 0 && !filterValue) {
return <Text as="span">There are no internal transactions for this address.</Text>; return <Text as="span">There are no internal transactions for this address.</Text>;
} }
let content;
if (data.items.length === 0) { if (data.items.length === 0) {
content = <EmptySearchResult text={ `Couldn${ apos }t find any transaction that matches your query.` }/>; return <EmptySearchResult text={ `Couldn${ apos }t find any transaction that matches your query.` }/>;
} else { }
content = (
return (
<> <>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
<AddressIntTxsList data={ data.items } currentAddress={ hash }/> <AddressIntTxsList data={ data.items } currentAddress={ hash }/>
...@@ -75,11 +79,11 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE ...@@ -75,11 +79,11 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
</Hide> </Hide>
</> </>
); );
} })();
return ( return (
<> <>
<ActionBar mt={ -6 } justifyContent="left"> <ActionBar mt={ -6 } justifyContent="left" showShadow={ isLoading }>
<AddressTxsFilter <AddressTxsFilter
defaultFilter={ filterValue } defaultFilter={ filterValue }
onFilterChange={ handleFilterChange } onFilterChange={ handleFilterChange }
......
...@@ -25,7 +25,7 @@ const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement> ...@@ -25,7 +25,7 @@ const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>
} }
const bar = isPaginationVisible ? ( const bar = isPaginationVisible ? (
<ActionBar mt={ -6 }> <ActionBar mt={ -6 } showShadow>
<Pagination ml="auto" { ...pagination }/> <Pagination ml="auto" { ...pagination }/>
</ActionBar> </ActionBar>
) : null; ) : null;
......
...@@ -169,7 +169,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -169,7 +169,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
return ( return (
<> <>
<Hide below="lg" ssr={ false }> <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> </Hide>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
<SkeletonList/> <SkeletonList/>
...@@ -253,7 +253,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -253,7 +253,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
<> <>
{ isMobile && tokenFilterComponent } { isMobile && tokenFilterComponent }
{ !isActionBarHidden && ( { !isActionBarHidden && (
<ActionBar mt={ -6 }> <ActionBar mt={ -6 } showShadow={ isLoading }>
{ !isMobile && tokenFilterComponent } { !isMobile && tokenFilterComponent }
{ !tokenFilter && ( { !tokenFilter && (
<TokenTransferFilter <TokenTransferFilter
......
...@@ -138,7 +138,7 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>} ...@@ -138,7 +138,7 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
return ( return (
<> <>
{ !isMobile && ( { !isMobile && (
<ActionBar mt={ -6 }> <ActionBar mt={ -6 } showShadow={ addressTxsQuery.isLoading }>
{ filter } { filter }
{ currentAddress && <AddressCsvExportLink address={ currentAddress } type="transactions" ml="auto"/> } { currentAddress && <AddressCsvExportLink address={ currentAddress } type="transactions" ml="auto"/> }
{ addressTxsQuery.isPaginationVisible && <Pagination { ...addressTxsQuery.pagination } ml={ 8 }/> } { addressTxsQuery.isPaginationVisible && <Pagination { ...addressTxsQuery.pagination } ml={ 8 }/> }
...@@ -152,6 +152,8 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>} ...@@ -152,6 +152,8 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
showSocketInfo={ addressTxsQuery.pagination.page === 1 } showSocketInfo={ addressTxsQuery.pagination.page === 1 }
socketInfoAlert={ socketAlert } socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount } socketInfoNum={ newItemsCount }
top={ 80 }
hasLongSkeleton
/> />
</> </>
); );
......
...@@ -43,6 +43,10 @@ const AddressCoinBalanceHistory = ({ query }: Props) => { ...@@ -43,6 +43,10 @@ const AddressCoinBalanceHistory = ({ query }: Props) => {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
if (query.data.items.length === 0 && !query.isPaginationVisible) {
return <span>There is no coin balance history for this address</span>;
}
return ( return (
<> <>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
......
...@@ -23,7 +23,7 @@ test('verified with changed byte code +@mobile +@dark-mode', async({ mount, page ...@@ -23,7 +23,7 @@ test('verified with changed byte code +@mobile +@dark-mode', async({ mount, page
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractCode/> <ContractCode addressHash={ addressHash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -39,7 +39,7 @@ test('verified with multiple sources +@mobile', async({ mount, page }) => { ...@@ -39,7 +39,7 @@ test('verified with multiple sources +@mobile', async({ mount, page }) => {
await mount( await mount(
<TestApp> <TestApp>
<ContractCode/> <ContractCode addressHash={ addressHash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -57,7 +57,7 @@ test('verified via sourcify', async({ mount, page }) => { ...@@ -57,7 +57,7 @@ test('verified via sourcify', async({ mount, page }) => {
await mount( await mount(
<TestApp> <TestApp>
<ContractCode/> <ContractCode addressHash={ addressHash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -73,7 +73,7 @@ test('self destructed', async({ mount, page }) => { ...@@ -73,7 +73,7 @@ test('self destructed', async({ mount, page }) => {
await mount( await mount(
<TestApp> <TestApp>
<ContractCode/> <ContractCode addressHash={ addressHash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -90,7 +90,7 @@ test('with twin address alert +@mobile', async({ mount, page }) => { ...@@ -90,7 +90,7 @@ test('with twin address alert +@mobile', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractCode/> <ContractCode addressHash={ addressHash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -106,7 +106,7 @@ test('with proxy address alert +@mobile', async({ mount, page }) => { ...@@ -106,7 +106,7 @@ test('with proxy address alert +@mobile', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractCode/> <ContractCode addressHash={ addressHash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -122,7 +122,7 @@ test('non verified', async({ mount, page }) => { ...@@ -122,7 +122,7 @@ test('non verified', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractCode/> <ContractCode addressHash={ addressHash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
......
import { Flex, Skeleton, Button, Grid, GridItem, Text, Alert, Link, chakra, Box } from '@chakra-ui/react'; 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 { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import getQueryParamString from 'lib/router/getQueryParamString';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
...@@ -16,6 +14,10 @@ import RawDataSnippet from 'ui/shared/RawDataSnippet'; ...@@ -16,6 +14,10 @@ import RawDataSnippet from 'ui/shared/RawDataSnippet';
import ContractSourceCode from './ContractSourceCode'; import ContractSourceCode from './ContractSourceCode';
type Props = {
addressHash?: string;
}
const InfoItem = chakra(({ label, value, className }: { label: string; value: string; className?: string }) => ( const InfoItem = chakra(({ label, value, className }: { label: string; value: string; className?: string }) => (
<GridItem display="flex" columnGap={ 6 } wordBreak="break-all" className={ className }> <GridItem display="flex" columnGap={ 6 } wordBreak="break-all" className={ className }>
<Text w="170px" flexShrink={ 0 } fontWeight={ 500 }>{ label }</Text> <Text w="170px" flexShrink={ 0 } fontWeight={ 500 }>{ label }</Text>
...@@ -23,10 +25,7 @@ const InfoItem = chakra(({ label, value, className }: { label: string; value: st ...@@ -23,10 +25,7 @@ const InfoItem = chakra(({ label, value, className }: { label: string; value: st
</GridItem> </GridItem>
)); ));
const ContractCode = () => { const ContractCode = ({ addressHash }: Props) => {
const router = useRouter();
const addressHash = getQueryParamString(router.query.hash);
const { data, isLoading, isError } = useApiQuery('contract', { const { data, isLoading, isError } = useApiQuery('contract', {
pathParams: { hash: addressHash }, pathParams: { hash: addressHash },
queryOptions: { queryOptions: {
...@@ -62,7 +61,7 @@ const ContractCode = () => { ...@@ -62,7 +61,7 @@ const ContractCode = () => {
ml="auto" ml="auto"
mr={ 3 } mr={ 3 }
as="a" as="a"
href={ route({ pathname: '/address/[hash]/contract_verification', query: { hash: addressHash } }) } href={ route({ pathname: '/address/[hash]/contract_verification', query: { hash: addressHash || '' } }) }
> >
Verify & publish Verify & publish
</Button> </Button>
...@@ -115,7 +114,7 @@ const ContractCode = () => { ...@@ -115,7 +114,7 @@ const ContractCode = () => {
{ data.is_verified_via_sourcify && ( { data.is_verified_via_sourcify && (
<Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap"> <Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap">
<span>This contract has been { data.is_partially_verified ? 'partially ' : '' }verified via Sourcify. </span> <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> </Alert>
) } ) }
{ data.is_changed_bytecode && ( { data.is_changed_bytecode && (
...@@ -131,7 +130,9 @@ const ContractCode = () => { ...@@ -131,7 +130,9 @@ const ContractCode = () => {
<AddressLink type="address" hash={ data.verified_twin_address_hash } truncation="constant" ml={ 2 }/> <AddressLink type="address" hash={ data.verified_twin_address_hash } truncation="constant" ml={ 2 }/>
</Address> </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> <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> <span> page</span>
</Alert> </Alert>
) } ) }
......
...@@ -35,14 +35,24 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, renderC ...@@ -35,14 +35,24 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, renderC
setId((id) => id + 1); setId((id) => id + 1);
}, []); }, []);
if (data.length === 0) {
return null;
}
return ( return (
<>
<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 }> <Accordion allowMultiple position="relative" onChange={ handleAccordionStateChange } index={ expandedSections }>
{ data.map((item, index) => { { data.map((item, index) => {
return ( return (
<AccordionItem key={ index } as="section" _first={{ borderTopWidth: '0', '.chakra-accordion__button': { pr: '150px' } }}> <AccordionItem key={ index } as="section" _first={{ borderTopWidth: '0' }}>
<h2> <h2>
<AccordionButton px={ 0 } py={ 3 } _hover={{ bgColor: 'inherit' }} wordBreak="break-all" textAlign="left"> <AccordionButton px={ 0 } py={ 3 } _hover={{ bgColor: 'inherit' }} wordBreak="break-all" textAlign="left">
<Box as="span" fontFamily="heading" fontWeight={ 500 } fontSize="lg" mr={ 1 }> <Box as="span" fontWeight={ 500 } mr={ 1 }>
{ index + 1 }. { item.type === 'fallback' || item.type === 'receive' ? item.type : item.name } { index + 1 }. { item.type === 'fallback' || item.type === 'receive' ? item.type : item.name }
</Box> </Box>
{ item.type === 'fallback' && ( { item.type === 'fallback' && (
...@@ -72,13 +82,8 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, renderC ...@@ -72,13 +82,8 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, renderC
</AccordionItem> </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> </Accordion>
</>
); );
}; };
......
...@@ -28,7 +28,7 @@ test('base view +@mobile +@dark-mode', async({ mount, page }) => { ...@@ -28,7 +28,7 @@ test('base view +@mobile +@dark-mode', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractRead/> <ContractRead addressHash={ addressHash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -57,7 +57,7 @@ test('error result', async({ mount, page }) => { ...@@ -57,7 +57,7 @@ test('error result', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractRead/> <ContractRead addressHash={ addressHash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
......
import { Alert, Flex } from '@chakra-ui/react'; import { Alert, Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { useAccount } from 'wagmi'; import { useAccount } from 'wagmi';
...@@ -7,7 +6,6 @@ import type { SmartContractReadMethod, SmartContractQueryMethodRead } from 'type ...@@ -7,7 +6,6 @@ import type { SmartContractReadMethod, SmartContractQueryMethodRead } from 'type
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion'; import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
...@@ -20,17 +18,15 @@ import ContractMethodConstant from './ContractMethodConstant'; ...@@ -20,17 +18,15 @@ import ContractMethodConstant from './ContractMethodConstant';
import ContractReadResult from './ContractReadResult'; import ContractReadResult from './ContractReadResult';
interface Props { interface Props {
addressHash?: string;
isProxy?: boolean; isProxy?: boolean;
isCustomAbi?: boolean; isCustomAbi?: boolean;
} }
const ContractRead = ({ isProxy, isCustomAbi }: Props) => { const ContractRead = ({ addressHash, isProxy, isCustomAbi }: Props) => {
const router = useRouter();
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const { address: userAddress } = useAccount(); const { address: userAddress } = useAccount();
const addressHash = getQueryParamString(router.query.hash);
const { data, isLoading, isError } = useApiQuery(isProxy ? 'contract_methods_read_proxy' : 'contract_methods_read', { const { data, isLoading, isError } = useApiQuery(isProxy ? 'contract_methods_read_proxy' : 'contract_methods_read', {
pathParams: { hash: addressHash }, pathParams: { hash: addressHash },
queryParams: { queryParams: {
......
...@@ -23,7 +23,7 @@ test('base view +@mobile', async({ mount, page }) => { ...@@ -23,7 +23,7 @@ test('base view +@mobile', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractWrite/> <ContractWrite addressHash={ addressHash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
......
import _capitalize from 'lodash/capitalize'; import _capitalize from 'lodash/capitalize';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { useAccount, useSigner } from 'wagmi'; import { useAccount, useSigner } from 'wagmi';
...@@ -7,7 +6,6 @@ import type { SmartContractWriteMethod } from 'types/api/contract'; ...@@ -7,7 +6,6 @@ import type { SmartContractWriteMethod } from 'types/api/contract';
import config from 'configs/app/config'; import config from 'configs/app/config';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion'; import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
...@@ -21,14 +19,12 @@ import ContractWriteResult from './ContractWriteResult'; ...@@ -21,14 +19,12 @@ import ContractWriteResult from './ContractWriteResult';
import { getNativeCoinValue, isExtendedError } from './utils'; import { getNativeCoinValue, isExtendedError } from './utils';
interface Props { interface Props {
addressHash?: string;
isProxy?: boolean; isProxy?: boolean;
isCustomAbi?: boolean; isCustomAbi?: boolean;
} }
const ContractWrite = ({ isProxy, isCustomAbi }: Props) => { const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
const router = useRouter();
const addressHash = getQueryParamString(router.query.hash);
const { data: signer } = useSigner(); const { data: signer } = useSigner();
const { isConnected } = useAccount(); const { isConnected } = useAccount();
......
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import type { Contract } from 'ethers'; import type { Contract } from 'ethers';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { useContract, useProvider, useSigner } from 'wagmi'; import { useContract, useProvider, useSigner } from 'wagmi';
...@@ -9,6 +8,7 @@ import type { Address } from 'types/api/address'; ...@@ -9,6 +8,7 @@ import type { Address } from 'types/api/address';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
type ProviderProps = { type ProviderProps = {
addressHash?: string;
children: React.ReactNode; children: React.ReactNode;
} }
...@@ -24,13 +24,11 @@ const ContractContext = React.createContext<TContractContext>({ ...@@ -24,13 +24,11 @@ const ContractContext = React.createContext<TContractContext>({
custom: null, custom: null,
}); });
export function ContractContextProvider({ children }: ProviderProps) { export function ContractContextProvider({ addressHash, children }: ProviderProps) {
const router = useRouter();
const provider = useProvider(); const provider = useProvider();
const { data: signer } = useSigner(); const { data: signer } = useSigner();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const addressHash = router.query.hash?.toString();
const { data: contractInfo } = useApiQuery('contract', { const { data: contractInfo } = useApiQuery('contract', {
pathParams: { hash: addressHash }, pathParams: { hash: addressHash },
queryOptions: { queryOptions: {
......
...@@ -20,7 +20,7 @@ const AddressNameInfo = ({ data }: Props) => { ...@@ -20,7 +20,7 @@ const AddressNameInfo = ({ data }: Props) => {
hint="Token name and symbol" hint="Token name and symbol"
> >
<LinkInternal href={ route({ pathname: '/token/[hash]', query: { hash: data.token.address } }) }> <LinkInternal href={ route({ pathname: '/token/[hash]', query: { hash: data.token.address } }) }>
{ data.token.name }{ symbol } { data.token.name || 'Unnamed token' }{ symbol }
</LinkInternal> </LinkInternal>
</DetailsInfoItem> </DetailsInfoItem>
); );
......
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;
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;
/* 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 = () => { ...@@ -118,7 +118,7 @@ const BlockDetails = () => {
hint="The number of transactions in the block" hint="The number of transactions in the block"
> >
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height, tab: 'txs' } }) }> <LinkInternal href={ route({ pathname: '/block/[height]', query: { height, tab: 'txs' } }) }>
{ data.tx_count } transactions { data.tx_count } transaction{ data.tx_count === 1 ? '' : 's' }
</LinkInternal> </LinkInternal>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
...@@ -274,18 +274,18 @@ const BlockDetails = () => { ...@@ -274,18 +274,18 @@ const BlockDetails = () => {
<DetailsInfoItem <DetailsInfoItem
title="Difficulty" title="Difficulty"
hint={ `Block difficulty for ${ validatorTitle }, used to calibrate block generation time` } 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>
<DetailsInfoItem <DetailsInfoItem
title="Total difficulty" title="Total difficulty"
hint="Total difficulty of the chain until this block" 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> </DetailsInfoItem>
{ sectionGap } { sectionGap }
......
...@@ -3,6 +3,7 @@ import React from 'react'; ...@@ -3,6 +3,7 @@ import React from 'react';
import type { SmartContractVerificationConfig } from 'types/api/contract'; import type { SmartContractVerificationConfig } from 'types/api/contract';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import ContractVerificationForm from './ContractVerificationForm'; import ContractVerificationForm from './ContractVerificationForm';
...@@ -34,6 +35,7 @@ const formConfig: SmartContractVerificationConfig = { ...@@ -34,6 +35,7 @@ const formConfig: SmartContractVerificationConfig = {
'sourcify', 'sourcify',
'multi-part', 'multi-part',
'vyper-code', 'vyper-code',
'vyper-multi-part',
], ],
vyper_compiler_versions: [ vyper_compiler_versions: [
'v0.3.7+commit.6020b8bb', 'v0.3.7+commit.6020b8bb',
...@@ -60,8 +62,9 @@ test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) = ...@@ -60,8 +62,9 @@ test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) =
{ hooksConfig }, { hooksConfig },
); );
await page.getByText(/flattened source code/i).click(); await component.getByLabel(/verification method/i).focus();
await page.getByText(/optimization enabled/i).click(); 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.getByText(/add contract libraries/i).click();
await page.locator('button[aria-label="add"]').click(); await page.locator('button[aria-label="add"]').click();
...@@ -76,21 +79,32 @@ test('standard input json method', async({ mount, page }) => { ...@@ -76,21 +79,32 @@ test('standard input json method', async({ mount, page }) => {
{ hooksConfig }, { 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(); await expect(component).toHaveScreenshot();
}); });
test('sourcify method +@dark-mode +@mobile', async({ mount, page }) => { test.describe('sourcify', () => {
const testWithSocket = test.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
testWithSocket.describe.configure({ mode: 'serial', timeout: 20_000 });
testWithSocket('with multiple contracts +@mobile', async({ mount, page, createSocket }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp withSocket>
<ContractVerificationForm config={ formConfig } hash={ hash }/> <ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
await page.getByText(/via sourcify/i).click(); await component.getByLabel(/verification method/i).focus();
await page.getByText(/upload files/i).click(); 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([ await page.locator('input[name="sources"]').setInputFiles([
'./playwright/mocks/file_mock_1.json', './playwright/mocks/file_mock_1.json',
'./playwright/mocks/file_mock_2.json', './playwright/mocks/file_mock_2.json',
...@@ -98,6 +112,28 @@ test('sourcify method +@dark-mode +@mobile', async({ mount, page }) => { ...@@ -98,6 +112,28 @@ test('sourcify method +@dark-mode +@mobile', async({ mount, page }) => {
]); ]);
await expect(component).toHaveScreenshot(); 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 }) => { test('multi-part files method', async({ mount, page }) => {
...@@ -108,7 +144,9 @@ test('multi-part files method', async({ mount, page }) => { ...@@ -108,7 +144,9 @@ test('multi-part files method', async({ mount, page }) => {
{ hooksConfig }, { 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(); await expect(component).toHaveScreenshot();
}); });
...@@ -121,7 +159,24 @@ test('vyper contract method', async({ mount, page }) => { ...@@ -121,7 +159,24 @@ test('vyper contract method', async({ mount, page }) => {
{ hooksConfig }, { 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(); await expect(component).toHaveScreenshot();
}); });
import { Button, chakra } from '@chakra-ui/react'; import { Button, chakra, useUpdateEffect } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
...@@ -20,7 +20,7 @@ import ContractVerificationSourcify from './methods/ContractVerificationSourcify ...@@ -20,7 +20,7 @@ import ContractVerificationSourcify from './methods/ContractVerificationSourcify
import ContractVerificationStandardInput from './methods/ContractVerificationStandardInput'; import ContractVerificationStandardInput from './methods/ContractVerificationStandardInput';
import ContractVerificationVyperContract from './methods/ContractVerificationVyperContract'; import ContractVerificationVyperContract from './methods/ContractVerificationVyperContract';
import ContractVerificationVyperMultiPartFile from './methods/ContractVerificationVyperMultiPartFile'; import ContractVerificationVyperMultiPartFile from './methods/ContractVerificationVyperMultiPartFile';
import { prepareRequestBody, formatSocketErrors } from './utils'; import { prepareRequestBody, formatSocketErrors, DEFAULT_VALUES } from './utils';
const METHOD_COMPONENTS = { const METHOD_COMPONENTS = {
'flattened-code': <ContractVerificationFlattenSourceCode/>, 'flattened-code': <ContractVerificationFlattenSourceCode/>,
...@@ -40,11 +40,9 @@ interface Props { ...@@ -40,11 +40,9 @@ interface Props {
const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Props) => { const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Props) => {
const formApi = useForm<FormFields>({ const formApi = useForm<FormFields>({
mode: 'onBlur', mode: 'onBlur',
defaultValues: { defaultValues: methodFromQuery ? DEFAULT_VALUES[methodFromQuery] : undefined,
method: methodFromQuery,
},
}); });
const { control, handleSubmit, watch, formState, setError } = formApi; const { control, handleSubmit, watch, formState, setError, reset } = formApi;
const submitPromiseResolver = React.useRef<(value: unknown) => void>(); const submitPromiseResolver = React.useRef<(value: unknown) => void>();
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
...@@ -56,7 +54,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -56,7 +54,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
try { try {
await apiFetch('contract_verification_via', { await apiFetch('contract_verification_via', {
pathParams: { method: data.method, hash }, pathParams: { method: data.method.value, hash: hash.toLowerCase() },
fetchParams: { fetchParams: {
method: 'POST', method: 'POST',
body, body,
...@@ -109,7 +107,10 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -109,7 +107,10 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
variant: 'subtle', variant: 'subtle',
isClosable: true, 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({ const channel = useSocketChannel({
topic: `addresses:${ hash.toLowerCase() }`, topic: `addresses:${ hash.toLowerCase() }`,
...@@ -124,7 +125,15 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -124,7 +125,15 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
}); });
const method = watch('method'); 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 ( return (
<FormProvider { ...formApi }> <FormProvider { ...formApi }>
...@@ -134,8 +143,8 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -134,8 +143,8 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
> >
<ContractVerificationFieldMethod <ContractVerificationFieldMethod
control={ control } control={ control }
isDisabled={ Boolean(method) }
methods={ config.verification_options } methods={ config.verification_options }
isDisabled={ formState.isSubmitting }
/> />
{ content } { content }
{ Boolean(method) && ( { Boolean(method) && (
......
...@@ -15,8 +15,8 @@ const ContractVerificationMethod = ({ title, children }: Props) => { ...@@ -15,8 +15,8 @@ const ContractVerificationMethod = ({ title, children }: Props) => {
return ( return (
<section ref={ ref }> <section ref={ ref }>
<Text variant="secondary" mt={ 12 } mb={ 5 } fontSize="sm">{ title }</Text> <Text fontWeight={ 500 } fontSize="lg" fontFamily="heading" mt={ 12 } mb={ 5 }>{ title }</Text>
<Grid columnGap="30px" rowGap={{ base: 2, lg: 4 }} templateColumns={{ base: '1fr', lg: 'minmax(auto, 680px) minmax(0, 340px)' }}> <Grid columnGap="30px" rowGap={{ base: 2, lg: 5 }} templateColumns={{ base: '1fr', lg: 'minmax(0, 680px) minmax(0, 340px)' }}>
{ children } { children }
</Grid> </Grid>
</section> </section>
......
...@@ -34,7 +34,6 @@ const ContractVerificationFieldAutodetectArgs = () => { ...@@ -34,7 +34,6 @@ const ContractVerificationFieldAutodetectArgs = () => {
name="autodetect_constructor_args" name="autodetect_constructor_args"
control={ control } control={ control }
render={ renderControl } render={ renderControl }
defaultValue={ true }
/> />
</ContractVerificationFormRow> </ContractVerificationFormRow>
{ !isOn && <ContractVerificationFieldConstructorArgs/> } { !isOn && <ContractVerificationFieldConstructorArgs/> }
......
...@@ -41,7 +41,6 @@ const ContractVerificationFieldCode = ({ isVyper }: Props) => { ...@@ -41,7 +41,6 @@ const ContractVerificationFieldCode = ({ isVyper }: Props) => {
control={ control } control={ control }
render={ renderControl } render={ renderControl }
rules={{ required: true }} rules={{ required: true }}
defaultValue=""
/> />
{ isVyper ? null : ( { isVyper ? null : (
<> <>
......
import { Code } from '@chakra-ui/react'; import { chakra, Checkbox, Code } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form'; import type { ControllerRenderProps } from 'react-hook-form';
...@@ -20,26 +20,30 @@ interface Props { ...@@ -20,26 +20,30 @@ interface Props {
} }
const ContractVerificationFieldCompiler = ({ isVyper }: 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 isMobile = useIsMobile();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config')); const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config'));
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(() => ( const options = React.useMemo(() => (
(isVyper ? config?.vyper_compiler_versions : config?.solidity_compiler_versions)?.map((option) => ({ label: option, value: option })) || [] (isVyper ? config?.vyper_compiler_versions : config?.solidity_compiler_versions)?.map((option) => ({ label: option, value: option })) || []
), [ config?.solidity_compiler_versions, config?.vyper_compiler_versions, isVyper ]); ), [ config?.solidity_compiler_versions, config?.vyper_compiler_versions, isVyper ]);
const loadOptions = React.useCallback(async(inputValue: string) => { const loadOptions = React.useCallback(async(inputValue: string) => {
return options return options
.filter(({ label }) => { .filter(({ label }) => !inputValue || label.toLowerCase().includes(inputValue.toLowerCase()))
if (!inputValue) { .filter(({ label }) => isNightly ? true : !label.includes('nightly'))
return !label.toLowerCase().includes('nightly');
}
return label.toLowerCase().includes(inputValue.toLowerCase());
})
.slice(0, OPTIONS_LIMIT); .slice(0, OPTIONS_LIMIT);
}, [ options ]); }, [ isNightly, options ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'compiler'>}) => { const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'compiler'>}) => {
const error = 'compiler' in formState.errors ? formState.errors.compiler : undefined; const error = 'compiler' in formState.errors ? formState.errors.compiler : undefined;
...@@ -61,20 +65,32 @@ const ContractVerificationFieldCompiler = ({ isVyper }: Props) => { ...@@ -61,20 +65,32 @@ const ContractVerificationFieldCompiler = ({ isVyper }: Props) => {
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<>
{ !isVyper && (
<Checkbox
size="lg"
mb={ 2 }
onChange={ handleCheckboxChange }
isDisabled={ formState.isSubmitting }
>
Include nightly builds
</Checkbox>
) }
<Controller <Controller
name="compiler" name="compiler"
control={ control } control={ control }
render={ renderControl } render={ renderControl }
rules={{ required: true }} rules={{ required: true }}
/> />
</>
{ isVyper ? null : ( { isVyper ? null : (
<> <chakra.div mt={{ base: 0, lg: 8 }}>
<span>The compiler version is specified in </span> <span >The compiler version is specified in </span>
<Code color="text_secondary">pragma solidity X.X.X</Code> <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> <span>. Use the compiler version rather than the nightly build. If using the Solidity compiler, run </span>
<Code color="text_secondary">solc —version</Code> <Code color="text_secondary">solc —version</Code>
<span> to check.</span> <span> to check.</span>
</> </chakra.div>
) } ) }
</ContractVerificationFormRow> </ContractVerificationFormRow>
); );
......
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);
import { Checkbox } from '@chakra-ui/react'; import { Checkbox, useUpdateEffect } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form'; import { useFieldArray, useFormContext } from 'react-hook-form';
...@@ -8,13 +8,21 @@ import ContractVerificationFormRow from '../ContractVerificationFormRow'; ...@@ -8,13 +8,21 @@ import ContractVerificationFormRow from '../ContractVerificationFormRow';
import ContractVerificationFieldLibraryItem from './ContractVerificationFieldLibraryItem'; import ContractVerificationFieldLibraryItem from './ContractVerificationFieldLibraryItem';
const ContractVerificationFieldLibraries = () => { const ContractVerificationFieldLibraries = () => {
const { formState, control } = useFormContext<FormFields>(); const { formState, control, getValues } = useFormContext<FormFields>();
const { fields, append, remove, insert } = useFieldArray({ const { fields, append, remove, insert } = useFieldArray({
name: 'libraries', name: 'libraries',
control, control,
}); });
const [ isEnabled, setIsEnabled ] = React.useState(fields.length > 0); 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(() => { const handleCheckboxChange = React.useCallback(() => {
if (!isEnabled) { if (!isEnabled) {
append({ name: '', address: '' }); append({ name: '', address: '' });
......
import { import {
RadioGroup,
Radio,
Stack,
Text,
Link, Link,
Icon, Icon,
chakra, chakra,
...@@ -14,16 +10,23 @@ import { ...@@ -14,16 +10,23 @@ import {
PopoverBody, PopoverBody,
useColorModeValue, useColorModeValue,
DarkMode, DarkMode,
useBoolean, ListItem,
OrderedList,
Grid,
Box,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps, Control } from 'react-hook-form'; import type { ControllerRenderProps, Control } from 'react-hook-form';
import { Controller } from 'react-hook-form'; import { Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import type { SmartContractVerificationMethod, SmartContractVerificationConfig } from 'types/api/contract'; import type { SmartContractVerificationConfig, SmartContractVerificationMethod } from 'types/api/contract';
import infoIcon from 'icons/info.svg'; 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 { interface Props {
control: Control<FormFields>; control: Control<FormFields>;
...@@ -32,91 +35,93 @@ interface Props { ...@@ -32,91 +35,93 @@ interface Props {
} }
const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props) => { const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props) => {
const [ isPopoverOpen, setIsPopoverOpen ] = useBoolean();
const tooltipBg = useColorModeValue('gray.700', 'gray.900'); const tooltipBg = useColorModeValue('gray.700', 'gray.900');
const isMobile = useIsMobile();
const options = React.useMemo(() => methods.map((method) => ({
value: method,
label: METHOD_LABELS[method],
})), [ methods ]);
const renderItem = React.useCallback((method: SmartContractVerificationMethod) => { 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) { switch (method) {
case 'flattened-code': 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': case 'standard-input':
return ( return (
<> <ListItem>
<span>Via standard </span> <span>Verification using </span>
<Link <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" target="_blank"
cursor={ isDisabled ? 'not-allowed' : 'pointer' }
> >
Input JSON Standard input JSON
</Link> </Link>
</> <span> file.</span>
</ListItem>
); );
case 'sourcify': case 'vyper-code':
return <ListItem>Verification of Vyper contract.</ListItem>;
case 'vyper-multi-part':
return <ListItem>Verification of multi-part Vyper files.</ListItem>;
}
}, []);
return ( return (
<> <section>
<span>Via sourcify: sources and metadata JSON file</span> <Grid columnGap="30px" rowGap={{ base: 2, lg: 4 }} templateColumns={{ base: '1fr', lg: 'minmax(auto, 680px) minmax(0, 340px)' }}>
<Popover trigger="hover" isLazy isOpen={ isDisabled ? false : isPopoverOpen } onOpen={ setIsPopoverOpen.on } onClose={ setIsPopoverOpen.off }> <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> <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' }}/> <Icon as={ infoIcon } boxSize={ 5 } color="link" _hover={{ color: 'link_hovered' }}/>
</chakra.span> </chakra.span>
</PopoverTrigger> </PopoverTrigger>
<Portal> <Portal>
<PopoverContent bgColor={ tooltipBg }> <PopoverContent bgColor={ tooltipBg } w={{ base: '300px', lg: '380px' }}>
<PopoverArrow bgColor={ tooltipBg }/> <PopoverArrow bgColor={ tooltipBg }/>
<PopoverBody color="white"> <PopoverBody color="white">
<DarkMode> <DarkMode>
<div> <span>Currently, Blockscout supports { methods.length } methods:</span>
<span>Verification through </span> <OrderedList>
<Link href="https://sourcify.dev/" target="_blank">Sourcify</Link> { methods.map(renderPopoverListItem) }
</div> </OrderedList>
<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>
</DarkMode> </DarkMode>
</PopoverBody> </PopoverBody>
</PopoverContent> </PopoverContent>
</Portal> </Portal>
</Popover> </Popover>
</> </Box>
);
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 <Controller
name="method" name="method"
control={ control } control={ control }
render={ renderRadioGroup } render={ renderControl }
rules={{ required: true }}
/> />
</div>
</Grid>
</section> </section>
); );
}; };
......
...@@ -41,7 +41,6 @@ const ContractVerificationFieldName = ({ hint }: Props) => { ...@@ -41,7 +41,6 @@ const ContractVerificationFieldName = ({ hint }: Props) => {
control={ control } control={ control }
render={ renderControl } render={ renderControl }
rules={{ required: true }} rules={{ required: true }}
defaultValue=""
/> />
{ hint ? <span>{ hint }</span> : ( { hint ? <span>{ hint }</span> : (
<> <>
......
import { FormControl, Input } from '@chakra-ui/react'; import { Flex, Input } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form'; import type { ControllerRenderProps } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form'; import { Controller, useFormContext } from 'react-hook-form';
...@@ -6,7 +6,6 @@ import { Controller, useFormContext } from 'react-hook-form'; ...@@ -6,7 +6,6 @@ import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import CheckboxInput from 'ui/shared/CheckboxInput'; import CheckboxInput from 'ui/shared/CheckboxInput';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
...@@ -14,56 +13,59 @@ const ContractVerificationFieldOptimization = () => { ...@@ -14,56 +13,59 @@ const ContractVerificationFieldOptimization = () => {
const [ isEnabled, setIsEnabled ] = React.useState(true); const [ isEnabled, setIsEnabled ] = React.useState(true);
const { formState, control } = useFormContext<FormFields>(); const { formState, control } = useFormContext<FormFields>();
const error = 'optimization_runs' in formState.errors ? formState.errors.optimization_runs : undefined;
const handleCheckboxChange = React.useCallback(() => { const handleCheckboxChange = React.useCallback(() => {
setIsEnabled(prev => !prev); setIsEnabled(prev => !prev);
}, []); }, []);
const renderCheckboxControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'is_optimization_enabled'>}) => ( const renderCheckboxControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'is_optimization_enabled'>}) => (
<Flex flexShrink={ 0 }>
<CheckboxInput<FormFields, 'is_optimization_enabled'> <CheckboxInput<FormFields, 'is_optimization_enabled'>
text="Optimization enabled" text="Optimization enabled"
field={ field } field={ field }
onChange={ handleCheckboxChange } onChange={ handleCheckboxChange }
isDisabled={ formState.isSubmitting } isDisabled={ formState.isSubmitting }
/> />
</Flex>
), [ formState.isSubmitting, handleCheckboxChange ]); ), [ formState.isSubmitting, handleCheckboxChange ]);
const renderInputControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'optimization_runs'>}) => { const renderInputControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'optimization_runs'>}) => {
return ( return (
<FormControl variant="floating" id={ field.name } size={{ base: 'md', lg: 'lg' }} isRequired>
<Input <Input
{ ...field } { ...field }
required required
isDisabled={ formState.isSubmitting } isDisabled={ formState.isSubmitting }
autoComplete="off" autoComplete="off"
type="number" type="number"
placeholder="Optimization runs"
size="xs"
minW="100px"
maxW="200px"
flexShrink={ 1 }
isInvalid={ Boolean(error) }
/> />
<InputPlaceholder text="Optimization runs"/>
</FormControl>
); );
}, [ formState.isSubmitting ]); }, [ error, formState.isSubmitting ]);
return ( return (
<>
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Flex columnGap={ 5 } h={{ base: 'auto', lg: '32px' }}>
<Controller <Controller
name="is_optimization_enabled" name="is_optimization_enabled"
control={ control } control={ control }
render={ renderCheckboxControl } render={ renderCheckboxControl }
defaultValue={ true }
/> />
</ContractVerificationFormRow>
{ isEnabled && ( { isEnabled && (
<ContractVerificationFormRow>
<Controller <Controller
name="optimization_runs" name="optimization_runs"
control={ control } control={ control }
render={ renderInputControl } render={ renderInputControl }
rules={{ required: true }} rules={{ required: true }}
defaultValue="200"
/> />
</ContractVerificationFormRow>
) } ) }
</> </Flex>
</ContractVerificationFormRow>
); );
}; };
......
import { Text, Button, Box, chakra, Flex } from '@chakra-ui/react'; import { Text, Button, Box, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps, FieldPathValue, ValidateResult } from 'react-hook-form'; import type { ControllerRenderProps, FieldPathValue, ValidateResult } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form'; import { Controller, useFormContext } from 'react-hook-form';
...@@ -6,7 +6,7 @@ import { Controller, useFormContext } from 'react-hook-form'; ...@@ -6,7 +6,7 @@ import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import { Mb } from 'lib/consts'; import { Mb } from 'lib/consts';
// import DragAndDropArea from 'ui/shared/forms/DragAndDropArea'; import DragAndDropArea from 'ui/shared/forms/DragAndDropArea';
import FieldError from 'ui/shared/forms/FieldError'; import FieldError from 'ui/shared/forms/FieldError';
import FileInput from 'ui/shared/forms/FileInput'; import FileInput from 'ui/shared/forms/FileInput';
import FileSnippet from 'ui/shared/forms/FileSnippet'; import FileSnippet from 'ui/shared/forms/FileSnippet';
...@@ -19,11 +19,10 @@ interface Props { ...@@ -19,11 +19,10 @@ interface Props {
fileTypes: Array<FileTypes>; fileTypes: Array<FileTypes>;
multiple?: boolean; multiple?: boolean;
title: string; title: string;
className?: string;
hint: 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 { setValue, getValues, control, formState, clearErrors } = useFormContext<FormFields>();
const error = 'sources' in formState.errors ? formState.errors.sources : undefined; const error = 'sources' in formState.errors ? formState.errors.sources : undefined;
...@@ -42,11 +41,28 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam ...@@ -42,11 +41,28 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam
}, [ getValues, clearErrors, setValue ]); }, [ 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 renderFiles = React.useCallback((files: Array<File>) => {
const errorList = fileError?.message?.split(';'); const errorList = fileError?.message?.split(';');
return ( 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) => ( { files.map((file, index) => (
<Box key={ file.name + file.lastModified + index }> <Box key={ file.name + file.lastModified + index }>
<FileSnippet <FileSnippet
...@@ -55,42 +71,36 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam ...@@ -55,42 +71,36 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam
onRemove={ handleFileRemove } onRemove={ handleFileRemove }
index={ index } index={ index }
isDisabled={ formState.isSubmitting } isDisabled={ formState.isSubmitting }
error={ errorList?.[index] }
/> />
{ errorList?.[index] && <FieldError message={ errorList?.[index] } mt={ 1 } px={ 3 }/> }
</Box> </Box>
)) } )) }
</Box> </Box>
); );
}, [ formState.isSubmitting, handleFileRemove, fileError ]); }, [ formState.isSubmitting, handleFileRemove, fileError ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'sources'>}) => ( 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 }> <FileInput<FormFields, 'sources'> accept={ fileTypes.join(',') } multiple={ multiple } field={ field }>
{ () => ( { ({ onChange }) => (
<Flex <Flex
flexDir="column" flexDir="column"
alignItems="flex-start" alignItems="flex-start"
rowGap={ 2 } rowGap={ 2 }
w="100%" w="100%"
display={ field.value && field.value.length > 0 && !multiple ? 'none' : 'block' }
mb={ field.value && field.value.length > 0 ? 2 : 0 }
> >
<Button <DragAndDropArea onDrop={ onChange } p={{ base: 3, lg: 6 }} isDisabled={ formState.isSubmitting }>
variant="outline" { hasValue ? renderFiles(field.value) : renderUploadButton() }
size="sm" </DragAndDropArea>
// mb={ 2 }
>
Upload file{ multiple ? 's' : '' }
</Button>
{ /* design is not ready */ }
{ /* <DragAndDropArea onDrop={ onChange }/> */ }
</Flex> </Flex>
) } ) }
</FileInput> </FileInput>
{ field.value && field.value.length > 0 && renderFiles(field.value) }
{ commonError?.message && <FieldError message={ commonError.type === 'required' ? 'Field is required' : commonError.message }/> } { commonError?.message && <FieldError message={ commonError.type === 'required' ? 'Field is required' : commonError.message }/> }
</> </>
), [ fileTypes, commonError, multiple, renderFiles ]); );
}, [ fileTypes, multiple, commonError, formState.isSubmitting, renderFiles, renderUploadButton ]);
const validateFileType = React.useCallback(async(value: FieldPathValue<FormFields, 'sources'>): Promise<ValidateResult> => { const validateFileType = React.useCallback(async(value: FieldPathValue<FormFields, 'sources'>): Promise<ValidateResult> => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
...@@ -114,19 +124,24 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam ...@@ -114,19 +124,24 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam
return true; 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(() => ({ const rules = React.useMemo(() => ({
required: true, required: true,
validate: { validate: {
file_type: validateFileType, file_type: validateFileType,
file_size: validateFileSize, file_size: validateFileSize,
quantity: validateQuantity,
}, },
}), [ validateFileSize, validateFileType ]); }), [ validateFileSize, validateFileType, validateQuantity ]);
return ( return (
<>
<ContractVerificationFormRow >
<Text fontWeight={ 500 } className={ className } mt={ 4 }>{ title }</Text>
</ContractVerificationFormRow>
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <Controller
name="sources" name="sources"
...@@ -136,8 +151,7 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam ...@@ -136,8 +151,7 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam
/> />
{ hint ? <span>{ hint }</span> : null } { hint ? <span>{ hint }</span> : null }
</ContractVerificationFormRow> </ContractVerificationFormRow>
</>
); );
}; };
export default React.memo(chakra(ContractVerificationFieldSources)); export default React.memo(ContractVerificationFieldSources);
...@@ -12,9 +12,9 @@ import ContractVerificationFieldOptimization from '../fields/ContractVerificatio ...@@ -12,9 +12,9 @@ import ContractVerificationFieldOptimization from '../fields/ContractVerificatio
const ContractVerificationFlattenSourceCode = () => { const ContractVerificationFlattenSourceCode = () => {
return ( return (
<ContractVerificationMethod title="New Solidity/Yul Smart Contract Verification"> <ContractVerificationMethod title="Contract verification via Solidity (fattened source code)">
<ContractVerificationFieldIsYul/>
<ContractVerificationFieldName/> <ContractVerificationFieldName/>
<ContractVerificationFieldIsYul/>
<ContractVerificationFieldCompiler/> <ContractVerificationFieldCompiler/>
<ContractVerificationFieldEvmVersion/> <ContractVerificationFieldEvmVersion/>
<ContractVerificationFieldOptimization/> <ContractVerificationFieldOptimization/>
......
...@@ -11,7 +11,7 @@ const FILE_TYPES = [ '.sol' as const, '.yul' as const ]; ...@@ -11,7 +11,7 @@ const FILE_TYPES = [ '.sol' as const, '.yul' as const ];
const ContractVerificationMultiPartFile = () => { const ContractVerificationMultiPartFile = () => {
return ( return (
<ContractVerificationMethod title="New Solidity/Yul Smart Contract Verification"> <ContractVerificationMethod title="Contract verification via Solidity (multi-part files)">
<ContractVerificationFieldCompiler/> <ContractVerificationFieldCompiler/>
<ContractVerificationFieldEvmVersion/> <ContractVerificationFieldEvmVersion/>
<ContractVerificationFieldOptimization/> <ContractVerificationFieldOptimization/>
......
import React from 'react'; import React from 'react';
import ContractVerificationMethod from '../ContractVerificationMethod'; import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldContractIndex from '../fields/ContractVerificationFieldContractIndex';
import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources'; import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources';
const FILE_TYPES = [ '.json' as const, '.sol' as const ]; const FILE_TYPES = [ '.json' as const, '.sol' as const ];
const ContractVerificationSourcify = () => { const ContractVerificationSourcify = () => {
return ( return (
<ContractVerificationMethod title="New Smart Contract Verification"> <ContractVerificationMethod title="Contract verification via Solidity (Sourcify)">
<ContractVerificationFieldSources <ContractVerificationFieldSources
fileTypes={ FILE_TYPES } fileTypes={ FILE_TYPES }
multiple 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." hint="Upload all Solidity contract source files and JSON metadata file(s) created during contract compilation."
/> />
<ContractVerificationFieldContractIndex/>
</ContractVerificationMethod> </ContractVerificationMethod>
); );
}; };
......
...@@ -10,7 +10,7 @@ const FILE_TYPES = [ '.json' as const ]; ...@@ -10,7 +10,7 @@ const FILE_TYPES = [ '.json' as const ];
const ContractVerificationStandardInput = () => { const ContractVerificationStandardInput = () => {
return ( return (
<ContractVerificationMethod title="New Smart Contract Verification"> <ContractVerificationMethod title="Contract verification via Solidity (standard JSON input) ">
<ContractVerificationFieldName/> <ContractVerificationFieldName/>
<ContractVerificationFieldCompiler/> <ContractVerificationFieldCompiler/>
<ContractVerificationFieldSources <ContractVerificationFieldSources
......
...@@ -8,7 +8,7 @@ import ContractVerificationFieldName from '../fields/ContractVerificationFieldNa ...@@ -8,7 +8,7 @@ import ContractVerificationFieldName from '../fields/ContractVerificationFieldNa
const ContractVerificationVyperContract = () => { const ContractVerificationVyperContract = () => {
return ( 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."/> <ContractVerificationFieldName hint="Must match the name specified in the code."/>
<ContractVerificationFieldCompiler isVyper/> <ContractVerificationFieldCompiler isVyper/>
<ContractVerificationFieldCode isVyper/> <ContractVerificationFieldCode isVyper/>
......
...@@ -9,7 +9,7 @@ const FILE_TYPES = [ '.vy' as const ]; ...@@ -9,7 +9,7 @@ const FILE_TYPES = [ '.vy' as const ];
const ContractVerificationVyperMultiPartFile = () => { const ContractVerificationVyperMultiPartFile = () => {
return ( return (
<ContractVerificationMethod title="New Vyper Smart Contract Verification"> <ContractVerificationMethod title="Contract verification via Vyper (multi-part files)">
<ContractVerificationFieldCompiler isVyper/> <ContractVerificationFieldCompiler isVyper/>
<ContractVerificationFieldEvmVersion isVyper/> <ContractVerificationFieldEvmVersion isVyper/>
<ContractVerificationFieldSources <ContractVerificationFieldSources
......
import type { SmartContractVerificationMethod } from 'types/api/contract';
import type { Option } from 'ui/shared/FancySelect/types'; import type { Option } from 'ui/shared/FancySelect/types';
export interface ContractLibrary { export interface ContractLibrary {
name: string; name: string;
address: string; address: string;
} }
interface MethodOption {
label: string;
value: SmartContractVerificationMethod;
}
export interface FormFieldsFlattenSourceCode { export interface FormFieldsFlattenSourceCode {
method: 'flattened-code'; method: MethodOption;
is_yul: boolean; is_yul: boolean;
name: string; name: string;
compiler: Option; compiler: Option;
...@@ -19,7 +26,7 @@ export interface FormFieldsFlattenSourceCode { ...@@ -19,7 +26,7 @@ export interface FormFieldsFlattenSourceCode {
} }
export interface FormFieldsStandardInput { export interface FormFieldsStandardInput {
method: 'standard-input'; method: MethodOption;
name: string; name: string;
compiler: Option; compiler: Option;
sources: Array<File>; sources: Array<File>;
...@@ -28,12 +35,13 @@ export interface FormFieldsStandardInput { ...@@ -28,12 +35,13 @@ export interface FormFieldsStandardInput {
} }
export interface FormFieldsSourcify { export interface FormFieldsSourcify {
method: 'sourcify'; method: MethodOption;
sources: Array<File>; sources: Array<File>;
contract_index?: Option;
} }
export interface FormFieldsMultiPartFile { export interface FormFieldsMultiPartFile {
method: 'multi-part'; method: MethodOption;
compiler: Option; compiler: Option;
evm_version: Option; evm_version: Option;
is_optimization_enabled: boolean; is_optimization_enabled: boolean;
...@@ -43,7 +51,7 @@ export interface FormFieldsMultiPartFile { ...@@ -43,7 +51,7 @@ export interface FormFieldsMultiPartFile {
} }
export interface FormFieldsVyperContract { export interface FormFieldsVyperContract {
method: 'vyper-code'; method: MethodOption;
name: string; name: string;
compiler: Option; compiler: Option;
code: string; code: string;
...@@ -51,7 +59,7 @@ export interface FormFieldsVyperContract { ...@@ -51,7 +59,7 @@ export interface FormFieldsVyperContract {
} }
export interface FormFieldsVyperMultiPartFile { export interface FormFieldsVyperMultiPartFile {
method: 'vyper-multi-part'; method: MethodOption;
compiler: Option; compiler: Option;
evm_version: Option; evm_version: Option;
sources: Array<File>; sources: Array<File>;
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment