Commit 6b444293 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into long-skeleton

parents af2c8685 0063a3fa
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
...@@ -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` |
......
...@@ -32,6 +32,7 @@ NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true ...@@ -32,6 +32,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
......
<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': [
...@@ -130,10 +129,10 @@ function makePolicyMap() { ...@@ -130,10 +129,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',
...@@ -167,7 +166,7 @@ function makePolicyMap() { ...@@ -167,7 +166,7 @@ function makePolicyMap() {
], ],
'frame-src': [ 'frame-src': [
...getMarketplaceAppsOrigins(), ...marketplaceAppsHosts.frames,
// ad // ad
'request-global.czilladx.com', 'request-global.czilladx.com',
......
...@@ -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 globeIcon from 'icons/globe-b.svg';
// import gearIcon from 'icons/gear.svg'; // import gearIcon from 'icons/gear.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,24 +28,44 @@ export interface NavItem { ...@@ -26,24 +28,44 @@ 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 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 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 },
......
...@@ -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
// ],
}; };
...@@ -34,6 +34,11 @@ const moduleExports = { ...@@ -34,6 +34,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);
...@@ -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';
......
...@@ -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;
} }
......
...@@ -20,40 +20,47 @@ interface Props { ...@@ -20,40 +20,47 @@ interface Props {
addressHash?: string; addressHash?: string;
} }
export const currentChain: Chain = { const { wagmiClient, ethereumClient } = (() => {
id: Number(appConfig.network.id), try {
name: appConfig.network.name || '', const currentChain: Chain = {
network: appConfig.network.name || '', id: Number(appConfig.network.id),
nativeCurrency: { name: appConfig.network.name || '',
decimals: appConfig.network.currency.decimals, network: appConfig.network.name || '',
name: appConfig.network.currency.name || '', nativeCurrency: {
symbol: appConfig.network.currency.symbol || '', decimals: appConfig.network.currency.decimals,
}, name: appConfig.network.currency.name || '',
rpcUrls: { symbol: appConfig.network.currency.symbol || '',
'default': { },
http: [ appConfig.network.rpcUrl || '' ], rpcUrls: {
}, 'default': {
}, http: [ appConfig.network.rpcUrl || '' ],
blockExplorers: { },
'default': { },
name: 'Blockscout', blockExplorers: {
url: appConfig.baseUrl, 'default': {
}, name: 'Blockscout',
}, 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,
...@@ -61,6 +68,12 @@ const TAB_LIST_PROPS = { ...@@ -61,6 +68,12 @@ const TAB_LIST_PROPS = {
const AddressContract = ({ addressHash, 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 }>
...@@ -71,7 +84,7 @@ const AddressContract = ({ addressHash, tabs }: Props) => { ...@@ -71,7 +84,7 @@ const AddressContract = ({ addressHash, tabs }: Props) => {
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';
...@@ -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>
); );
......
...@@ -35,50 +35,55 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, renderC ...@@ -35,50 +35,55 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, renderC
setId((id) => id + 1); setId((id) => id + 1);
}, []); }, []);
if (data.length === 0) {
return null;
}
return ( return (
<Accordion allowMultiple position="relative" onChange={ handleAccordionStateChange } index={ expandedSections }> <>
{ data.map((item, index) => { <Flex mb={ 3 }>
return ( <Box fontWeight={ 500 }>Contract information</Box>
<AccordionItem key={ index } as="section" _first={{ borderTopWidth: '0', '.chakra-accordion__button': { pr: '150px' } }}> <Link onClick={ handleExpandAll } ml="auto">{ expandedSections.length === data.length ? 'Collapse' : 'Expand' } all</Link>
<h2> <Link onClick={ handleReset } ml={ 3 }>Reset</Link>
<AccordionButton px={ 0 } py={ 3 } _hover={{ bgColor: 'inherit' }} wordBreak="break-all" textAlign="left"> </Flex>
<Box as="span" fontFamily="heading" fontWeight={ 500 } fontSize="lg" mr={ 1 }> <Accordion allowMultiple position="relative" onChange={ handleAccordionStateChange } index={ expandedSections }>
{ index + 1 }. { item.type === 'fallback' || item.type === 'receive' ? item.type : item.name } { data.map((item, index) => {
</Box> return (
{ item.type === 'fallback' && ( <AccordionItem key={ index } as="section" _first={{ borderTopWidth: '0' }}>
<Hint <h2>
label={ <AccordionButton px={ 0 } py={ 3 } _hover={{ bgColor: 'inherit' }} wordBreak="break-all" textAlign="left">
`The fallback function is executed on a call to the contract if none of the other functions match <Box as="span" fontWeight={ 500 } mr={ 1 }>
{ index + 1 }. { item.type === 'fallback' || item.type === 'receive' ? item.type : item.name }
</Box>
{ item.type === 'fallback' && (
<Hint
label={
`The fallback function is executed on a call to the contract if none of the other functions match
the given function signature, or if no data was supplied at all and there is no receive Ether function. the given function signature, or if no data was supplied at all and there is no receive Ether function.
The fallback function always receives data, but in order to also receive Ether it must be marked payable.` The fallback function always receives data, but in order to also receive Ether it must be marked payable.`
}/> }/>
) } ) }
{ item.type === 'receive' && ( { item.type === 'receive' && (
<Hint <Hint
label={ label={
`The receive function is executed on a call to the contract with empty calldata. `The receive function is executed on a call to the contract with empty calldata.
This is the function that is executed on plain Ether transfers (e.g. via .send() or .transfer()). This is the function that is executed on plain Ether transfers (e.g. via .send() or .transfer()).
If no such function exists, but a payable fallback function exists, the fallback function will be called on a plain Ether transfer. If no such function exists, but a payable fallback function exists, the fallback function will be called on a plain Ether transfer.
If neither a receive Ether nor a payable fallback function is present, If neither a receive Ether nor a payable fallback function is present,
the contract cannot receive Ether through regular transactions and throws an exception.` the contract cannot receive Ether through regular transactions and throws an exception.`
}/> }/>
) } ) }
<AccordionIcon/> <AccordionIcon/>
</AccordionButton> </AccordionButton>
</h2> </h2>
<AccordionPanel pb={ 4 } px={ 0 }> <AccordionPanel pb={ 4 } px={ 0 }>
{ renderContent(item, index, id) } { renderContent(item, index, id) }
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
); );
}) } }) }
{ data.length > 0 && ( </Accordion>
<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>
); );
}; };
......
...@@ -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,12 +79,64 @@ test('standard input json method', async({ mount, page }) => { ...@@ -76,12 +79,64 @@ 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(
<TestApp withSocket>
<ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>,
{ hooksConfig },
);
await component.getByLabel(/verification method/i).focus();
await component.getByLabel(/verification method/i).type('solidity');
await page.getByRole('button', { name: /sourcify/i }).click();
await page.getByText(/drop files/i).click();
await page.locator('input[name="sources"]').setInputFiles([
'./playwright/mocks/file_mock_1.json',
'./playwright/mocks/file_mock_2.json',
'./playwright/mocks/file_mock_with_very_long_name.json',
]);
await expect(component).toHaveScreenshot();
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ hash.toLowerCase() }`);
await page.getByRole('button', { name: /verify/i }).click();
socketServer.sendMessage(socket, channel, 'verification_result', {
status: 'error',
errors: {
// eslint-disable-next-line max-len
files: [ 'Detected 5 contracts (ERC20, IERC20, IERC20Metadata, Context, MockERC20), but can only verify 1 at a time. Please choose a main contract and click Verify again.' ],
},
});
await component.getByLabel(/contract name/i).focus();
await component.getByLabel(/contract name/i).type('e');
const contractNameOption = page.getByRole('button', { name: /MockERC20/i });
await expect(contractNameOption).toBeVisible();
await expect(component).toHaveScreenshot();
});
});
test('multi-part files method', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractVerificationForm config={ formConfig } hash={ hash }/> <ContractVerificationForm config={ formConfig } hash={ hash }/>
...@@ -89,18 +144,14 @@ test('sourcify method +@dark-mode +@mobile', async({ mount, page }) => { ...@@ -89,18 +144,14 @@ test('sourcify method +@dark-mode +@mobile', async({ mount, page }) => {
{ 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.locator('input[name="sources"]').setInputFiles([ await page.getByRole('button', { name: /multi-part files/i }).click();
'./playwright/mocks/file_mock_1.json',
'./playwright/mocks/file_mock_2.json',
'./playwright/mocks/file_mock_with_very_long_name.json',
]);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('multi-part files method', async({ mount, page }) => { test('vyper contract method', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractVerificationForm config={ formConfig } hash={ hash }/> <ContractVerificationForm config={ formConfig } hash={ hash }/>
...@@ -108,12 +159,14 @@ test('multi-part files method', async({ mount, page }) => { ...@@ -108,12 +159,14 @@ 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('vyper');
await page.getByRole('button', { name: /contract/i }).click();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('vyper contract method', async({ mount, page }) => { test('vyper multi-part method', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractVerificationForm config={ formConfig } hash={ hash }/> <ContractVerificationForm config={ formConfig } hash={ hash }/>
...@@ -121,7 +174,9 @@ test('vyper contract method', async({ mount, page }) => { ...@@ -121,7 +174,9 @@ 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: /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>
<Controller <>
name="compiler" { !isVyper && (
control={ control } <Checkbox
render={ renderControl } size="lg"
rules={{ required: true }} mb={ 2 }
/> onChange={ handleCheckboxChange }
isDisabled={ formState.isSubmitting }
>
Include nightly builds
</Checkbox>
) }
<Controller
name="compiler"
control={ control }
render={ renderControl }
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 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 renderItem = React.useCallback((method: SmartContractVerificationMethod) => { 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 ( return <ListItem>Verification of Vyper contract.</ListItem>;
<> case 'vyper-multi-part':
<span>Via sourcify: sources and metadata JSON file</span> return <ListItem>Verification of multi-part Vyper files.</ListItem>;
<Popover trigger="hover" isLazy isOpen={ isDisabled ? false : isPopoverOpen } onOpen={ setIsPopoverOpen.on } onClose={ setIsPopoverOpen.off }> }
}, []);
return (
<section>
<Grid columnGap="30px" rowGap={{ base: 2, lg: 4 }} templateColumns={{ base: '1fr', lg: 'minmax(auto, 680px) minmax(0, 340px)' }}>
<div>
<Box mb={ 5 }>
<chakra.span fontWeight={ 500 } fontSize="lg" fontFamily="heading">
Currently, Blockscout supports { methods.length } contract verification methods
</chakra.span>
<Popover trigger="hover" isLazy placement={ isMobile ? 'bottom-end' : 'right-start' } offset={ [ -8, 8 ] }>
<PopoverTrigger> <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>
); <Controller
case 'multi-part': name="method"
return 'Via multi-part files'; control={ control }
case 'vyper-code': render={ renderControl }
return 'Vyper contract'; rules={{ required: true }}
case 'vyper-multi-part': />
return 'Via multi-part Vyper files'; </div>
</Grid>
default:
break;
}
}, [ isDisabled, isPopoverOpen, setIsPopoverOpen.off, setIsPopoverOpen.on, tooltipBg ]);
const renderRadioGroup = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'method'>}) => {
return (
<RadioGroup defaultValue="add" colorScheme="blue" isDisabled={ isDisabled } isFocusable={ !isDisabled } { ...field } >
<Stack spacing={ 4 }>
{ methods.map((method) => {
return <Radio key={ method } value={ method } size="lg">{ renderItem(method) }</Radio>;
}) }
</Stack>
</RadioGroup>
);
}, [ isDisabled, methods, renderItem ]);
return (
<section>
<Text variant="secondary" fontSize="sm" mb={ 5 }>Smart-contract verification method</Text>
<Controller
name="method"
control={ control }
render={ renderRadioGroup }
/>
</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'>}) => (
<CheckboxInput<FormFields, 'is_optimization_enabled'> <Flex flexShrink={ 0 }>
text="Optimization enabled" <CheckboxInput<FormFields, 'is_optimization_enabled'>
field={ field } text="Optimization enabled"
onChange={ handleCheckboxChange } field={ field }
isDisabled={ formState.isSubmitting } onChange={ handleCheckboxChange }
/> 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"
<InputPlaceholder text="Optimization runs"/> minW="100px"
</FormControl> maxW="200px"
flexShrink={ 1 }
isInvalid={ Boolean(error) }
/>
); );
}, [ 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;
<FileInput<FormFields, 'sources'> accept={ fileTypes.join(',') } multiple={ multiple } field={ field }> return (
{ () => ( <>
<Flex <FileInput<FormFields, 'sources'> accept={ fileTypes.join(',') } multiple={ multiple } field={ field }>
flexDir="column" { ({ onChange }) => (
alignItems="flex-start" <Flex
rowGap={ 2 } flexDir="column"
w="100%" alignItems="flex-start"
display={ field.value && field.value.length > 0 && !multiple ? 'none' : 'block' } rowGap={ 2 }
mb={ field.value && field.value.length > 0 ? 2 : 0 } w="100%"
>
<Button
variant="outline"
size="sm"
// mb={ 2 }
> >
Upload file{ multiple ? 's' : '' } <DragAndDropArea onDrop={ onChange } p={{ base: 3, lg: 6 }} isDisabled={ formState.isSubmitting }>
</Button> { hasValue ? renderFiles(field.value) : renderUploadButton() }
{ /* design is not ready */ } </DragAndDropArea>
{ /* <DragAndDropArea onDrop={ onChange }/> */ } </Flex>
</Flex> ) }
) } </FileInput>
</FileInput> { commonError?.message && <FieldError message={ commonError.type === 'required' ? 'Field is required' : commonError.message }/> }
{ field.value && field.value.length > 0 && renderFiles(field.value) } </>
{ commonError?.message && <FieldError message={ commonError.type === 'required' ? 'Field is required' : commonError.message }/> } );
</> }, [ fileTypes, multiple, commonError, formState.isSubmitting, renderFiles, renderUploadButton ]);
), [ fileTypes, commonError, multiple, renderFiles ]);
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,30 +124,34 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam ...@@ -114,30 +124,34 @@ 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>
<ContractVerificationFormRow > <Controller
<Text fontWeight={ 500 } className={ className } mt={ 4 }>{ title }</Text> name="sources"
</ContractVerificationFormRow> control={ control }
<ContractVerificationFormRow> render={ renderControl }
<Controller rules={ rules }
name="sources" />
control={ control } { hint ? <span>{ hint }</span> : null }
render={ renderControl } </ContractVerificationFormRow>
rules={ rules }
/>
{ hint ? <span>{ hint }</span> : null }
</ContractVerificationFormRow>
</>
); );
}; };
export default React.memo(chakra(ContractVerificationFieldSources)); export default React.memo(ContractVerificationFieldSources);
...@@ -12,9 +12,9 @@ import ContractVerificationFieldOptimization from '../fields/ContractVerificatio ...@@ -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>;
......
import type { FieldPath, ErrorOption } from 'react-hook-form'; import type { FieldPath, ErrorOption } from 'react-hook-form';
import type { ContractLibrary, FormFields } from './types'; import type {
ContractLibrary,
FormFields,
FormFieldsFlattenSourceCode,
FormFieldsMultiPartFile,
FormFieldsSourcify,
FormFieldsStandardInput,
FormFieldsVyperContract,
FormFieldsVyperMultiPartFile,
} from './types';
import type { SmartContractVerificationMethod, SmartContractVerificationError } from 'types/api/contract'; import type { SmartContractVerificationMethod, SmartContractVerificationError } from 'types/api/contract';
import type { Params as FetchParams } from 'lib/hooks/useFetch'; import type { Params as FetchParams } from 'lib/hooks/useFetch';
...@@ -14,6 +23,83 @@ export const SUPPORTED_VERIFICATION_METHODS: Array<SmartContractVerificationMeth ...@@ -14,6 +23,83 @@ export const SUPPORTED_VERIFICATION_METHODS: Array<SmartContractVerificationMeth
'vyper-multi-part', 'vyper-multi-part',
]; ];
export const METHOD_LABELS: Record<SmartContractVerificationMethod, string> = {
'flattened-code': 'Solidity (Flattened source code)',
'standard-input': 'Solidity (Standard JSON input)',
sourcify: 'Solidity (Sourcify)',
'multi-part': 'Solidity (Multi-part files)',
'vyper-code': 'Vyper (Contract)',
'vyper-multi-part': 'Vyper (Multi-part files)',
};
export const DEFAULT_VALUES = {
'flattened-code': {
method: {
value: 'flattened-code' as const,
label: METHOD_LABELS['flattened-code'],
},
is_yul: false,
name: '',
compiler: null,
evm_version: null,
is_optimization_enabled: true,
optimization_runs: '200',
code: '',
autodetect_constructor_args: true,
constructor_args: '',
libraries: [],
},
'standard-input': {
method: {
value: 'standard-input' as const,
label: METHOD_LABELS['standard-input'],
},
name: '',
compiler: null,
sources: [],
autodetect_constructor_args: true,
constructor_args: '',
},
sourcify: {
method: {
value: 'sourcify' as const,
label: METHOD_LABELS.sourcify,
},
sources: [],
},
'multi-part': {
method: {
value: 'multi-part' as const,
label: METHOD_LABELS['multi-part'],
},
compiler: null,
evm_version: null,
is_optimization_enabled: true,
optimization_runs: 200,
sources: [],
libraries: [],
},
'vyper-code': {
method: {
value: 'vyper-code' as const,
label: METHOD_LABELS['vyper-code'],
},
name: '',
compiler: null,
code: '',
constructor_args: '',
},
'vyper-multi-part': {
method: {
value: 'vyper-multi-part' as const,
label: METHOD_LABELS['vyper-multi-part'],
},
compiler: null,
evm_version: null,
sources: [],
},
};
export function isValidVerificationMethod(method?: string): method is SmartContractVerificationMethod { export function isValidVerificationMethod(method?: string): method is SmartContractVerificationMethod {
return method && SUPPORTED_VERIFICATION_METHODS.includes(method as SmartContractVerificationMethod) ? true : false; return method && SUPPORTED_VERIFICATION_METHODS.includes(method as SmartContractVerificationMethod) ? true : false;
} }
...@@ -34,68 +120,79 @@ export function sortVerificationMethods(methodA: SmartContractVerificationMethod ...@@ -34,68 +120,79 @@ export function sortVerificationMethods(methodA: SmartContractVerificationMethod
} }
export function prepareRequestBody(data: FormFields): FetchParams['body'] { export function prepareRequestBody(data: FormFields): FetchParams['body'] {
switch (data.method) { switch (data.method.value) {
case 'flattened-code': { case 'flattened-code': {
const _data = data as FormFieldsFlattenSourceCode;
return { return {
compiler_version: data.compiler?.value, compiler_version: _data.compiler?.value,
source_code: data.code, source_code: _data.code,
is_optimization_enabled: data.is_optimization_enabled, is_optimization_enabled: _data.is_optimization_enabled,
is_yul_contract: data.is_yul, is_yul_contract: _data.is_yul,
optimization_runs: data.optimization_runs, optimization_runs: _data.optimization_runs,
contract_name: data.name, contract_name: _data.name,
libraries: reduceLibrariesArray(data.libraries), libraries: reduceLibrariesArray(_data.libraries),
evm_version: data.evm_version?.value, evm_version: _data.evm_version?.value,
autodetect_constructor_args: data.autodetect_constructor_args, autodetect_constructor_args: _data.autodetect_constructor_args,
constructor_args: data.constructor_args, constructor_args: _data.constructor_args,
}; };
} }
case 'standard-input': { case 'standard-input': {
const _data = data as FormFieldsStandardInput;
const body = new FormData(); const body = new FormData();
body.set('compiler_version', data.compiler?.value); body.set('compiler_version', _data.compiler?.value);
body.set('contract_name', data.name); body.set('contract_name', _data.name);
body.set('autodetect_constructor_args', String(Boolean(data.autodetect_constructor_args))); body.set('autodetect_constructor_args', String(Boolean(_data.autodetect_constructor_args)));
body.set('constructor_args', data.constructor_args); body.set('constructor_args', _data.constructor_args);
addFilesToFormData(body, data.sources); addFilesToFormData(body, _data.sources);
return body; return body;
} }
case 'sourcify': { case 'sourcify': {
const _data = data as FormFieldsSourcify;
const body = new FormData(); const body = new FormData();
addFilesToFormData(body, data.sources); addFilesToFormData(body, _data.sources);
_data.contract_index && body.set('chosen_contract_index', _data.contract_index.value);
return body; return body;
} }
case 'multi-part': { case 'multi-part': {
const _data = data as FormFieldsMultiPartFile;
const body = new FormData(); const body = new FormData();
body.set('compiler_version', data.compiler?.value); body.set('compiler_version', _data.compiler?.value);
body.set('evm_version', data.evm_version?.value); body.set('evm_version', _data.evm_version?.value);
body.set('is_optimization_enabled', String(Boolean(data.is_optimization_enabled))); body.set('is_optimization_enabled', String(Boolean(_data.is_optimization_enabled)));
data.is_optimization_enabled && body.set('optimization_runs', data.optimization_runs); _data.is_optimization_enabled && body.set('optimization_runs', _data.optimization_runs);
const libraries = reduceLibrariesArray(data.libraries); const libraries = reduceLibrariesArray(_data.libraries);
libraries && body.set('libraries', JSON.stringify(libraries)); libraries && body.set('libraries', JSON.stringify(libraries));
addFilesToFormData(body, data.sources); addFilesToFormData(body, _data.sources);
return body; return body;
} }
case 'vyper-code': { case 'vyper-code': {
const _data = data as FormFieldsVyperContract;
return { return {
compiler_version: data.compiler?.value, compiler_version: _data.compiler?.value,
source_code: data.code, source_code: _data.code,
contract_name: data.name, contract_name: _data.name,
constructor_args: data.constructor_args, constructor_args: _data.constructor_args,
}; };
} }
case 'vyper-multi-part': { case 'vyper-multi-part': {
const _data = data as FormFieldsVyperMultiPartFile;
const body = new FormData(); const body = new FormData();
body.set('compiler_version', data.compiler?.value); body.set('compiler_version', _data.compiler?.value);
body.set('evm_version', data.evm_version?.value); body.set('evm_version', _data.evm_version?.value);
addFilesToFormData(body, data.sources); addFilesToFormData(body, _data.sources);
return body; return body;
} }
......
...@@ -20,7 +20,6 @@ import AddressLogs from 'ui/address/AddressLogs'; ...@@ -20,7 +20,6 @@ import AddressLogs from 'ui/address/AddressLogs';
import AddressTokens from 'ui/address/AddressTokens'; import AddressTokens from 'ui/address/AddressTokens';
import AddressTokenTransfers from 'ui/address/AddressTokenTransfers'; import AddressTokenTransfers from 'ui/address/AddressTokenTransfers';
import AddressTxs from 'ui/address/AddressTxs'; import AddressTxs from 'ui/address/AddressTxs';
import AdBanner from 'ui/shared/ad/AdBanner';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
...@@ -51,10 +50,15 @@ const AddressPageContent = () => { ...@@ -51,10 +50,15 @@ const AddressPageContent = () => {
}); });
const tags = [ const tags = [
addressQuery.data?.is_contract ? { label: 'contract', display_name: 'Contract' } : { label: 'eoa', display_name: 'EOA' },
addressQuery.data?.implementation_address ? { label: 'proxy', display_name: 'Proxy' } : undefined,
addressQuery.data?.token ? { label: 'token', display_name: 'Token' } : undefined,
...(addressQuery.data?.private_tags || []), ...(addressQuery.data?.private_tags || []),
...(addressQuery.data?.public_tags || []), ...(addressQuery.data?.public_tags || []),
...(addressQuery.data?.watchlist_names || []), ...(addressQuery.data?.watchlist_names || []),
].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>); ]
.filter(notEmpty)
.map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>);
const contractTabs = useContractTabs(addressQuery.data); const contractTabs = useContractTabs(addressQuery.data);
...@@ -97,7 +101,7 @@ const AddressPageContent = () => { ...@@ -97,7 +101,7 @@ const AddressPageContent = () => {
return ( return (
<Page> <Page>
<TextAd mb={ 6 }/> { addressQuery.isLoading ? <Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/> : <TextAd mb={ 6 }/> }
{ addressQuery.isLoading ? ( { addressQuery.isLoading ? (
<Skeleton h={ 10 } w="260px" mb={ 6 }/> <Skeleton h={ 10 } w="260px" mb={ 6 }/>
) : ( ) : (
...@@ -109,7 +113,6 @@ const AddressPageContent = () => { ...@@ -109,7 +113,6 @@ const AddressPageContent = () => {
/> />
) } ) }
<AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/> <AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/>
<AdBanner mt={{ base: 6, lg: 8 }} justifyContent="center"/>
{ /* should stay before tabs to scroll up whith pagination */ } { /* should stay before tabs to scroll up whith pagination */ }
<Box ref={ tabsScrollRef }></Box> <Box ref={ tabsScrollRef }></Box>
{ addressQuery.isLoading ? <SkeletonTabs/> : content } { addressQuery.isLoading ? <SkeletonTabs/> : content }
......
import { Skeleton, Box, Flex, SkeletonCircle, Icon } from '@chakra-ui/react'; import { Skeleton, Box, Flex, SkeletonCircle, Icon, Tag } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
...@@ -13,7 +13,6 @@ import useQueryWithPages from 'lib/hooks/useQueryWithPages'; ...@@ -13,7 +13,6 @@ import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import notEmpty from 'lib/notEmpty'; import notEmpty from 'lib/notEmpty';
import trimTokenSymbol from 'lib/token/trimTokenSymbol'; import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import AddressContract from 'ui/address/AddressContract'; import AddressContract from 'ui/address/AddressContract';
import AdBanner from 'ui/shared/ad/AdBanner';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
...@@ -157,10 +156,13 @@ const TokenPageContent = () => { ...@@ -157,10 +156,13 @@ const TokenPageContent = () => {
return ( return (
<Page> <Page>
{ tokenQuery.isLoading ? ( { tokenQuery.isLoading ? (
<Flex alignItems="center" mb={ 6 }> <>
<SkeletonCircle w={ 6 } h={ 6 } mr={ 3 }/> <Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/>
<Skeleton w="500px" h={ 10 }/> <Flex alignItems="center" mb={ 6 }>
</Flex> <SkeletonCircle w={ 6 } h={ 6 } mr={ 3 }/>
<Skeleton w="500px" h={ 10 }/>
</Flex>
</>
) : ( ) : (
<> <>
<TextAd mb={ 6 }/> <TextAd mb={ 6 }/>
...@@ -171,12 +173,12 @@ const TokenPageContent = () => { ...@@ -171,12 +173,12 @@ const TokenPageContent = () => {
additionalsLeft={ ( additionalsLeft={ (
<TokenLogo hash={ tokenQuery.data?.address } name={ tokenQuery.data?.name } boxSize={ 6 }/> <TokenLogo hash={ tokenQuery.data?.address } name={ tokenQuery.data?.name } boxSize={ 6 }/>
) } ) }
additionalsRight={ <Tag>{ tokenQuery.data?.type }</Tag> }
/> />
</> </>
) } ) }
<TokenContractInfo tokenQuery={ tokenQuery }/> <TokenContractInfo tokenQuery={ tokenQuery }/>
<TokenDetails tokenQuery={ tokenQuery }/> <TokenDetails tokenQuery={ tokenQuery }/>
<AdBanner mt={{ base: 6, lg: 8 }} justifyContent="center"/>
{ /* should stay before tabs to scroll up whith pagination */ } { /* should stay before tabs to scroll up whith pagination */ }
<Box ref={ scrollRef }></Box> <Box ref={ scrollRef }></Box>
......
import { GridItem } from '@chakra-ui/react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import AdBanner from 'ui/shared/ad/AdBanner';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
const DetailsSponsoredItem = () => {
const isMobile = useIsMobile();
if (isMobile) {
return (
<GridItem mt={ 5 }>
<AdBanner justifyContent="center"/>
</GridItem>
);
}
return (
<DetailsInfoItem
title="Sponsored"
hint="Sponsored banner advertisement"
>
<AdBanner/>
</DetailsInfoItem>
);
};
export default React.memo(DetailsSponsoredItem);
...@@ -26,6 +26,8 @@ type AdData = { ...@@ -26,6 +26,8 @@ type AdData = {
const CoinzillaTextAd = ({ className }: {className?: string}) => { const CoinzillaTextAd = ({ className }: {className?: string}) => {
const [ adData, setAdData ] = React.useState<AdData | null>(null); const [ adData, setAdData ] = React.useState<AdData | null>(null);
const [ isLoading, setIsLoading ] = React.useState(true);
useEffect(() => { useEffect(() => {
fetch('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242') fetch('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242')
.then(res => res.status === 200 ? res.json() : null) .then(res => res.status === 200 ? res.json() : null)
...@@ -34,9 +36,16 @@ const CoinzillaTextAd = ({ className }: {className?: string}) => { ...@@ -34,9 +36,16 @@ const CoinzillaTextAd = ({ className }: {className?: string}) => {
if (data?.ad?.impressionUrl) { if (data?.ad?.impressionUrl) {
fetch(data.ad.impressionUrl); fetch(data.ad.impressionUrl);
} }
})
.finally(() => {
setIsLoading(false);
}); });
}, []); }, []);
if (isLoading) {
return <Box className={ className } h={{ base: 12, lg: 6 }}/>;
}
if (!adData) { if (!adData) {
return null; return null;
} }
...@@ -52,7 +61,7 @@ const CoinzillaTextAd = ({ className }: {className?: string}) => { ...@@ -52,7 +61,7 @@ const CoinzillaTextAd = ({ className }: {className?: string}) => {
mr={ 3 } mr={ 3 }
display={{ base: 'none', lg: 'inline' }} display={{ base: 'none', lg: 'inline' }}
> >
ADs: Ads:
</Text> </Text>
{ urlObject.hostname === 'nifty.ink' ? { urlObject.hostname === 'nifty.ink' ?
<Text as="span" mr={ 1 }>🎨</Text> : <Text as="span" mr={ 1 }>🎨</Text> :
......
import { Box } from '@chakra-ui/react'; import { chakra, Center, useColorModeValue } from '@chakra-ui/react';
import type { DragEvent } from 'react'; import type { DragEvent } from 'react';
import React from 'react'; import React from 'react';
import { getAllFileEntries, convertFileEntryToFile } from './utils/files'; import { getAllFileEntries, convertFileEntryToFile } from './utils/files';
interface Props { interface Props {
children: React.ReactNode;
onDrop: (files: Array<File>) => void; onDrop: (files: Array<File>) => void;
className?: string;
isDisabled?: boolean;
} }
const DragAndDropArea = ({ onDrop }: Props) => { const DragAndDropArea = ({ onDrop, children, className, isDisabled }: Props) => {
const [ isDragOver, setIsDragOver ] = React.useState(false); const [ isDragOver, setIsDragOver ] = React.useState(false);
const handleDrop = React.useCallback(async(event: DragEvent<HTMLDivElement>) => { const handleDrop = React.useCallback(async(event: DragEvent<HTMLDivElement>) => {
event.preventDefault(); event.preventDefault();
if (isDisabled) {
return;
}
const fileEntries = await getAllFileEntries(event.dataTransfer.items); const fileEntries = await getAllFileEntries(event.dataTransfer.items);
const files = await Promise.all(fileEntries.map(convertFileEntryToFile)); const files = await Promise.all(fileEntries.map(convertFileEntryToFile));
onDrop(files); onDrop(files);
setIsDragOver(false); setIsDragOver(false);
}, [ onDrop ]); }, [ isDisabled, onDrop ]);
const handleDragOver = React.useCallback((event: DragEvent<HTMLDivElement>) => { const handleDragOver = React.useCallback((event: DragEvent<HTMLDivElement>) => {
event.preventDefault(); event.preventDefault();
...@@ -35,18 +42,39 @@ const DragAndDropArea = ({ onDrop }: Props) => { ...@@ -35,18 +42,39 @@ const DragAndDropArea = ({ onDrop }: Props) => {
setIsDragOver(false); setIsDragOver(false);
}, []); }, []);
const handleClick = React.useCallback((event: React.MouseEvent) => {
if (isDisabled) {
event.preventDefault();
event.stopPropagation();
}
}, [ isDisabled ]);
const disabledBorderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const borderColor = isDragOver ? 'link_hovered' : 'link';
return ( return (
<Box <Center
className={ className }
w="100%" w="100%"
h="200px" minH="120px"
bgColor="lightpink" borderWidth="2px"
opacity={ isDragOver ? 0.8 : 1 } borderColor={ isDisabled ? disabledBorderColor : borderColor }
_hover={{
borderColor: isDisabled ? disabledBorderColor : 'link_hovered',
}}
borderRadius="base"
borderStyle="dashed"
cursor="pointer"
textAlign="center"
onClick={ handleClick }
onDrop={ handleDrop } onDrop={ handleDrop }
onDragOver={ handleDragOver } onDragOver={ handleDragOver }
onDragEnter={ handleDragEnter } onDragEnter={ handleDragEnter }
onDragLeave={ handleDragLeave } onDragLeave={ handleDragLeave }
/> >
{ children }
</Center>
); );
}; };
export default React.memo(DragAndDropArea); export default React.memo(chakra(DragAndDropArea));
...@@ -7,7 +7,7 @@ interface Props { ...@@ -7,7 +7,7 @@ interface Props {
} }
const FieldError = ({ message, className }: Props) => { const FieldError = ({ message, className }: Props) => {
return <Box className={ className } color="error" fontSize="sm" mt={ 2 } wordBreak="break-all">{ message }</Box>; return <Box className={ className } color="error" fontSize="sm" mt={ 2 } wordBreak="break-word">{ message }</Box>;
}; };
export default chakra(FieldError); export default chakra(FieldError);
import { Box, Flex, Icon, Text, useColorModeValue, IconButton, chakra } from '@chakra-ui/react'; import { Box, Flex, Icon, Text, useColorModeValue, IconButton, chakra, Tooltip } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import CrossIcon from 'icons/cross.svg'; import CrossIcon from 'icons/cross.svg';
import imageIcon from 'icons/files/image.svg';
import jsonIcon from 'icons/files/json.svg'; import jsonIcon from 'icons/files/json.svg';
import placeholderIcon from 'icons/files/placeholder.svg';
import solIcon from 'icons/files/sol.svg'; import solIcon from 'icons/files/sol.svg';
import yulIcon from 'icons/files/yul.svg'; import yulIcon from 'icons/files/yul.svg';
import infoIcon from 'icons/info.svg';
import { shortenNumberWithLetter } from 'lib/formatters'; import { shortenNumberWithLetter } from 'lib/formatters';
const FILE_ICONS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGElement>>> = { const FILE_ICONS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGElement>>> = {
...@@ -29,42 +30,79 @@ interface Props { ...@@ -29,42 +30,79 @@ interface Props {
index?: number; index?: number;
onRemove?: (index?: number) => void; onRemove?: (index?: number) => void;
isDisabled?: boolean; isDisabled?: boolean;
error?: string;
} }
const FileSnippet = ({ file, className, index, onRemove, isDisabled }: Props) => { const FileSnippet = ({ file, className, index, onRemove, isDisabled, error }: Props) => {
const handleRemove = React.useCallback(() => { const handleRemove = React.useCallback((event: React.MouseEvent) => {
event.stopPropagation();
onRemove?.(index); onRemove?.(index);
}, [ index, onRemove ]); }, [ index, onRemove ]);
const handleErrorHintIconClick = React.useCallback((event: React.MouseEvent) => {
event.stopPropagation();
}, []);
const fileExtension = getFileExtension(file.name); const fileExtension = getFileExtension(file.name);
const fileIcon = FILE_ICONS[fileExtension] || imageIcon; const fileIcon = FILE_ICONS[fileExtension] || placeholderIcon;
const iconColor = useColorModeValue('gray.600', 'gray.400');
return ( return (
<Flex <Flex
p={ 3 }
borderWidth="2px"
borderRadius="md"
borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') }
maxW="300px" maxW="300px"
overflow="hidden" overflow="hidden"
className={ className } className={ className }
alignItems="center"
textAlign="left"
> >
<Icon as={ fileIcon } boxSize="50px" color={ useColorModeValue('gray.600', 'gray.400') } mr={ 2 }/> <Icon
<Box width="calc(100% - 58px - 24px)" > as={ fileIcon }
<Text fontWeight={ 600 } overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">{ file.name }</Text> boxSize="74px"
color={ error ? 'error' : iconColor }
mr={ 2 }
borderWidth="2px"
borderRadius="md"
borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') }
p={ 3 }
/>
<Box maxW="calc(100% - 58px - 24px)">
<Flex alignItems="center">
<Text
fontWeight={ 600 }
overflow="hidden"
textOverflow="ellipsis"
whiteSpace="nowrap"
color={ error ? 'error' : 'initial' }
>
{ file.name }
</Text>
{ Boolean(error) && (
<Tooltip
label={ error }
placement="top"
maxW="320px"
>
<Box cursor="pointer" display="inherit" onClick={ handleErrorHintIconClick } ml={ 1 }>
<Icon as={ infoIcon } boxSize={ 5 } color="error"/>
</Box>
</Tooltip>
) }
<IconButton
aria-label="remove"
icon={ <CrossIcon/> }
boxSize={ 6 }
variant="simple"
display="inline-block"
flexShrink={ 0 }
ml="auto"
onClick={ handleRemove }
isDisabled={ isDisabled }
alignSelf="flex-start"
/>
</Flex>
<Text variant="secondary" mt={ 1 }>{ shortenNumberWithLetter(file.size) }B</Text> <Text variant="secondary" mt={ 1 }>{ shortenNumberWithLetter(file.size) }B</Text>
</Box> </Box>
<IconButton
aria-label="remove"
icon={ <CrossIcon/> }
boxSize={ 6 }
variant="simple"
display="inline-block"
flexShrink={ 0 }
ml="auto"
onClick={ handleRemove }
isDisabled={ isDisabled }
/>
</Flex> </Flex>
); );
}; };
......
...@@ -8,11 +8,20 @@ import Burger from './Burger'; ...@@ -8,11 +8,20 @@ import Burger from './Burger';
test.use({ viewport: devices['iPhone 13 Pro'].viewport }); test.use({ viewport: devices['iPhone 13 Pro'].viewport });
const hooksConfig = {
router: {
route: '/blocks',
query: { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' },
pathname: '/blocks',
},
};
test('base view', async({ mount, page }) => { test('base view', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<Burger/> <Burger/>
</TestApp>, </TestApp>,
{ hooksConfig },
); );
await component.locator('svg[aria-label="Menu button"]').click(); await component.locator('svg[aria-label="Menu button"]').click();
...@@ -30,6 +39,7 @@ test.describe('dark mode', () => { ...@@ -30,6 +39,7 @@ test.describe('dark mode', () => {
<TestApp> <TestApp>
<Burger/> <Burger/>
</TestApp>, </TestApp>,
{ hooksConfig },
); );
await component.locator('svg[aria-label="Menu button"]').click(); await component.locator('svg[aria-label="Menu button"]').click();
...@@ -53,9 +63,23 @@ test.describe('auth', () => { ...@@ -53,9 +63,23 @@ test.describe('auth', () => {
<TestApp> <TestApp>
<Burger/> <Burger/>
</TestApp>, </TestApp>,
{ hooksConfig },
);
await component.locator('svg[aria-label="Menu button"]').click();
await expect(page).toHaveScreenshot();
});
extendedTest('submenu', async({ mount, page }) => {
const component = await mount(
<TestApp>
<Burger/>
</TestApp>,
{ hooksConfig },
); );
await component.locator('svg[aria-label="Menu button"]').click(); await component.locator('svg[aria-label="Menu button"]').click();
await page.locator('div[aria-label="Blockchain link group"]').click();
await expect(page).toHaveScreenshot(); await expect(page).toHaveScreenshot();
}); });
}); });
...@@ -4,9 +4,9 @@ import { route } from 'nextjs-routes'; ...@@ -4,9 +4,9 @@ import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { NavItem } from 'lib/hooks/useNavItems'; import type { NavItem } from 'lib/hooks/useNavItems';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import useColors from './useColors'; import useColors from './useColors';
import useNavLinkStyleProps from './useNavLinkStyleProps';
type Props = NavItem & { type Props = NavItem & {
isCollapsed?: boolean; isCollapsed?: boolean;
...@@ -15,23 +15,19 @@ type Props = NavItem & { ...@@ -15,23 +15,19 @@ type Props = NavItem & {
const NavLink = ({ text, nextRoute, icon, isCollapsed, isActive, px, isNewUi }: Props) => { const NavLink = ({ text, nextRoute, icon, isCollapsed, isActive, px, isNewUi }: Props) => {
const colors = useColors(); const colors = useColors();
const isExpanded = isCollapsed === false; const isExpanded = isCollapsed === false;
const styleProps = useNavLinkStyleProps({ isCollapsed, isExpanded, isActive });
const content = ( const content = (
<Link <Link
{ ...(isNewUi ? {} : { href: route(nextRoute) }) } { ...(isNewUi ? {} : { href: route(nextRoute) }) }
w={{ base: '100%', lg: isExpanded ? '180px' : '60px', xl: isCollapsed ? '60px' : '180px' }} { ...styleProps.itemProps }
px={ px || { base: 3, lg: isExpanded ? 3 : '15px', xl: isCollapsed ? '15px' : 3 } } w={{ base: '100%', lg: isExpanded ? '100%' : '60px', xl: isCollapsed ? '60px' : '100%' }}
py={ 2.5 }
display="flex" display="flex"
color={ isActive ? colors.text.active : colors.text.default } px={ px || { base: 3, lg: isExpanded ? 3 : '15px', xl: isCollapsed ? '15px' : 3 } }
bgColor={ isActive ? colors.bg.active : colors.bg.default }
_hover={{ color: isActive ? colors.text.active : colors.text.hover }}
borderRadius="base"
whiteSpace="nowrap"
aria-label={ `${ text } link` } aria-label={ `${ text } link` }
{ ...getDefaultTransitionProps({ transitionProperty: 'width, padding' }) } whiteSpace="nowrap"
> >
<Tooltip <Tooltip
label={ text } label={ text }
...@@ -44,15 +40,7 @@ const NavLink = ({ text, nextRoute, icon, isCollapsed, isActive, px, isNewUi }: ...@@ -44,15 +40,7 @@ const NavLink = ({ text, nextRoute, icon, isCollapsed, isActive, px, isNewUi }:
> >
<HStack spacing={ 3 } overflow="hidden"> <HStack spacing={ 3 } overflow="hidden">
<Icon as={ icon } boxSize="30px"/> <Icon as={ icon } boxSize="30px"/>
<Text <Text { ...styleProps.textProps }>
variant="inherit"
fontSize="sm"
lineHeight="20px"
opacity={{ base: '1', lg: isExpanded ? '1' : '0', xl: isCollapsed ? '0' : '1' }}
transitionProperty="opacity"
transitionDuration="normal"
transitionTimingFunction="ease"
>
{ text } { text }
</Text> </Text>
</HStack> </HStack>
......
import {
Icon,
Text,
HStack,
Flex,
Box,
Link,
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
VStack,
} from '@chakra-ui/react';
import React from 'react';
import chevronIcon from 'icons/arrows/east-mini.svg';
import type { NavGroupItem } from 'lib/hooks/useNavItems';
import NavLink from './NavLink';
import useNavLinkStyleProps from './useNavLinkStyleProps';
type Props = NavGroupItem & {
isCollapsed?: boolean;
}
const NavLinkGroupDesktop = ({ text, subItems, icon, isCollapsed, isActive }: Props) => {
const isExpanded = isCollapsed === false;
const styleProps = useNavLinkStyleProps({ isCollapsed, isExpanded, isActive });
return (
<Box as="li" listStyleType="none" w="100%">
<Popover
trigger="hover"
placement="right-start"
isLazy
>
<PopoverTrigger>
<Link
{ ...styleProps.itemProps }
w={{ lg: isExpanded ? '180px' : '60px', xl: isCollapsed ? '60px' : '180px' }}
pl={{ lg: isExpanded ? 3 : '15px', xl: isCollapsed ? '15px' : 3 }}
pr={{ lg: isExpanded ? 0 : '15px', xl: isCollapsed ? '15px' : 0 }}
aria-label={ `${ text } link group` }
display="grid"
gridColumnGap={ 3 }
gridTemplateColumns="auto, 30px"
overflow="hidden"
>
<Flex justifyContent="space-between" width="100%" alignItems="center" pr={ 1 }>
<HStack spacing={ 3 } overflow="hidden">
<Icon as={ icon } boxSize="30px"/>
<Text
{ ...styleProps.textProps }
>
{ text }
</Text>
</HStack>
<Icon
as={ chevronIcon }
transform="rotate(180deg)"
boxSize={ 6 }
opacity={{ lg: isExpanded ? '1' : '0', xl: isCollapsed ? '0' : '1' }}
transitionProperty="opacity"
transitionDuration="normal"
transitionTimingFunction="ease"
/>
</Flex>
</Link>
</PopoverTrigger>
<PopoverContent width="auto" top={{ lg: isExpanded ? '-16px' : 0, xl: isCollapsed ? 0 : '-16px' }}>
<PopoverBody p={ 4 }>
<Text variant="secondary" fontSize="sm" mb={ 2 } display={{ lg: isExpanded ? 'none' : 'block', xl: isCollapsed ? 'block' : 'none' }}>
{ text }
</Text>
<VStack spacing={ 1 } alignItems="start">
{ subItems.map(item => <NavLink key={ item.text } { ...item } isCollapsed={ false }/>) }
</VStack>
</PopoverBody>
</PopoverContent>
</Popover>
</Box>
);
};
export default NavLinkGroupDesktop;
import {
Icon,
Text,
HStack,
Flex,
Box,
} from '@chakra-ui/react';
import React from 'react';
import chevronIcon from 'icons/arrows/east-mini.svg';
import type { NavGroupItem } from 'lib/hooks/useNavItems';
import useNavLinkStyleProps from './useNavLinkStyleProps';
type Props = NavGroupItem & {
isCollapsed?: boolean;
onClick: () => void;
}
const NavLinkGroup = ({ text, icon, isActive, onClick }: Props) => {
const styleProps = useNavLinkStyleProps({ isActive });
return (
<Box as="li" listStyleType="none" w="100%" onClick={ onClick }>
<Box
{ ...styleProps.itemProps }
w="100%"
px={ 3 }
aria-label={ `${ text } link group` }
>
<Flex justifyContent="space-between" width="100%" alignItems="center" pr={ 1 }>
<HStack spacing={ 3 } overflow="hidden">
<Icon as={ icon } boxSize="30px"/>
<Text
{ ...styleProps.textProps }
>
{ text }
</Text>
</HStack>
<Icon as={ chevronIcon } transform="rotate(180deg)" boxSize={ 6 }/>
</Flex>
</Box>
</Box>
);
};
export default NavLinkGroup;
...@@ -70,6 +70,21 @@ test('with tooltips +@desktop-xl -@default', async({ mount, page }) => { ...@@ -70,6 +70,21 @@ test('with tooltips +@desktop-xl -@default', async({ mount, page }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('with submenu +@desktop-xl +@dark-mode', async({ mount, page }) => {
const component = await mount(
<TestApp>
<Flex w="100%" minH="100vh" alignItems="stretch">
<NavigationDesktop/>
<Box bgColor="lightpink" w="100%"/>
</Flex>
</TestApp>,
{ hooksConfig },
);
await page.locator('a[aria-label="Blockchain link group"]').hover();
await expect(component).toHaveScreenshot();
});
test.describe('cookie set to false', () => { test.describe('cookie set to false', () => {
const extendedTest = test.extend({ const extendedTest = test.extend({
context: ({ context }, use) => { context: ({ context }, use) => {
......
...@@ -5,13 +5,14 @@ import appConfig from 'configs/app/config'; ...@@ -5,13 +5,14 @@ import appConfig from 'configs/app/config';
import chevronIcon from 'icons/arrows/east-mini.svg'; import chevronIcon from 'icons/arrows/east-mini.svg';
import { useAppContext } from 'lib/appContext'; import { useAppContext } from 'lib/appContext';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useNavItems from 'lib/hooks/useNavItems'; import useNavItems, { isGroupItem } from 'lib/hooks/useNavItems';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo'; import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import NetworkMenu from 'ui/snippets/networkMenu/NetworkMenu'; import NetworkMenu from 'ui/snippets/networkMenu/NetworkMenu';
import NavFooter from './NavFooter'; import NavFooter from './NavFooter';
import NavLink from './NavLink'; import NavLink from './NavLink';
import NavLinkGroupDesktop from './NavLinkGroupDesktop';
const NavigationDesktop = () => { const NavigationDesktop = () => {
const appProps = useAppContext(); const appProps = useAppContext();
...@@ -51,7 +52,7 @@ const NavigationDesktop = () => { ...@@ -51,7 +52,7 @@ const NavigationDesktop = () => {
display={{ base: 'none', lg: 'flex' }} display={{ base: 'none', lg: 'flex' }}
position="relative" position="relative"
flexDirection="column" flexDirection="column"
alignItems="flex-start" alignItems="stretch"
borderRight="1px solid" borderRight="1px solid"
borderColor="divider" borderColor="divider"
px={{ lg: isExpanded ? 6 : 4, xl: isCollapsed ? 4 : 6 }} px={{ lg: isExpanded ? 6 : 4, xl: isCollapsed ? 4 : 6 }}
...@@ -66,7 +67,8 @@ const NavigationDesktop = () => { ...@@ -66,7 +67,8 @@ const NavigationDesktop = () => {
alignItems="center" alignItems="center"
flexDirection="row" flexDirection="row"
w="100%" w="100%"
px={{ lg: isExpanded ? 3 : '15px', xl: isCollapsed ? '15px' : 3 }} pl={{ lg: isExpanded ? 3 : '15px', xl: isCollapsed ? '15px' : 3 }}
pr={{ lg: isExpanded ? 0 : '15px', xl: isCollapsed ? '15px' : 0 }}
h={ 10 } h={ 10 }
transitionProperty="padding" transitionProperty="padding"
transitionDuration="normal" transitionDuration="normal"
...@@ -77,7 +79,13 @@ const NavigationDesktop = () => { ...@@ -77,7 +79,13 @@ const NavigationDesktop = () => {
</Box> </Box>
<Box as="nav" mt={ 8 }> <Box as="nav" mt={ 8 }>
<VStack as="ul" spacing="1" alignItems="flex-start"> <VStack as="ul" spacing="1" alignItems="flex-start">
{ mainNavItems.map((item) => <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>) } { mainNavItems.map((item) => {
if (isGroupItem(item)) {
return <NavLinkGroupDesktop key={ item.text } { ...item } isCollapsed={ isCollapsed }/>;
} else {
return <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>;
}
}) }
</VStack> </VStack>
</Box> </Box>
{ hasAccount && ( { hasAccount && (
......
import { Box, VStack } from '@chakra-ui/react'; import { Box, Flex, Text, Icon, VStack, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import { animate, motion, useMotionValue } from 'framer-motion';
import React, { useCallback } from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import chevronIcon from 'icons/arrows/east-mini.svg';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useNavItems from 'lib/hooks/useNavItems'; import useNavItems, { isGroupItem } from 'lib/hooks/useNavItems';
import NavFooter from 'ui/snippets/navigation/NavFooter'; import NavFooter from 'ui/snippets/navigation/NavFooter';
import NavLink from 'ui/snippets/navigation/NavLink'; import NavLink from 'ui/snippets/navigation/NavLink';
import NavLinkGroupMobile from './NavLinkGroupMobile';
const NavigationMobile = () => { const NavigationMobile = () => {
const { mainNavItems, accountNavItems } = useNavItems(); const { mainNavItems, accountNavItems } = useNavItems();
const [ openedGroupIndex, setOpenedGroupIndex ] = React.useState(-1);
const mainX = useMotionValue(0);
const subX = useMotionValue(250);
const onGroupItemOpen = (index: number) => () => {
setOpenedGroupIndex(index);
animate(mainX, -250, { ease: 'easeInOut' });
animate(subX, 0, { ease: 'easeInOut' });
};
const onGroupItemClose = useCallback(() => {
animate(mainX, 0, { ease: 'easeInOut' });
animate(subX, 250, { ease: 'easeInOut', onComplete: () => setOpenedGroupIndex(-1) });
}, [ mainX, subX ]);
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN)); const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
const hasAccount = appConfig.isAccountSupported && isAuth; const hasAccount = appConfig.isAccountSupported && isAuth;
const iconColor = useColorModeValue('blue.600', 'blue.300');
const openedItem = mainNavItems[openedGroupIndex];
return ( return (
<> <>
<Box as="nav" mt={ 6 }> <Box position="relative">
<VStack as="ul" spacing="1" alignItems="flex-start"> <Box
{ mainNavItems.map((item) => <NavLink key={ item.text } { ...item }/>) } as={ motion.nav }
</VStack> mt={ 6 }
</Box> style={{ x: mainX }}
{ isAuth && ( >
<Box as="nav" mt={ 6 }> <VStack
<VStack as="ul" spacing="1" alignItems="flex-start"> w="100%"
{ accountNavItems.map((item) => <NavLink key={ item.text } { ...item }/>) } as="ul"
spacing="1"
alignItems="flex-start"
>
{ mainNavItems.map((item, index) => {
if (isGroupItem(item)) {
return <NavLinkGroupMobile key={ item.text } { ...item } onClick={ onGroupItemOpen(index) }/>;
} else {
return <NavLink key={ item.text } { ...item }/>;
}
}) }
</VStack> </VStack>
</Box> </Box>
) } { isAuth && (
<Box
as={ motion.nav }
mt={ 6 }
style={{ x: mainX }}
>
<VStack as="ul" spacing="1" alignItems="flex-start">
{ accountNavItems.map((item) => <NavLink key={ item.text } { ...item }/>) }
</VStack>
</Box>
) }
{ openedGroupIndex >= 0 && (
<Box
as={ motion.nav }
w="100%"
mt={ 6 }
position="absolute"
top={ 0 }
style={{ x: subX }}
key="sub"
>
<VStack
w="100%"
as="ul"
spacing="1"
alignItems="flex-start"
>
<Flex alignItems="center" px={ 3 } py={ 2.5 } w="100%" h="50px" onClick={ onGroupItemClose }>
<Icon as={ chevronIcon } boxSize={ 6 } mr={ 2 } color={ iconColor }/>
<Text variant="secondary" fontSize="sm">{ mainNavItems[openedGroupIndex].text }</Text>
</Flex>
{ isGroupItem(openedItem) && openedItem.subItems?.map((item) => <NavLink key={ item.text } { ...item }/>) }
</VStack>
</Box>
) }
</Box>
<NavFooter hasAccount={ hasAccount }/> <NavFooter hasAccount={ hasAccount }/>
</> </>
); );
......
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