Commit c441f01a authored by tom's avatar tom

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

parents 6d1b99ec fcdf0f2d
NEXT_PUBLIC_SENTRY_DSN=xxx
SENTRY_CSP_REPORT_URI=xxx
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
\ No newline at end of file
......@@ -48,9 +48,14 @@ NEXT_PUBLIC_API_PROTOCOL=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_PROTOCOL__
NEXT_PUBLIC_API_PORT=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_PORT__
NEXT_PUBLIC_STATS_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_STATS_API_HOST__
NEXT_PUBLIC_VISUALIZE_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_VISUALIZE_API_HOST__
NEXT_PUBLIC_API_SPEC_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_SPEC_URL__
# external services config
NEXT_PUBLIC_SENTRY_DSN=__PLACEHOLDER_FOR_NEXT_PUBLIC_SENTRY_DSN__
NEXT_PUBLIC_AUTH0_CLIENT_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_AUTH0_CLIENT_ID__
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID__
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=__PLACEHOLDER_FOR_NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY__
# l2 config
NEXT_PUBLIC_IS_L2_NETWORK=__PLACEHOLDER_FOR_NEXT_PUBLIC_IS_L2_NETWORKL__
NEXT_PUBLIC_L1_BASE_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_L1_BASE_URL__
......@@ -251,7 +251,7 @@
"presentation": {
"reveal": "always",
"panel": "shared",
"close": true,
"close": false,
"revealProblems": "onProblem",
"focus": true,
},
......
......@@ -46,7 +46,7 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_NETWORK_SHORT_NAME | `string` *(optional)* | Used for SEO attributes (page title and description) | `OoG` |
| NEXT_PUBLIC_NETWORK_TYPE | `string` *(optional)* | Network type (used for matching pre-defined assets, e.g network logo and icon, which are stored in the project). See all possible values here | `xdai_mainnet` |
| NEXT_PUBLIC_NETWORK_ID | `number` | Chain id, see [https://chainlist.org/](https://chainlist.org/) for the reference | `99` |
| NEXT_PUBLIC_NETWORK_RPC_URL | `string` | Chain server RPC url, see [https://chainlist.org/](https://chainlist.org/) for the reference | `https://core.poa.network` |
| NEXT_PUBLIC_NETWORK_RPC_URL | `string` *(optional)* | Chain server RPC url, see [https://chainlist.org/](https://chainlist.org/) for the reference. If not provided, some functionality of the explorer, related to smart contracts interaction and third-party apps integration, will be unavailable | `https://core.poa.network` |
| NEXT_PUBLIC_NETWORK_CURRENCY_NAME | `string` | Network currency name | `Ether` |
| NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | Network currency symbol | `ETH` |
| NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS | `string` | Network currency decimals | `18` |
......@@ -79,6 +79,7 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` *(optional)* | Set to false if average block time is useless for the network | `true` |
| NEXT_PUBLIC_DOMAIN_WITH_AD | `string` *(optional)* | The domain on which we display ads | `blockscout.com` |
| NEXT_PUBLIC_AD_ADBUTLER_ON | `boolean` *(optional)* | Set to true to show Adbutler banner instead of Coinzilla banner | `false` |
| NEXT_PUBLIC_API_SPEC_URL | `string` *(optional)* | Spec to be displayed on api-docs page | `https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml` |
### App configuration
| Variable | Type | Description | Default value
......@@ -129,6 +130,12 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID | `string` | Project id for [WalletConnect](https://docs.walletconnect.com/2.0/web3modal/react/installation#obtain-project-id) integration | `<secret>` |
| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | Site key for [reCAPTCHA](https://developers.google.com/recaptcha) service | `<secret>` |
### L2 configuration
| Variable | Type | Description | Default value
| --- | --- | --- | --- |
| NEXT_PUBLIC_IS_L2_NETWORK | `boolean` *(optional)* | Set to true for L2 solutions (Optimism Bedrock based) | false |
| NEXT_PUBLIC_L1_BASE_URL | `string` *(optional)* | Base Blockscout URL for L1 network | `'http://eth-goerli.blockscout.com'` |
### Marketplace app configuration properties
| Property | Type | Description | Example value
......
......@@ -104,6 +104,10 @@ const config = Object.freeze({
socket: apiHost ? `wss://${ apiHost }` : 'wss://blockscout.com',
basePath: stripTrailingSlash(getEnvValue(process.env.NEXT_PUBLIC_API_BASE_PATH) || ''),
},
L2: {
isL2Network: getEnvValue(process.env.NEXT_PUBLIC_IS_L2_NETWORK) === 'true',
L1BaseUrl: getEnvValue(process.env.NEXT_PUBLIC_L1_BASE_URL),
},
statsApi: {
endpoint: getEnvValue(process.env.NEXT_PUBLIC_STATS_API_HOST),
basePath: '',
......@@ -122,6 +126,9 @@ const config = Object.freeze({
walletConnect: {
projectId: getEnvValue(process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID),
},
apiDoc: {
specUrl: getEnvValue(process.env.NEXT_PUBLIC_API_SPEC_URL),
},
reCaptcha: {
siteKey: getEnvValue(process.env.NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY) || '',
},
......
......@@ -9,6 +9,8 @@ NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
# api config
NEXT_PUBLIC_API_HOST=blockscout.com
......
This diff is collapsed.
......@@ -17,6 +17,7 @@ NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cup']
NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT=radial-gradient(at 12% 37%, hsla(324,73%,67%,1) 0px, transparent 50%), radial-gradient(at 62% 14%, hsla(256,87%,73%,1) 0px, transparent 50%), radial-gradient(at 84% 80%, hsla(128,75%,73%,1) 0px, transparent 50%), radial-gradient(at 57% 46%, hsla(285,63%,72%,1) 0px, transparent 50%), radial-gradient(at 37% 30%, hsla(174,70%,61%,1) 0px, transparent 50%), radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%), radial-gradient(at 67% 57%, hsla(14,95%,76%,1) 0px, transparent 50%)
#NEXT_PUBLIC_NETWORK_LOGO=https://placekitten.com/300/60
#NEXT_PUBLIC_NETWORK_SMALL_LOGO=https://placekitten.com/300/300
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
# network config
NEXT_PUBLIC_NETWORK_NAME=POA
......@@ -32,6 +33,7 @@ NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
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_NETWORK_RPC_URL=https://core.poa.network
# api config
NEXT_PUBLIC_API_BASE_PATH=/poa/core
......
......@@ -40,6 +40,10 @@ blockscout:
_default: ENC[AES256_GCM,data:mTY6sjNKZ+VEvH47eyyoUHt//beWvuxyreu+1WrmMvkdkwR6jgXnSXh+1pTuiG8e3it96Egxraz0hjZxlkY9kSmE91/dfoqKTns=,iv:op8OuOXXeBmmUnVkCL14j4HrfjHHoN3n2XZqLdg03Mw=,tag:bbFQYi0Aa1sUdfoUjuQ/+w==,type:str]
ACCOUNT_AUTH0_LOGOUT_URL:
_default: ENC[AES256_GCM,data:IZFfi6pn+hy7g0wnEtP9TYHH1fNiC2gqgRHVdgm4C9smPerEvS0pq9dBwVY=,iv:BxbSInFQ6GE2loTv+IzdYr25PlyzdWZI1wdT6r+uvBg=,tag:psujCU372k59OGmlRdH9Fg==,type:str]
RE_CAPTCHA_SECRET_KEY:
_default: ENC[AES256_GCM,data:e9jfs4PwY+UPk0EuXZxERylKUFSUo6zsAz8oly3YwwybipP+A5rSBQ==,iv:TvrD/a+6+11IGDVFLUCn8U+H3v1YfIRrcndOVdt/taI=,tag:OAFVIznkeeeVX9UsigfP4A==,type:str]
RE_CAPTCHA_CLIENT_KEY:
_default: ENC[AES256_GCM,data:6GPj8CQddvUofViL26AIBsVeMaYyrQ1Mh0kYUJ0HiIJc0otDewUc2Q==,iv:1LR4GBdSqN6ciKNCRI67d8oPWsTXnVI55hPSMvLL4hE=,tag:58pE8s9X7+eMOCY6bl4Z5w==,type:str]
scVerifier:
environment:
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__ACCESS_KEY:
......@@ -134,8 +138,8 @@ sops:
azure_kv: []
hc_vault: []
age: []
lastmodified: "2023-02-14T08:03:14Z"
mac: ENC[AES256_GCM,data:B2AkQb4I83dP3UUitRCcrUfzm3nWmcknIUoMWHyYaG9jasnccbr8zZatYdpbvKFcELVTtjhYk6ly5Sx7+6sk2PZm6o7dN3yHG5lSWmnZqNXkwo42GIk/F6vzDdLutZsu8HH8pWHd9y5R272CIPOOh4+Ur0OtwiGgj3Bp1od76qM=,iv:j7aIPflH0FsYhE/iylvBh5nDmVdghhxAFvaeXlR560k=,tag:/oe6OeitIHaZ4TgM7w/0pg==,type:str]
lastmodified: "2023-02-19T10:34:54Z"
mac: ENC[AES256_GCM,data:c7Nlguw+82tpgNAclx6XgXQip+gHh37Wrb9pq0MQ+bgp3LmfWpNDYYtxvdkIxMPNYsLxQOvw9Kc26qYWY1qDggmkHFvIeImCScjxBmrjSAf1K61lk0NIQ7AnEVh7ahipAopUO7y/0ogcjyjJCGs/QPRg9yelsONhCIjRkeUi7mQ=,iv:Y5tWWJJCqdNujHczKnouk4TiHBwMBezKhKCmvWeL9Kc=,tag:hIjUobEooUAQuKwHFraryw==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
......
......@@ -491,6 +491,7 @@ frontend:
- "/token"
- "/accounts"
- "/visualize"
- "/api-docs"
- "/csv-export"
resources:
......@@ -572,3 +573,5 @@ frontend:
_default: https://rpc.ankr.com/eth_goerli
NEXT_PUBLIC_HOMEPAGE_CHARTS:
_default: "['daily_txs','coin_price','market_cup']"
NEXT_PUBLIC_API_SPEC_URL:
_default: https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
......@@ -327,6 +327,7 @@ frontend:
- "/tokens"
- "/accounts"
- "/visualize"
- "/api-docs"
- "/csv-export"
resources:
......@@ -409,3 +410,5 @@ frontend:
_default: https://rpc.ankr.com/eth_goerli
NEXT_PUBLIC_NETWORK_EXPLORERS:
_default: "[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/ethereum/goerli/transaction','address':'/ethereum/ethereum/goerli/address'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address'}}]"
NEXT_PUBLIC_API_SPEC_URL:
_default: https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
<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 ];
// eslint-disable-next-line no-restricted-properties
const REPORT_URI = process.env.SENTRY_CSP_REPORT_URI;
function getNetworksExternalAssets() {
function getNetworksExternalAssetsHosts() {
const icons = featuredNetworks
.filter(({ icon }) => typeof icon === 'string')
.map(({ icon }) => new URL(icon as string));
.map(({ icon }) => new URL(icon as string).host);
const logo = appConfig.network.logo ? new URL(appConfig.network.logo) : undefined;
const logo = appConfig.network.logo ? new URL(appConfig.network.logo).host : undefined;
return logo ? icons.concat(logo) : icons;
}
function getMarketplaceAppsOrigins() {
return appConfig.marketplaceAppList.map(({ url }) => url);
}
function getMarketplaceAppsLogosOrigins() {
return appConfig.marketplaceAppList.map(({ logo }) => new URL(logo));
function getMarketplaceAppsHosts() {
return {
frames: appConfig.marketplaceAppList.map(({ url }) => new URL(url).host),
logos: appConfig.marketplaceAppList.map(({ logo }) => new URL(logo).host),
};
}
// we cannot use lodash/uniq in middleware code since it calls new Set() and it'is causing an error in Nextjs
......@@ -46,7 +45,7 @@ function unique(array: Array<string | undefined>) {
}
function makePolicyMap() {
const networkExternalAssets = getNetworksExternalAssets();
const marketplaceAppsHosts = getMarketplaceAppsHosts();
return {
'default-src': [
......@@ -135,10 +134,10 @@ function makePolicyMap() {
'avatars.githubusercontent.com', // github avatars
// network assets
...networkExternalAssets.map((url) => url.host),
...getNetworksExternalAssetsHosts(),
// marketplace apps logos
...getMarketplaceAppsLogosOrigins().map((url) => url.host),
...marketplaceAppsHosts.logos,
// ad
'servedbyadbutler.com',
......@@ -172,7 +171,7 @@ function makePolicyMap() {
],
'frame-src': [
...getMarketplaceAppsOrigins(),
...marketplaceAppsHosts.frames,
// ad
'request-global.czilladx.com',
......
import React from 'react';
import type { Address } from 'types/api/address';
import notEmpty from 'lib/notEmpty';
import ContractCode from 'ui/address/contract/ContractCode';
import ContractRead from 'ui/address/contract/ContractRead';
import ContractWrite from 'ui/address/contract/ContractWrite';
export default function useContractTabs(data: Address | undefined) {
return React.useMemo(() => {
return [
{ id: 'contact_code', title: 'Code', component: <ContractCode addressHash={ data?.hash }/> },
// this is not implemented in api yet
// data?.has_decompiled_code ?
// { id: 'contact_decompiled_code', title: 'Decompiled code', component: <div>Decompiled code</div> } :
// undefined,
data?.has_methods_read ?
{ id: 'read_contract', title: 'Read contract', component: <ContractRead addressHash={ data?.hash }/> } :
undefined,
data?.has_methods_read_proxy ?
{ id: 'read_proxy', title: 'Read proxy', component: <ContractRead addressHash={ data?.hash } isProxy/> } :
undefined,
data?.has_custom_methods_read ?
{ id: 'read_custom_methods', title: 'Read custom', component: <ContractRead addressHash={ data?.hash } isCustomAbi/> } :
undefined,
data?.has_methods_write ?
{ id: 'write_contract', title: 'Write contract', component: <ContractWrite addressHash={ data?.hash }/> } :
undefined,
data?.has_methods_write_proxy ?
{ id: 'write_proxy', title: 'Write proxy', component: <ContractWrite addressHash={ data?.hash } isProxy/> } :
undefined,
data?.has_custom_methods_write ?
{ id: 'write_custom_methods', title: 'Write custom', component: <ContractWrite addressHash={ data?.hash } isCustomAbi/> } :
undefined,
].filter(notEmpty);
}, [ data ]);
}
......@@ -14,14 +14,6 @@ export default function useGetCsrfToken() {
const url = buildUrl('csrf');
const apiResponse = await fetch(url, { credentials: 'include' });
const csrfFromHeader = apiResponse.headers.get('x-bs-account-csrf');
// eslint-disable-next-line no-console
console.log('>>> RESPONSE HEADERS <<<');
// eslint-disable-next-line no-console
console.table([ {
'content-length': apiResponse.headers.get('content-length'),
'x-bs-account-csrf': csrfFromHeader,
} ]);
return csrfFromHeader ? { token: csrfFromHeader } : undefined;
}
......
......@@ -7,14 +7,16 @@ import abiIcon from 'icons/ABI.svg';
import apiKeysIcon from 'icons/API.svg';
import appsIcon from 'icons/apps.svg';
import blocksIcon from 'icons/block.svg';
// import gearIcon from 'icons/gear.svg';
import gearIcon from 'icons/gear.svg';
import globeIcon from 'icons/globe-b.svg';
import privateTagIcon from 'icons/privattags.svg';
import profileIcon from 'icons/profile.svg';
import publicTagIcon from 'icons/publictags.svg';
import statsIcon from 'icons/stats.svg';
import tokensIcon from 'icons/token.svg';
import topAccountsIcon from 'icons/top-accounts.svg';
import transactionsIcon from 'icons/transactions.svg';
import walletIcon from 'icons/wallet.svg';
// import verifiedIcon from 'icons/verified.svg';
import watchlistIcon from 'icons/watchlist.svg';
import notEmpty from 'lib/notEmpty';
......@@ -26,31 +28,64 @@ export interface NavItem {
isNewUi?: boolean;
}
export interface NavGroupItem extends Omit<NavItem, 'nextRoute'> {
subItems: Array<NavItem>;
}
interface ReturnType {
mainNavItems: Array<NavItem>;
mainNavItems: Array<NavItem | NavGroupItem>;
accountNavItems: Array<NavItem>;
profileItem: NavItem;
}
export function isGroupItem(item: NavItem | NavGroupItem): item is NavGroupItem {
return 'subItems' in item;
}
export default function useNavItems(): ReturnType {
const isMarketplaceFilled = appConfig.marketplaceAppList.length > 0;
const isMarketplaceFilled = appConfig.marketplaceAppList.length > 0 && appConfig.network.rpcUrl;
const hasAPIDocs = appConfig.apiDoc.specUrl;
const router = useRouter();
const pathname = router.pathname;
return React.useMemo(() => {
const mainNavItems = [
const blockchainNavItems: Array<NavItem> = [
{ text: 'Top accounts', nextRoute: { pathname: '/accounts' as const }, icon: topAccountsIcon, isActive: pathname === '/accounts', isNewUi: true },
{ text: 'Blocks', nextRoute: { pathname: '/blocks' as const }, icon: blocksIcon, isActive: pathname.startsWith('/block'), isNewUi: true },
{ text: 'Transactions', nextRoute: { pathname: '/txs' as const }, icon: transactionsIcon, isActive: pathname.startsWith('/tx'), isNewUi: true },
// eslint-disable-next-line max-len
// { text: 'Verified contracts', nextRoute: { pathname: '/verified_contracts' as const }, icon: verifiedIcon, isActive: pathname === '/verified_contracts', isNewUi: false },
];
const otherNavItems: Array<NavItem> = [
hasAPIDocs ? {
text: 'API documentation',
nextRoute: { pathname: '/api-docs' as const },
// FIXME: need icon for this item
icon: topAccountsIcon,
isActive: pathname === '/api-docs',
isNewUi: true,
} : null,
].filter(notEmpty);
const mainNavItems = [
{
text: 'Blockchain',
icon: globeIcon,
isActive: blockchainNavItems.some(item => item.isActive),
isNewUi: true,
subItems: blockchainNavItems,
},
{ text: 'Tokens', nextRoute: { pathname: '/tokens' as const }, icon: tokensIcon, isActive: pathname.startsWith('/token'), isNewUi: true },
{ text: 'Accounts', nextRoute: { pathname: '/accounts' as const }, icon: walletIcon, isActive: pathname === '/accounts', isNewUi: true },
isMarketplaceFilled ?
{ text: 'Apps', nextRoute: { pathname: '/apps' as const }, icon: appsIcon, isActive: pathname.startsWith('/app'), isNewUi: true } : null,
{ text: 'Charts & stats', nextRoute: { pathname: '/stats' as const }, icon: statsIcon, isActive: pathname === '/stats', isNewUi: true },
// there should be custom site sections like Stats, Faucet, More, etc but never an 'other'
// examples https://explorer-edgenet.polygon.technology/ and https://explorer.celo.org/
// at this stage custom menu items is under development, we will implement it later
// { text: 'Other', url: link('other'), icon: gearIcon, isActive: pathname === 'other' },
otherNavItems.length > 0 ?
{ text: 'Other', icon: gearIcon, isActive: otherNavItems.some(item => item.isActive), subItems: otherNavItems } : null,
].filter(notEmpty);
const accountNavItems = [
......@@ -87,5 +122,5 @@ export default function useNavItems(): ReturnType {
text: 'My profile', nextRoute: { pathname: '/auth/profile' as const }, icon: profileIcon, isActive: pathname === '/auth/profile', isNewUi: true };
return { mainNavItems, accountNavItems, profileItem };
}, [ isMarketplaceFilled, pathname ]);
}, [ hasAPIDocs, isMarketplaceFilled, pathname ]);
}
import type { GetServerSideProps } from 'next';
import appConfig from 'configs/app/config';
import type { Props } from 'lib/next/getServerSideProps';
import { getServerSideProps as getServerSidePropsBase } from 'lib/next/getServerSideProps';
export const getServerSideProps: GetServerSideProps<Props> = async(args) => {
if (!appConfig.L2.isL2Network) {
return {
notFound: true,
};
}
return getServerSidePropsBase(args);
};
......@@ -36,8 +36,10 @@ export function middleware(req: NextRequest) {
/**
* Configure which routes should pass through the Middleware.
* Exclude all `_next` urls.
*/
export const config = {
matcher: [ '/', '/:notunderscore((?!_next).+)' ],
// matcher: [
// '/((?!.*\\.|api\\/|node-api\\/).*)', // exclude all static + api + node-api routes
// ],
};
const withTM = require('next-transpile-modules')([
'react-syntax-highlighter',
'swagger-client',
'swagger-ui-react',
]);
const withRoutes = require('nextjs-routes/config')({
outDir: 'types',
});
......@@ -7,7 +12,7 @@ const headers = require('./configs/nextjs/headers');
const redirects = require('./configs/nextjs/redirects');
const rewrites = require('./configs/nextjs/rewrites');
const moduleExports = {
const moduleExports = withTM({
include: path.resolve(__dirname, 'icons'),
reactStrictMode: true,
webpack(config, { webpack }) {
......@@ -34,6 +39,11 @@ const moduleExports = {
redirects,
headers,
output: 'standalone',
};
api: {
// disable body parser since we use next.js api only for local development and as a proxy
// otherwise it is impossible to upload large files (over 1Mb)
bodyParser: false,
},
});
module.exports = withRoutes(moduleExports);
......@@ -12,6 +12,7 @@
"dev:local": "./node_modules/.bin/dotenv -e ./configs/envs/.env.localhost -e ./configs/envs/.env.secrets next dev | ./node_modules/.bin/pino-pretty",
"dev:poa_core": "./node_modules/.bin/dotenv -e ./configs/envs/.env.poa_core -e ./configs/envs/.env.common -e ./configs/envs/.env.secrets next dev | ./node_modules/.bin/pino-pretty",
"dev:goerli": "./node_modules/.bin/dotenv -e ./configs/envs/.env.goerli -e ./configs/envs/.env.common -e ./configs/envs/.env.secrets next dev | ./node_modules/.bin/pino-pretty",
"dev:goerli:l2test": "./node_modules/.bin/dotenv -e ./configs/envs/.env.goerli.l2test -e ./configs/envs/.env.common -e ./configs/envs/.env.secrets next dev | ./node_modules/.bin/pino-pretty",
"build": "next build",
"build:docker": "docker build --build-arg GIT_COMMIT_SHA=$(git rev-parse HEAD) -t blockscout ./",
"start": "next start",
......@@ -69,6 +70,7 @@
"react-identicons": "^1.2.5",
"react-jazzicon": "^1.0.4",
"react-scroll": "^1.8.7",
"swagger-ui-react": "^4.15.5",
"use-font-face-observer": "^1.2.1",
"wagmi": "^0.10.6"
},
......@@ -85,6 +87,7 @@
"@types/qrcode": "^1.5.0",
"@types/react": "18.0.9",
"@types/react-dom": "18.0.5",
"@types/swagger-ui-react": "^4.11.0",
"@types/react-google-recaptcha": "^2.1.5",
"@types/ws": "^8.5.3",
"@typescript-eslint/eslint-plugin": "^5.27.0",
......@@ -101,6 +104,7 @@
"jest-environment-jsdom": "^29.2.1",
"lint-staged": ">=10",
"mockdate": "^3.0.5",
"next-transpile-modules": "^10.0.0",
"playwright": "^1.28.0",
"svgo": "^2.8.0",
"ts-jest": "^29.0.3",
......
import type { NextPage, GetServerSideProps } from 'next';
import Head from 'next/head';
import React from 'react';
import appConfig from 'configs/app/config';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import { getServerSideProps as getServerSidePropsBase } from 'lib/next/getServerSideProps';
import type { Props } from 'lib/next/getServerSideProps';
import SwaggerUI from 'ui/apiDocs/SwaggerUI';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
const AppsPage: NextPage = () => {
const networkTitle = getNetworkTitle();
return (
<Page>
<PageTitle text="API Documentation"/>
<Head><title>{ `API for the ${ networkTitle }` }</title></Head>
<SwaggerUI/>
</Page>
);
};
export default AppsPage;
export const getServerSideProps: GetServerSideProps<Props> = async(args) => {
if (!appConfig.apiDoc.specUrl) {
return {
notFound: true,
};
}
return getServerSidePropsBase(args);
};
......@@ -4,6 +4,7 @@ import { WebSocketServer } from 'ws';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import type { NewBlockSocketResponse } from 'types/api/block';
import type { SmartContractVerificationResponse } from 'types/api/contract';
type ReturnType = () => Promise<WebSocket>;
......@@ -59,6 +60,7 @@ export function sendMessage(socket: WebSocket, channel: Channel, msg: 'token_bal
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transaction', payload: { transaction: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'pending_transaction', payload: { pending_transaction: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'new_block', payload: NewBlockSocketResponse): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'verification_result', payload: SmartContractVerificationResponse): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: string, payload: unknown): void {
socket.send(JSON.stringify([
...channel,
......
*,
*::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 './index.css';
import { beforeMount } from '@playwright/experimental-ct-react/hooks';
import _defaultsDeep from 'lodash/defaultsDeep';
import MockDate from 'mockdate';
......
......@@ -20,6 +20,9 @@ const semanticTokens = {
_dark: 'red.300',
},
},
shadows: {
action_bar: '0 4px 4px -4px rgb(0 0 0 / 10%), 0 2px 4px -4px rgb(0 0 0 / 6%)',
},
};
export default semanticTokens;
......@@ -42,7 +42,7 @@ export interface TokenInstance {
image_url: string | null;
animation_url: string | null;
external_app_url: string | null;
metadata: unknown;
metadata: Record<string, unknown> | null;
owner: AddressParam;
token: TokenInfo;
}
......
......@@ -16,6 +16,7 @@ declare module "nextjs-routes" {
| DynamicRoute<"/address/[hash]", { "hash": string }>
| StaticRoute<"/api/csrf">
| StaticRoute<"/api/proxy">
| StaticRoute<"/api-docs">
| DynamicRoute<"/apps/[id]", { "id": string }>
| StaticRoute<"/apps">
| StaticRoute<"/auth/auth0">
......
......@@ -76,7 +76,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
return (
<>
<Hide below="lg" ssr={ false }>
<SkeletonTable columns={ [ '17%', '17%', '16%', '25%', '25%' ] }/>
<SkeletonTable columns={ [ '17%', '17%', '16%', '25%', '25%' ] } isLong/>
</Hide>
<Show below="lg" ssr={ false }>
<SkeletonList/>
......@@ -125,7 +125,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
return (
<Box>
{ query.isPaginationVisible && (
<ActionBar mt={ -6 }>
<ActionBar mt={ -6 } showShadow={ query.isLoading }>
<Pagination ml="auto" { ...query.pagination }/>
</ActionBar>
) }
......
......@@ -17,9 +17,12 @@ import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
interface Props {
tabs: Array<RoutedSubTab>;
addressHash?: string;
}
export const currentChain: Chain = {
const { wagmiClient, ethereumClient } = (() => {
try {
const currentChain: Chain = {
id: Number(appConfig.network.id),
name: appConfig.network.name || '',
network: appConfig.network.name || '',
......@@ -39,38 +42,49 @@ export const currentChain: Chain = {
url: appConfig.baseUrl,
},
},
};
};
const chains = [ currentChain ];
const chains = [ currentChain ];
const { provider } = configureChains(chains, [
const { provider } = configureChains(chains, [
walletConnectProvider({ projectId: appConfig.walletConnect.projectId || '' }),
]);
const wagmiClient = createClient({
]);
const wagmiClient = createClient({
autoConnect: true,
connectors: modalConnectors({ appName: 'web3Modal', chains }),
provider,
});
});
const ethereumClient = new EthereumClient(wagmiClient, chains);
const ethereumClient = new EthereumClient(wagmiClient, chains);
return { wagmiClient, ethereumClient };
} catch (error) {
return { wagmiClient: undefined, ethereumClient: undefined };
}
})();
const TAB_LIST_PROPS = {
columnGap: 3,
};
const AddressContract = ({ tabs }: Props) => {
const AddressContract = ({ addressHash, tabs }: Props) => {
const modalZIndex = useToken<string>('zIndices', 'modal');
const web3ModalTheme = useColorModeValue('light', 'dark');
const noProviderTabs = React.useMemo(() => tabs.filter(({ id }) => id === 'contact_code'), [ tabs ]);
if (!wagmiClient || !ethereumClient) {
return <RoutedTabs tabs={ noProviderTabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS }/>;
}
return (
<WagmiConfig client={ wagmiClient }>
<ContractContextProvider>
<ContractContextProvider addressHash={ addressHash }>
<RoutedTabs tabs={ tabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS }/>
</ContractContextProvider>
<Web3Modal
projectId={ appConfig.walletConnect.projectId }
ethereumClient={ ethereumClient }
themeZIndex={ Number(modalZIndex) }
themeMode={ useColorModeValue('light', 'dark') }
themeMode={ web3ModalTheme }
themeBackground="themeColor"
/>
</WagmiConfig>
......
......@@ -11,6 +11,7 @@ import * as countersMock from 'mocks/address/counters';
import * as tokensMock from 'mocks/address/tokens';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import insertAdPlaceholder from 'playwright/utils/insertAdPlaceholder';
import AddressDetails from './AddressDetails';
import MockAddressPage from './testUtils/MockAddressPage';
......@@ -44,6 +45,8 @@ test('contract +@mobile', async({ mount, page }) => {
{ hooksConfig },
);
await insertAdPlaceholder(page);
await expect(component).toHaveScreenshot();
});
......@@ -82,6 +85,8 @@ test('token', async({ mount, page }) => {
{ hooksConfig },
);
await insertAdPlaceholder(page);
await expect(component).toHaveScreenshot();
});
......@@ -102,5 +107,7 @@ test('validator +@mobile', async({ mount, page }) => {
{ hooksConfig },
);
await insertAdPlaceholder(page);
await expect(component).toHaveScreenshot();
});
......@@ -16,6 +16,7 @@ import AddressLink from 'ui/shared/address/AddressLink';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
......@@ -92,7 +93,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
<Text fontSize="sm">Verify with other explorers</Text>
{ explorers.map((explorer) => {
const url = new URL(explorer.paths.address + '/' + addressHash, explorer.baseUrl);
return <LinkExternal key={ explorer.baseUrl } title={ explorer.title } href={ url.toString() }/>;
return <LinkExternal key={ explorer.baseUrl } href={ url.toString() }>{ explorer.title }</LinkExternal>;
}) }
</Flex>
) }
......@@ -193,6 +194,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
</LinkInternal>
</DetailsInfoItem>
) }
<DetailsSponsoredItem/>
</Grid>
</Box>
);
......
......@@ -9,13 +9,13 @@ import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities';
import getQueryParamString from 'lib/router/getQueryParamString';
import AddressIntTxsSkeletonDesktop from 'ui/address/internals/AddressIntTxsSkeletonDesktop';
import AddressIntTxsSkeletonMobile from 'ui/address/internals/AddressIntTxsSkeletonMobile';
import AddressIntTxsTable from 'ui/address/internals/AddressIntTxsTable';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Pagination from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import AddressCsvExportLink from './AddressCsvExportLink';
import AddressTxsFilter from './AddressTxsFilter';
......@@ -43,29 +43,33 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
onFilterChange({ filter: newVal });
}, [ onFilterChange ]);
const content = (() => {
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return (
<>
<Show below="lg" ssr={ false }><AddressIntTxsSkeletonMobile/></Show>
<Hide below="lg" ssr={ false }><AddressIntTxsSkeletonDesktop/></Hide>
<Show below="lg" ssr={ false }>
<SkeletonList/>
</Show>
<Hide below="lg" ssr={ false }>
<SkeletonTable columns={ [ '15%', '15%', '10%', '20%', '20%', '20%' ] } isLong/>
</Hide>
</>
);
}
if (isError) {
return <DataFetchAlert/>;
}
if (data.items.length === 0 && !filterValue) {
return <Text as="span">There are no internal transactions for this address.</Text>;
}
let content;
if (data.items.length === 0) {
content = <EmptySearchResult text={ `Couldn${ apos }t find any transaction that matches your query.` }/>;
} else {
content = (
return <EmptySearchResult text={ `Couldn${ apos }t find any transaction that matches your query.` }/>;
}
return (
<>
<Show below="lg" ssr={ false }>
<AddressIntTxsList data={ data.items } currentAddress={ hash }/>
......@@ -75,11 +79,11 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
</Hide>
</>
);
}
})();
return (
<>
<ActionBar mt={ -6 } justifyContent="left">
<ActionBar mt={ -6 } justifyContent="left" showShadow={ isLoading }>
<AddressTxsFilter
defaultFilter={ filterValue }
onFilterChange={ handleFilterChange }
......
......@@ -25,7 +25,7 @@ const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>
}
const bar = isPaginationVisible ? (
<ActionBar mt={ -6 }>
<ActionBar mt={ -6 } showShadow>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) : null;
......
......@@ -169,7 +169,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
return (
<>
<Hide below="lg" ssr={ false }>
<SkeletonTable columns={ [ '44px', '185px', '160px', '25%', '25%', '25%', '25%' ] }/>
<SkeletonTable columns={ [ '44px', '185px', '160px', '25%', '25%', '25%', '25%' ] } isLong/>
</Hide>
<Show below="lg" ssr={ false }>
<SkeletonList/>
......@@ -253,7 +253,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
<>
{ isMobile && tokenFilterComponent }
{ !isActionBarHidden && (
<ActionBar mt={ -6 }>
<ActionBar mt={ -6 } showShadow={ isLoading }>
{ !isMobile && tokenFilterComponent }
{ !tokenFilter && (
<TokenTransferFilter
......
......@@ -138,7 +138,7 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
return (
<>
{ !isMobile && (
<ActionBar mt={ -6 }>
<ActionBar mt={ -6 } showShadow={ addressTxsQuery.isLoading }>
{ filter }
{ currentAddress && <AddressCsvExportLink address={ currentAddress } type="transactions" ml="auto"/> }
{ addressTxsQuery.isPaginationVisible && <Pagination { ...addressTxsQuery.pagination } ml={ 8 }/> }
......@@ -152,6 +152,8 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
showSocketInfo={ addressTxsQuery.pagination.page === 1 }
socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount }
top={ 80 }
hasLongSkeleton
/>
</>
);
......
......@@ -43,6 +43,10 @@ const AddressCoinBalanceHistory = ({ query }: Props) => {
return <DataFetchAlert/>;
}
if (query.data.items.length === 0 && !query.isPaginationVisible) {
return <span>There is no coin balance history for this address</span>;
}
return (
<>
<Hide below="lg" ssr={ false }>
......
......@@ -23,7 +23,7 @@ test('verified with changed byte code +@mobile +@dark-mode', async({ mount, page
const component = await mount(
<TestApp>
<ContractCode/>
<ContractCode addressHash={ addressHash }/>
</TestApp>,
{ hooksConfig },
);
......@@ -39,7 +39,7 @@ test('verified with multiple sources +@mobile', async({ mount, page }) => {
await mount(
<TestApp>
<ContractCode/>
<ContractCode addressHash={ addressHash }/>
</TestApp>,
{ hooksConfig },
);
......@@ -57,7 +57,7 @@ test('verified via sourcify', async({ mount, page }) => {
await mount(
<TestApp>
<ContractCode/>
<ContractCode addressHash={ addressHash }/>
</TestApp>,
{ hooksConfig },
);
......@@ -73,7 +73,7 @@ test('self destructed', async({ mount, page }) => {
await mount(
<TestApp>
<ContractCode/>
<ContractCode addressHash={ addressHash }/>
</TestApp>,
{ hooksConfig },
);
......@@ -90,7 +90,7 @@ test('with twin address alert +@mobile', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractCode/>
<ContractCode addressHash={ addressHash }/>
</TestApp>,
{ hooksConfig },
);
......@@ -106,7 +106,7 @@ test('with proxy address alert +@mobile', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractCode/>
<ContractCode addressHash={ addressHash }/>
</TestApp>,
{ hooksConfig },
);
......@@ -122,7 +122,7 @@ test('non verified', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractCode/>
<ContractCode addressHash={ addressHash }/>
</TestApp>,
{ hooksConfig },
);
......
import { Flex, Skeleton, Button, Grid, GridItem, Text, Alert, Link, chakra, Box } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { route } from 'nextjs-routes';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import dayjs from 'lib/date/dayjs';
import getQueryParamString from 'lib/router/getQueryParamString';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
......@@ -16,6 +14,10 @@ import RawDataSnippet from 'ui/shared/RawDataSnippet';
import ContractSourceCode from './ContractSourceCode';
type Props = {
addressHash?: string;
}
const InfoItem = chakra(({ label, value, className }: { label: string; value: string; className?: string }) => (
<GridItem display="flex" columnGap={ 6 } wordBreak="break-all" className={ className }>
<Text w="170px" flexShrink={ 0 } fontWeight={ 500 }>{ label }</Text>
......@@ -23,10 +25,7 @@ const InfoItem = chakra(({ label, value, className }: { label: string; value: st
</GridItem>
));
const ContractCode = () => {
const router = useRouter();
const addressHash = getQueryParamString(router.query.hash);
const ContractCode = ({ addressHash }: Props) => {
const { data, isLoading, isError } = useApiQuery('contract', {
pathParams: { hash: addressHash },
queryOptions: {
......@@ -62,7 +61,7 @@ const ContractCode = () => {
ml="auto"
mr={ 3 }
as="a"
href={ route({ pathname: '/address/[hash]/contract_verification', query: { hash: addressHash } }) }
href={ route({ pathname: '/address/[hash]/contract_verification', query: { hash: addressHash || '' } }) }
>
Verify & publish
</Button>
......@@ -115,7 +114,7 @@ const ContractCode = () => {
{ data.is_verified_via_sourcify && (
<Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap">
<span>This contract has been { data.is_partially_verified ? 'partially ' : '' }verified via Sourcify. </span>
{ data.sourcify_repo_url && <LinkExternal href={ data.sourcify_repo_url } title="View contract in Sourcify repository" fontSize="md"/> }
{ data.sourcify_repo_url && <LinkExternal href={ data.sourcify_repo_url } fontSize="md">View contract in Sourcify repository</LinkExternal> }
</Alert>
) }
{ data.is_changed_bytecode && (
......@@ -131,7 +130,9 @@ const ContractCode = () => {
<AddressLink type="address" hash={ data.verified_twin_address_hash } truncation="constant" ml={ 2 }/>
</Address>
<chakra.span mt={ 1 }>All functions displayed below are from ABI of that contract. In order to verify current contract, proceed with </chakra.span>
<LinkInternal href={ route({ pathname: '/address/[hash]/contract_verification', query: { hash: addressHash } }) }>Verify & Publish</LinkInternal>
<LinkInternal href={ route({ pathname: '/address/[hash]/contract_verification', query: { hash: addressHash || '' } }) }>
Verify & Publish
</LinkInternal>
<span> page</span>
</Alert>
) }
......
......@@ -35,14 +35,24 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, renderC
setId((id) => id + 1);
}, []);
if (data.length === 0) {
return null;
}
return (
<>
<Flex mb={ 3 }>
<Box fontWeight={ 500 }>Contract information</Box>
<Link onClick={ handleExpandAll } ml="auto">{ expandedSections.length === data.length ? 'Collapse' : 'Expand' } all</Link>
<Link onClick={ handleReset } ml={ 3 }>Reset</Link>
</Flex>
<Accordion allowMultiple position="relative" onChange={ handleAccordionStateChange } index={ expandedSections }>
{ data.map((item, index) => {
return (
<AccordionItem key={ index } as="section" _first={{ borderTopWidth: '0', '.chakra-accordion__button': { pr: '150px' } }}>
<AccordionItem key={ index } as="section" _first={{ borderTopWidth: '0' }}>
<h2>
<AccordionButton px={ 0 } py={ 3 } _hover={{ bgColor: 'inherit' }} wordBreak="break-all" textAlign="left">
<Box as="span" fontFamily="heading" fontWeight={ 500 } fontSize="lg" mr={ 1 }>
<Box as="span" fontWeight={ 500 } mr={ 1 }>
{ index + 1 }. { item.type === 'fallback' || item.type === 'receive' ? item.type : item.name }
</Box>
{ item.type === 'fallback' && (
......@@ -72,13 +82,8 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, renderC
</AccordionItem>
);
}) }
{ data.length > 0 && (
<Flex columnGap={ 3 } position="absolute" top={ 0 } right={ 0 } py={ 3 } lineHeight="27px">
<Link onClick={ handleExpandAll }>{ expandedSections.length === data.length ? 'Collapse' : 'Expand' } all</Link>
<Link onClick={ handleReset }>Reset</Link>
</Flex>
) }
</Accordion>
</>
);
};
......
......@@ -28,7 +28,7 @@ test('base view +@mobile +@dark-mode', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractRead/>
<ContractRead addressHash={ addressHash }/>
</TestApp>,
{ hooksConfig },
);
......@@ -57,7 +57,7 @@ test('error result', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractRead/>
<ContractRead addressHash={ addressHash }/>
</TestApp>,
{ hooksConfig },
);
......
import { Alert, Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import { useAccount } from 'wagmi';
......@@ -7,7 +6,6 @@ import type { SmartContractReadMethod, SmartContractQueryMethodRead } from 'type
import useApiFetch from 'lib/api/useApiFetch';
import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
......@@ -20,17 +18,15 @@ import ContractMethodConstant from './ContractMethodConstant';
import ContractReadResult from './ContractReadResult';
interface Props {
addressHash?: string;
isProxy?: boolean;
isCustomAbi?: boolean;
}
const ContractRead = ({ isProxy, isCustomAbi }: Props) => {
const router = useRouter();
const ContractRead = ({ addressHash, isProxy, isCustomAbi }: Props) => {
const apiFetch = useApiFetch();
const { address: userAddress } = useAccount();
const addressHash = getQueryParamString(router.query.hash);
const { data, isLoading, isError } = useApiQuery(isProxy ? 'contract_methods_read_proxy' : 'contract_methods_read', {
pathParams: { hash: addressHash },
queryParams: {
......
......@@ -23,7 +23,7 @@ test('base view +@mobile', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractWrite/>
<ContractWrite addressHash={ addressHash }/>
</TestApp>,
{ hooksConfig },
);
......
import _capitalize from 'lodash/capitalize';
import { useRouter } from 'next/router';
import React from 'react';
import { useAccount, useSigner } from 'wagmi';
......@@ -7,7 +6,6 @@ import type { SmartContractWriteMethod } from 'types/api/contract';
import config from 'configs/app/config';
import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
......@@ -21,14 +19,12 @@ import ContractWriteResult from './ContractWriteResult';
import { getNativeCoinValue, isExtendedError } from './utils';
interface Props {
addressHash?: string;
isProxy?: boolean;
isCustomAbi?: boolean;
}
const ContractWrite = ({ isProxy, isCustomAbi }: Props) => {
const router = useRouter();
const addressHash = getQueryParamString(router.query.hash);
const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
const { data: signer } = useSigner();
const { isConnected } = useAccount();
......
import { useQueryClient } from '@tanstack/react-query';
import type { Contract } from 'ethers';
import { useRouter } from 'next/router';
import React from 'react';
import { useContract, useProvider, useSigner } from 'wagmi';
......@@ -9,6 +8,7 @@ import type { Address } from 'types/api/address';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
type ProviderProps = {
addressHash?: string;
children: React.ReactNode;
}
......@@ -24,13 +24,11 @@ const ContractContext = React.createContext<TContractContext>({
custom: null,
});
export function ContractContextProvider({ children }: ProviderProps) {
const router = useRouter();
export function ContractContextProvider({ addressHash, children }: ProviderProps) {
const provider = useProvider();
const { data: signer } = useSigner();
const queryClient = useQueryClient();
const addressHash = router.query.hash?.toString();
const { data: contractInfo } = useApiQuery('contract', {
pathParams: { hash: addressHash },
queryOptions: {
......
......@@ -20,7 +20,7 @@ const AddressNameInfo = ({ data }: Props) => {
hint="Token name and symbol"
>
<LinkInternal href={ route({ pathname: '/token/[hash]', query: { hash: data.token.address } }) }>
{ data.token.name }{ symbol }
{ data.token.name || 'Unnamed token' }{ symbol }
</LinkInternal>
</DetailsInfoItem>
);
......
import React from 'react';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
const TxInternalsSkeletonDesktop = () => {
return (
<SkeletonTable columns={ [ '15%', '15%', '10%', '20%', '20%', '20%' ] }/>
);
};
export default TxInternalsSkeletonDesktop;
import { Skeleton, SkeletonCircle, Flex, Box } from '@chakra-ui/react';
import React from 'react';
const TxInternalsSkeletonMobile = () => {
return (
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
key={ index }
rowGap={ 3 }
flexDirection="column"
paddingY={ 6 }
borderTopWidth="1px"
borderColor="divider"
_last={{
borderBottomWidth: '1px',
}}
>
<Flex h={ 6 }>
<Skeleton w="100px" mr={ 2 }/>
<Skeleton w="90px"/>
</Flex>
<Flex h={ 6 }>
<Skeleton w="100%" mr={ 2 }/>
<Skeleton w="60px"/>
</Flex>
<Flex h={ 6 }>
<Skeleton w="70px" mr={ 2 }/>
<Skeleton w="30px"/>
</Flex>
<Flex h={ 6 }>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
<Skeleton w={ 6 } mr={ 3 }/>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
</Flex>
<Flex h={ 6 }>
<Skeleton w="70px" mr={ 2 }/>
<Skeleton w="30px"/>
</Flex>
</Flex>
)) }
</Box>
);
};
export default TxInternalsSkeletonMobile;
/* eslint-disable @typescript-eslint/naming-convention */
const SwaggerUIReact = dynamic(() => import('swagger-ui-react'), {
loading: () => <Spinner/>,
ssr: false,
});
import { Box, Spinner, useColorModeValue } from '@chakra-ui/react';
import dynamic from 'next/dynamic';
import React from 'react';
import appConfig from 'configs/app/config';
import 'swagger-ui-react/swagger-ui.css';
const DEFAULT_SERVER = 'blockscout.com/poa/core';
const NeverShowInfoPlugin = () => {
return {
components: {
SchemesContainer: () => null,
ServersContainer: () => null,
InfoContainer: () => null,
},
};
};
const SwaggerUI = () => {
const swaggerStyle = {
'.scheme-container, .opblock-tag': {
display: 'none',
},
'.swagger-ui': {
color: useColorModeValue('blackAlpha.800', 'whiteAlpha.800'),
},
'.swagger-ui .opblock-summary-control:focus': {
outline: 'none',
},
// eslint-disable-next-line max-len
'.swagger-ui .opblock .opblock-summary-path, .swagger-ui .opblock .opblock-summary-description, .swagger-ui div, .swagger-ui p, .swagger-ui h5, .swagger-ui .response-col_links, .swagger-ui h4, .swagger-ui table thead tr th, .swagger-ui table thead tr td, .swagger-ui .parameter__name, .swagger-ui .parameter__type, .swagger-ui .response-col_status, .swagger-ui .tab li, .swagger-ui .opblock .opblock-section-header h4': {
color: 'unset',
},
'.swagger-ui input': {
color: 'blackAlpha.800',
},
'.swagger-ui .opblock .opblock-section-header': {
background: useColorModeValue('whiteAlpha.800', 'blackAlpha.800'),
},
'.swagger-ui .response-col_description__inner p, .swagger-ui .parameters-col_description p': {
margin: 0,
},
'.swagger-ui .wrapper': {
padding: 0,
},
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const reqInterceptor = React.useCallback((req: any) => {
if (!req.loadSpec) {
req.url = req.url.replace(DEFAULT_SERVER, appConfig.api.host);
}
return req;
}, []);
return (
<Box sx={ swaggerStyle }>
<SwaggerUIReact
url={ appConfig.apiDoc.specUrl }
plugins={ [ NeverShowInfoPlugin ] }
requestInterceptor={ reqInterceptor }
/>
</Box>
);
};
export default SwaggerUI;
......@@ -118,7 +118,7 @@ const BlockDetails = () => {
hint="The number of transactions in the block"
>
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height, tab: 'txs' } }) }>
{ data.tx_count } transactions
{ data.tx_count } transaction{ data.tx_count === 1 ? '' : 's' }
</LinkInternal>
</DetailsInfoItem>
<DetailsInfoItem
......@@ -274,18 +274,18 @@ const BlockDetails = () => {
<DetailsInfoItem
title="Difficulty"
hint={ `Block difficulty for ${ validatorTitle }, used to calibrate block generation time` }
whiteSpace="normal"
wordBreak="break-all"
>
{ BigNumber(data.difficulty).toFormat() }
<Box whiteSpace="nowrap" overflow="hidden">
<HashStringShortenDynamic hash={ BigNumber(data.difficulty).toFormat() }/>
</Box>
</DetailsInfoItem>
<DetailsInfoItem
title="Total difficulty"
hint="Total difficulty of the chain until this block"
whiteSpace="normal"
wordBreak="break-all"
>
{ BigNumber(data.total_difficulty).toFormat() }
<Box whiteSpace="nowrap" overflow="hidden">
<HashStringShortenDynamic hash={ BigNumber(data.total_difficulty).toFormat() }/>
</Box>
</DetailsInfoItem>
{ sectionGap }
......
......@@ -3,6 +3,7 @@ import React from 'react';
import type { SmartContractVerificationConfig } from 'types/api/contract';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import ContractVerificationForm from './ContractVerificationForm';
......@@ -34,6 +35,7 @@ const formConfig: SmartContractVerificationConfig = {
'sourcify',
'multi-part',
'vyper-code',
'vyper-multi-part',
],
vyper_compiler_versions: [
'v0.3.7+commit.6020b8bb',
......@@ -60,8 +62,9 @@ test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) =
{ hooksConfig },
);
await page.getByText(/flattened source code/i).click();
await page.getByText(/optimization enabled/i).click();
await component.getByLabel(/verification method/i).focus();
await component.getByLabel(/verification method/i).type('solidity');
await page.getByRole('button', { name: /flattened source code/i }).click();
await page.getByText(/add contract libraries/i).click();
await page.locator('button[aria-label="add"]').click();
......@@ -76,21 +79,32 @@ test('standard input json method', async({ mount, page }) => {
{ hooksConfig },
);
await page.getByText(/via standard/i).click();
await component.getByLabel(/verification method/i).focus();
await component.getByLabel(/verification method/i).type('solidity');
await page.getByRole('button', { name: /standard json input/i }).click();
await expect(component).toHaveScreenshot();
});
test('sourcify method +@dark-mode +@mobile', async({ mount, page }) => {
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>
<TestApp withSocket>
<ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>,
{ hooksConfig },
);
await page.getByText(/via sourcify/i).click();
await page.getByText(/upload files/i).click();
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',
......@@ -98,6 +112,28 @@ test('sourcify method +@dark-mode +@mobile', async({ mount, page }) => {
]);
await expect(component).toHaveScreenshot();
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ hash.toLowerCase() }`);
await page.getByRole('button', { name: /verify/i }).click();
socketServer.sendMessage(socket, channel, 'verification_result', {
status: 'error',
errors: {
// eslint-disable-next-line max-len
files: [ 'Detected 5 contracts (ERC20, IERC20, IERC20Metadata, Context, MockERC20), but can only verify 1 at a time. Please choose a main contract and click Verify again.' ],
},
});
await component.getByLabel(/contract name/i).focus();
await component.getByLabel(/contract name/i).type('e');
const contractNameOption = page.getByRole('button', { name: /MockERC20/i });
await expect(contractNameOption).toBeVisible();
await expect(component).toHaveScreenshot();
});
});
test('multi-part files method', async({ mount, page }) => {
......@@ -108,7 +144,9 @@ test('multi-part files method', async({ mount, page }) => {
{ hooksConfig },
);
await page.getByText(/via multi-part files/i).click();
await component.getByLabel(/verification method/i).focus();
await component.getByLabel(/verification method/i).type('solidity');
await page.getByRole('button', { name: /multi-part files/i }).click();
await expect(component).toHaveScreenshot();
});
......@@ -121,7 +159,24 @@ test('vyper contract method', async({ mount, page }) => {
{ hooksConfig },
);
await page.getByText(/vyper contract/i).click();
await component.getByLabel(/verification method/i).focus();
await component.getByLabel(/verification method/i).type('vyper');
await page.getByRole('button', { name: /contract/i }).click();
await expect(component).toHaveScreenshot();
});
test('vyper multi-part method', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>,
{ hooksConfig },
);
await component.getByLabel(/verification method/i).focus();
await component.getByLabel(/verification method/i).type('vyper');
await page.getByRole('button', { name: /multi-part files/i }).click();
await expect(component).toHaveScreenshot();
});
import { Button, chakra } from '@chakra-ui/react';
import { Button, chakra, useUpdateEffect } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { SubmitHandler } from 'react-hook-form';
......@@ -20,7 +20,7 @@ import ContractVerificationSourcify from './methods/ContractVerificationSourcify
import ContractVerificationStandardInput from './methods/ContractVerificationStandardInput';
import ContractVerificationVyperContract from './methods/ContractVerificationVyperContract';
import ContractVerificationVyperMultiPartFile from './methods/ContractVerificationVyperMultiPartFile';
import { prepareRequestBody, formatSocketErrors } from './utils';
import { prepareRequestBody, formatSocketErrors, DEFAULT_VALUES } from './utils';
const METHOD_COMPONENTS = {
'flattened-code': <ContractVerificationFlattenSourceCode/>,
......@@ -40,11 +40,9 @@ interface Props {
const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Props) => {
const formApi = useForm<FormFields>({
mode: 'onBlur',
defaultValues: {
method: methodFromQuery,
},
defaultValues: methodFromQuery ? DEFAULT_VALUES[methodFromQuery] : undefined,
});
const { control, handleSubmit, watch, formState, setError } = formApi;
const { control, handleSubmit, watch, formState, setError, reset } = formApi;
const submitPromiseResolver = React.useRef<(value: unknown) => void>();
const apiFetch = useApiFetch();
......@@ -56,7 +54,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
try {
await apiFetch('contract_verification_via', {
pathParams: { method: data.method, hash },
pathParams: { method: data.method.value, hash: hash.toLowerCase() },
fetchParams: {
method: 'POST',
body,
......@@ -109,7 +107,10 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
variant: 'subtle',
isClosable: true,
});
}, [ formState.isSubmitting, toast ]);
// callback should not change when form is submitted
// otherwise it will resubscribe to channel, but we don't want that since in that case we might miss verification result message
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ toast ]);
const channel = useSocketChannel({
topic: `addresses:${ hash.toLowerCase() }`,
......@@ -124,7 +125,15 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
});
const method = watch('method');
const content = METHOD_COMPONENTS[method] || null;
const content = METHOD_COMPONENTS[method?.value] || null;
const methodValue = method?.value;
useUpdateEffect(() => {
if (methodValue) {
reset(DEFAULT_VALUES[methodValue]);
}
// !!! should run only when method is changed
}, [ methodValue ]);
return (
<FormProvider { ...formApi }>
......@@ -134,8 +143,8 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
>
<ContractVerificationFieldMethod
control={ control }
isDisabled={ Boolean(method) }
methods={ config.verification_options }
isDisabled={ formState.isSubmitting }
/>
{ content }
{ Boolean(method) && (
......
......@@ -15,8 +15,8 @@ const ContractVerificationMethod = ({ title, children }: Props) => {
return (
<section ref={ ref }>
<Text variant="secondary" mt={ 12 } mb={ 5 } fontSize="sm">{ title }</Text>
<Grid columnGap="30px" rowGap={{ base: 2, lg: 4 }} templateColumns={{ base: '1fr', lg: 'minmax(auto, 680px) minmax(0, 340px)' }}>
<Text fontWeight={ 500 } fontSize="lg" fontFamily="heading" mt={ 12 } mb={ 5 }>{ title }</Text>
<Grid columnGap="30px" rowGap={{ base: 2, lg: 5 }} templateColumns={{ base: '1fr', lg: 'minmax(0, 680px) minmax(0, 340px)' }}>
{ children }
</Grid>
</section>
......
......@@ -34,7 +34,6 @@ const ContractVerificationFieldAutodetectArgs = () => {
name="autodetect_constructor_args"
control={ control }
render={ renderControl }
defaultValue={ true }
/>
</ContractVerificationFormRow>
{ !isOn && <ContractVerificationFieldConstructorArgs/> }
......
......@@ -41,7 +41,6 @@ const ContractVerificationFieldCode = ({ isVyper }: Props) => {
control={ control }
render={ renderControl }
rules={{ required: true }}
defaultValue=""
/>
{ isVyper ? null : (
<>
......
import { Code } from '@chakra-ui/react';
import { chakra, Checkbox, Code } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
......@@ -20,26 +20,30 @@ interface Props {
}
const ContractVerificationFieldCompiler = ({ isVyper }: Props) => {
const { formState, control } = useFormContext<FormFields>();
const [ isNightly, setIsNightly ] = React.useState(false);
const { formState, control, getValues, resetField } = useFormContext<FormFields>();
const isMobile = useIsMobile();
const queryClient = useQueryClient();
const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config'));
const handleCheckboxChange = React.useCallback(() => {
if (isNightly) {
const field = getValues('compiler');
field?.value.includes('nightly') && resetField('compiler', { defaultValue: null });
}
setIsNightly(prev => !prev);
}, [ getValues, isNightly, resetField ]);
const options = React.useMemo(() => (
(isVyper ? config?.vyper_compiler_versions : config?.solidity_compiler_versions)?.map((option) => ({ label: option, value: option })) || []
), [ config?.solidity_compiler_versions, config?.vyper_compiler_versions, isVyper ]);
const loadOptions = React.useCallback(async(inputValue: string) => {
return options
.filter(({ label }) => {
if (!inputValue) {
return !label.toLowerCase().includes('nightly');
}
return label.toLowerCase().includes(inputValue.toLowerCase());
})
.filter(({ label }) => !inputValue || label.toLowerCase().includes(inputValue.toLowerCase()))
.filter(({ label }) => isNightly ? true : !label.includes('nightly'))
.slice(0, OPTIONS_LIMIT);
}, [ options ]);
}, [ isNightly, options ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'compiler'>}) => {
const error = 'compiler' in formState.errors ? formState.errors.compiler : undefined;
......@@ -61,20 +65,32 @@ const ContractVerificationFieldCompiler = ({ isVyper }: Props) => {
return (
<ContractVerificationFormRow>
<>
{ !isVyper && (
<Checkbox
size="lg"
mb={ 2 }
onChange={ handleCheckboxChange }
isDisabled={ formState.isSubmitting }
>
Include nightly builds
</Checkbox>
) }
<Controller
name="compiler"
control={ control }
render={ renderControl }
rules={{ required: true }}
/>
</>
{ isVyper ? null : (
<>
<span>The compiler version is specified in </span>
<chakra.div mt={{ base: 0, lg: 8 }}>
<span >The compiler version is specified in </span>
<Code color="text_secondary">pragma solidity X.X.X</Code>
<span>. Use the compiler version rather than the nightly build. If using the Solidity compiler, run </span>
<Code color="text_secondary">solc —version</Code>
<span> to check.</span>
</>
</chakra.div>
) }
</ContractVerificationFormRow>
);
......
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 { useFieldArray, useFormContext } from 'react-hook-form';
......@@ -8,13 +8,21 @@ import ContractVerificationFormRow from '../ContractVerificationFormRow';
import ContractVerificationFieldLibraryItem from './ContractVerificationFieldLibraryItem';
const ContractVerificationFieldLibraries = () => {
const { formState, control } = useFormContext<FormFields>();
const { formState, control, getValues } = useFormContext<FormFields>();
const { fields, append, remove, insert } = useFieldArray({
name: 'libraries',
control,
});
const [ isEnabled, setIsEnabled ] = React.useState(fields.length > 0);
const value = getValues('libraries');
useUpdateEffect(() => {
if (!value || value.length === 0) {
setIsEnabled(false);
}
}, [ value ]);
const handleCheckboxChange = React.useCallback(() => {
if (!isEnabled) {
append({ name: '', address: '' });
......
import {
RadioGroup,
Radio,
Stack,
Text,
Link,
Icon,
chakra,
......@@ -14,16 +10,23 @@ import {
PopoverBody,
useColorModeValue,
DarkMode,
useBoolean,
ListItem,
OrderedList,
Grid,
Box,
} from '@chakra-ui/react';
import React from 'react';
import type { ControllerRenderProps, Control } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { FormFields } from '../types';
import type { SmartContractVerificationMethod, SmartContractVerificationConfig } from 'types/api/contract';
import type { SmartContractVerificationConfig, SmartContractVerificationMethod } from 'types/api/contract';
import infoIcon from 'icons/info.svg';
import useIsMobile from 'lib/hooks/useIsMobile';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
import { METHOD_LABELS } from '../utils';
interface Props {
control: Control<FormFields>;
......@@ -32,91 +35,93 @@ interface Props {
}
const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props) => {
const [ isPopoverOpen, setIsPopoverOpen ] = useBoolean();
const tooltipBg = useColorModeValue('gray.700', 'gray.900');
const isMobile = useIsMobile();
const options = React.useMemo(() => methods.map((method) => ({
value: method,
label: METHOD_LABELS[method],
})), [ methods ]);
const renderItem = React.useCallback((method: SmartContractVerificationMethod) => {
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'method'>}) => {
return (
<FancySelect
{ ...field }
options={ options }
size={ isMobile ? 'md' : 'lg' }
placeholder="Verification method (compiler type)"
isDisabled={ isDisabled }
isRequired
isAsync={ false }
/>
);
}, [ isDisabled, isMobile, options ]);
const renderPopoverListItem = React.useCallback((method: SmartContractVerificationMethod) => {
switch (method) {
case 'flattened-code':
return 'Via flattened source code';
return <ListItem>Verification through flattened source code.</ListItem>;
case 'multi-part':
return <ListItem>Verification of multi-part Solidity files.</ListItem>;
case 'sourcify':
return <ListItem>Verification through <Link href="https://sourcify.dev/" target="_blank">Sourcify</Link>.</ListItem>;
case 'standard-input':
return (
<>
<span>Via standard </span>
<ListItem>
<span>Verification using </span>
<Link
href={ isDisabled ? undefined : 'https://docs.soliditylang.org/en/latest/using-the-compiler.html#input-description' }
href="https://docs.soliditylang.org/en/latest/using-the-compiler.html#input-description"
target="_blank"
cursor={ isDisabled ? 'not-allowed' : 'pointer' }
>
Input JSON
Standard input JSON
</Link>
</>
<span> file.</span>
</ListItem>
);
case 'sourcify':
case 'vyper-code':
return <ListItem>Verification of Vyper contract.</ListItem>;
case 'vyper-multi-part':
return <ListItem>Verification of multi-part Vyper files.</ListItem>;
}
}, []);
return (
<>
<span>Via sourcify: sources and metadata JSON file</span>
<Popover trigger="hover" isLazy isOpen={ isDisabled ? false : isPopoverOpen } onOpen={ setIsPopoverOpen.on } onClose={ setIsPopoverOpen.off }>
<section>
<Grid columnGap="30px" rowGap={{ base: 2, lg: 4 }} templateColumns={{ base: '1fr', lg: 'minmax(auto, 680px) minmax(0, 340px)' }}>
<div>
<Box mb={ 5 }>
<chakra.span fontWeight={ 500 } fontSize="lg" fontFamily="heading">
Currently, Blockscout supports { methods.length } contract verification methods
</chakra.span>
<Popover trigger="hover" isLazy placement={ isMobile ? 'bottom-end' : 'right-start' } offset={ [ -8, 8 ] }>
<PopoverTrigger>
<chakra.span cursor={ isDisabled ? 'not-allowed' : 'pointer' } display="inline-block" verticalAlign="middle" h="24px" ml={ 1 }>
<chakra.span display="inline-block" ml={ 1 } cursor="pointer" verticalAlign="middle" h="22px">
<Icon as={ infoIcon } boxSize={ 5 } color="link" _hover={{ color: 'link_hovered' }}/>
</chakra.span>
</PopoverTrigger>
<Portal>
<PopoverContent bgColor={ tooltipBg }>
<PopoverContent bgColor={ tooltipBg } w={{ base: '300px', lg: '380px' }}>
<PopoverArrow bgColor={ tooltipBg }/>
<PopoverBody color="white">
<DarkMode>
<div>
<span>Verification through </span>
<Link href="https://sourcify.dev/" target="_blank">Sourcify</Link>
</div>
<div>
<span>a) if smart-contract already verified on Sourcify, it will automatically fetch the data from the </span>
<Link href="https://repo.sourcify.dev/" target="_blank">repo</Link>
</div>
<div>
b) otherwise you will be asked to upload source files and JSON metadata file(s).
</div>
<span>Currently, Blockscout supports { methods.length } methods:</span>
<OrderedList>
{ methods.map(renderPopoverListItem) }
</OrderedList>
</DarkMode>
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
</>
);
case 'multi-part':
return 'Via multi-part files';
case 'vyper-code':
return 'Vyper contract';
case 'vyper-multi-part':
return 'Via multi-part Vyper files';
default:
break;
}
}, [ isDisabled, isPopoverOpen, setIsPopoverOpen.off, setIsPopoverOpen.on, tooltipBg ]);
const renderRadioGroup = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'method'>}) => {
return (
<RadioGroup defaultValue="add" colorScheme="blue" isDisabled={ isDisabled } isFocusable={ !isDisabled } { ...field } >
<Stack spacing={ 4 }>
{ methods.map((method) => {
return <Radio key={ method } value={ method } size="lg">{ renderItem(method) }</Radio>;
}) }
</Stack>
</RadioGroup>
);
}, [ isDisabled, methods, renderItem ]);
return (
<section>
<Text variant="secondary" fontSize="sm" mb={ 5 }>Smart-contract verification method</Text>
</Box>
<Controller
name="method"
control={ control }
render={ renderRadioGroup }
render={ renderControl }
rules={{ required: true }}
/>
</div>
</Grid>
</section>
);
};
......
......@@ -41,7 +41,6 @@ const ContractVerificationFieldName = ({ hint }: Props) => {
control={ control }
render={ renderControl }
rules={{ required: true }}
defaultValue=""
/>
{ hint ? <span>{ hint }</span> : (
<>
......
import { FormControl, Input } from '@chakra-ui/react';
import { Flex, Input } from '@chakra-ui/react';
import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
......@@ -6,7 +6,6 @@ import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types';
import CheckboxInput from 'ui/shared/CheckboxInput';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
......@@ -14,56 +13,59 @@ const ContractVerificationFieldOptimization = () => {
const [ isEnabled, setIsEnabled ] = React.useState(true);
const { formState, control } = useFormContext<FormFields>();
const error = 'optimization_runs' in formState.errors ? formState.errors.optimization_runs : undefined;
const handleCheckboxChange = React.useCallback(() => {
setIsEnabled(prev => !prev);
}, []);
const renderCheckboxControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'is_optimization_enabled'>}) => (
<Flex flexShrink={ 0 }>
<CheckboxInput<FormFields, 'is_optimization_enabled'>
text="Optimization enabled"
field={ field }
onChange={ handleCheckboxChange }
isDisabled={ formState.isSubmitting }
/>
</Flex>
), [ formState.isSubmitting, handleCheckboxChange ]);
const renderInputControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'optimization_runs'>}) => {
return (
<FormControl variant="floating" id={ field.name } size={{ base: 'md', lg: 'lg' }} isRequired>
<Input
{ ...field }
required
isDisabled={ formState.isSubmitting }
autoComplete="off"
type="number"
placeholder="Optimization runs"
size="xs"
minW="100px"
maxW="200px"
flexShrink={ 1 }
isInvalid={ Boolean(error) }
/>
<InputPlaceholder text="Optimization runs"/>
</FormControl>
);
}, [ formState.isSubmitting ]);
}, [ error, formState.isSubmitting ]);
return (
<>
<ContractVerificationFormRow>
<Flex columnGap={ 5 } h={{ base: 'auto', lg: '32px' }}>
<Controller
name="is_optimization_enabled"
control={ control }
render={ renderCheckboxControl }
defaultValue={ true }
/>
</ContractVerificationFormRow>
{ isEnabled && (
<ContractVerificationFormRow>
<Controller
name="optimization_runs"
control={ control }
render={ renderInputControl }
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 type { ControllerRenderProps, FieldPathValue, ValidateResult } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
......@@ -6,7 +6,7 @@ import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types';
import { Mb } from 'lib/consts';
// import DragAndDropArea from 'ui/shared/forms/DragAndDropArea';
import DragAndDropArea from 'ui/shared/forms/DragAndDropArea';
import FieldError from 'ui/shared/forms/FieldError';
import FileInput from 'ui/shared/forms/FileInput';
import FileSnippet from 'ui/shared/forms/FileSnippet';
......@@ -19,11 +19,10 @@ interface Props {
fileTypes: Array<FileTypes>;
multiple?: boolean;
title: string;
className?: string;
hint: string;
}
const ContractVerificationFieldSources = ({ fileTypes, multiple, title, className, hint }: Props) => {
const ContractVerificationFieldSources = ({ fileTypes, multiple, title, hint }: Props) => {
const { setValue, getValues, control, formState, clearErrors } = useFormContext<FormFields>();
const error = 'sources' in formState.errors ? formState.errors.sources : undefined;
......@@ -42,11 +41,28 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam
}, [ getValues, clearErrors, setValue ]);
const renderUploadButton = React.useCallback(() => {
return (
<div>
<Text fontWeight={ 500 } color="text_secondary" mb={ 3 }>{ title }</Text>
<Button size="sm" variant="outline">
Drop file{ multiple ? 's' : '' } or click here
</Button>
</div>
);
}, [ multiple, title ]);
const renderFiles = React.useCallback((files: Array<File>) => {
const errorList = fileError?.message?.split(';');
return (
<Box display="grid" gridTemplateColumns={{ base: 'minmax(0, 1fr)', lg: 'minmax(0, 1fr) minmax(0, 1fr)' }} columnGap={ 3 } rowGap={ 3 }>
<Box
display="grid"
gridTemplateColumns={{ base: 'minmax(0, 1fr)', lg: 'minmax(0, 1fr) minmax(0, 1fr)' }}
columnGap={ 3 }
rowGap={ 3 }
w="100%"
>
{ files.map((file, index) => (
<Box key={ file.name + file.lastModified + index }>
<FileSnippet
......@@ -55,42 +71,36 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam
onRemove={ handleFileRemove }
index={ index }
isDisabled={ formState.isSubmitting }
error={ errorList?.[index] }
/>
{ errorList?.[index] && <FieldError message={ errorList?.[index] } mt={ 1 } px={ 3 }/> }
</Box>
)) }
</Box>
);
}, [ formState.isSubmitting, handleFileRemove, fileError ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'sources'>}) => (
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'sources'>}) => {
const hasValue = field.value && field.value.length > 0;
return (
<>
<FileInput<FormFields, 'sources'> accept={ fileTypes.join(',') } multiple={ multiple } field={ field }>
{ () => (
{ ({ onChange }) => (
<Flex
flexDir="column"
alignItems="flex-start"
rowGap={ 2 }
w="100%"
display={ field.value && field.value.length > 0 && !multiple ? 'none' : 'block' }
mb={ field.value && field.value.length > 0 ? 2 : 0 }
>
<Button
variant="outline"
size="sm"
// mb={ 2 }
>
Upload file{ multiple ? 's' : '' }
</Button>
{ /* design is not ready */ }
{ /* <DragAndDropArea onDrop={ onChange }/> */ }
<DragAndDropArea onDrop={ onChange } p={{ base: 3, lg: 6 }} isDisabled={ formState.isSubmitting }>
{ hasValue ? renderFiles(field.value) : renderUploadButton() }
</DragAndDropArea>
</Flex>
) }
</FileInput>
{ field.value && field.value.length > 0 && renderFiles(field.value) }
{ commonError?.message && <FieldError message={ commonError.type === 'required' ? 'Field is required' : commonError.message }/> }
</>
), [ fileTypes, commonError, multiple, renderFiles ]);
);
}, [ fileTypes, multiple, commonError, formState.isSubmitting, renderFiles, renderUploadButton ]);
const validateFileType = React.useCallback(async(value: FieldPathValue<FormFields, 'sources'>): Promise<ValidateResult> => {
if (Array.isArray(value)) {
......@@ -114,19 +124,24 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam
return true;
}, []);
const validateQuantity = React.useCallback(async(value: FieldPathValue<FormFields, 'sources'>): Promise<ValidateResult> => {
if (!multiple && Array.isArray(value) && value.length > 1) {
return 'You can upload only one file';
}
return true;
}, [ multiple ]);
const rules = React.useMemo(() => ({
required: true,
validate: {
file_type: validateFileType,
file_size: validateFileSize,
quantity: validateQuantity,
},
}), [ validateFileSize, validateFileType ]);
}), [ validateFileSize, validateFileType, validateQuantity ]);
return (
<>
<ContractVerificationFormRow >
<Text fontWeight={ 500 } className={ className } mt={ 4 }>{ title }</Text>
</ContractVerificationFormRow>
<ContractVerificationFormRow>
<Controller
name="sources"
......@@ -136,8 +151,7 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam
/>
{ hint ? <span>{ hint }</span> : null }
</ContractVerificationFormRow>
</>
);
};
export default React.memo(chakra(ContractVerificationFieldSources));
export default React.memo(ContractVerificationFieldSources);
......@@ -12,9 +12,9 @@ import ContractVerificationFieldOptimization from '../fields/ContractVerificatio
const ContractVerificationFlattenSourceCode = () => {
return (
<ContractVerificationMethod title="New Solidity/Yul Smart Contract Verification">
<ContractVerificationFieldIsYul/>
<ContractVerificationMethod title="Contract verification via Solidity (fattened source code)">
<ContractVerificationFieldName/>
<ContractVerificationFieldIsYul/>
<ContractVerificationFieldCompiler/>
<ContractVerificationFieldEvmVersion/>
<ContractVerificationFieldOptimization/>
......
......@@ -11,7 +11,7 @@ const FILE_TYPES = [ '.sol' as const, '.yul' as const ];
const ContractVerificationMultiPartFile = () => {
return (
<ContractVerificationMethod title="New Solidity/Yul Smart Contract Verification">
<ContractVerificationMethod title="Contract verification via Solidity (multi-part files)">
<ContractVerificationFieldCompiler/>
<ContractVerificationFieldEvmVersion/>
<ContractVerificationFieldOptimization/>
......
import React from 'react';
import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldContractIndex from '../fields/ContractVerificationFieldContractIndex';
import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources';
const FILE_TYPES = [ '.json' as const, '.sol' as const ];
const ContractVerificationSourcify = () => {
return (
<ContractVerificationMethod title="New Smart Contract Verification">
<ContractVerificationMethod title="Contract verification via Solidity (Sourcify)">
<ContractVerificationFieldSources
fileTypes={ FILE_TYPES }
multiple
title="Sources and Metadata JSON" mt={ 0 }
title="Sources and Metadata JSON"
hint="Upload all Solidity contract source files and JSON metadata file(s) created during contract compilation."
/>
<ContractVerificationFieldContractIndex/>
</ContractVerificationMethod>
);
};
......
......@@ -10,7 +10,7 @@ const FILE_TYPES = [ '.json' as const ];
const ContractVerificationStandardInput = () => {
return (
<ContractVerificationMethod title="New Smart Contract Verification">
<ContractVerificationMethod title="Contract verification via Solidity (standard JSON input) ">
<ContractVerificationFieldName/>
<ContractVerificationFieldCompiler/>
<ContractVerificationFieldSources
......
......@@ -8,7 +8,7 @@ import ContractVerificationFieldName from '../fields/ContractVerificationFieldNa
const ContractVerificationVyperContract = () => {
return (
<ContractVerificationMethod title="New Vyper Smart Contract Verification">
<ContractVerificationMethod title="Contract verification via Vyper (contract)">
<ContractVerificationFieldName hint="Must match the name specified in the code."/>
<ContractVerificationFieldCompiler isVyper/>
<ContractVerificationFieldCode isVyper/>
......
......@@ -9,7 +9,7 @@ const FILE_TYPES = [ '.vy' as const ];
const ContractVerificationVyperMultiPartFile = () => {
return (
<ContractVerificationMethod title="New Vyper Smart Contract Verification">
<ContractVerificationMethod title="Contract verification via Vyper (multi-part files)">
<ContractVerificationFieldCompiler isVyper/>
<ContractVerificationFieldEvmVersion isVyper/>
<ContractVerificationFieldSources
......
import type { SmartContractVerificationMethod } from 'types/api/contract';
import type { Option } from 'ui/shared/FancySelect/types';
export interface ContractLibrary {
name: string;
address: string;
}
interface MethodOption {
label: string;
value: SmartContractVerificationMethod;
}
export interface FormFieldsFlattenSourceCode {
method: 'flattened-code';
method: MethodOption;
is_yul: boolean;
name: string;
compiler: Option;
......@@ -19,7 +26,7 @@ export interface FormFieldsFlattenSourceCode {
}
export interface FormFieldsStandardInput {
method: 'standard-input';
method: MethodOption;
name: string;
compiler: Option;
sources: Array<File>;
......@@ -28,12 +35,13 @@ export interface FormFieldsStandardInput {
}
export interface FormFieldsSourcify {
method: 'sourcify';
method: MethodOption;
sources: Array<File>;
contract_index?: Option;
}
export interface FormFieldsMultiPartFile {
method: 'multi-part';
method: MethodOption;
compiler: Option;
evm_version: Option;
is_optimization_enabled: boolean;
......@@ -43,7 +51,7 @@ export interface FormFieldsMultiPartFile {
}
export interface FormFieldsVyperContract {
method: 'vyper-code';
method: MethodOption;
name: string;
compiler: Option;
code: string;
......@@ -51,7 +59,7 @@ export interface FormFieldsVyperContract {
}
export interface FormFieldsVyperMultiPartFile {
method: 'vyper-multi-part';
method: MethodOption;
compiler: Option;
evm_version: Option;
sources: Array<File>;
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment